From 68ca6198294160e30cee5395daea5013c52a1d2c Mon Sep 17 00:00:00 2001
From: Arturo Corro
Date: Wed, 5 Mar 2025 20:28:54 -0600
Subject: [PATCH] first commit
---
.editorconfig | 18 +
.gitattributes | 38 +
.gitignore | 10 +
.prettierignore | 16 +
.prettierrc.json | 29 +
Actions/Fortify/CreateNewUser.php | 40 +
Actions/Fortify/PasswordValidationRules.php | 18 +
Actions/Fortify/ResetUserPassword.php | 29 +
Actions/Fortify/UpdateUserPassword.php | 32 +
.../Fortify/UpdateUserProfileInformation.php | 60 +
CHANGELOG.md | 41 +
CONTRIBUTING.md | 9 +
Console/Commands/CleanInitialAvatars.php | 43 +
Console/Commands/SyncRBAC.php | 26 +
Helpers/CatalogHelper.php | 72 +
Helpers/VuexyHelper.php | 209 +
Http/Controllers/AdminController.php | 62 +
Http/Controllers/AuthController.php | 144 +
Http/Controllers/CacheController.php | 41 +
Http/Controllers/HomeController.php | 32 +
Http/Controllers/LanguageController.php | 21 +
Http/Controllers/PermissionController.php | 37 +
Http/Controllers/RoleController.php | 38 +
Http/Controllers/RolePermissionController.php | 76 +
Http/Controllers/UserController copy.php | 188 +
Http/Controllers/UserController.php | 234 +
Http/Controllers/UserProfileController.php | 54 +
Http/Middleware/AdminTemplateMiddleware.php | 37 +
Listeners/ClearUserCache.php | 25 +
Listeners/HandleUserLogin.php | 26 +
.../AdminSettings/ApplicationSettings.php | 83 +
Livewire/AdminSettings/GeneralSettings.php | 84 +
Livewire/AdminSettings/InterfaceSettings.php | 118 +
.../MailSenderResponseSettings.php | 106 +
Livewire/AdminSettings/MailSmtpSettings.php | 175 +
Livewire/Cache/CacheFunctions.php | 212 +
Livewire/Cache/CacheStats.php | 65 +
Livewire/Cache/MemcachedStats.php | 64 +
Livewire/Cache/RedisStats.php | 64 +
Livewire/Cache/SessionStats.php | 63 +
Livewire/Form/AbstractFormComponent.php | 515 +
.../Form/AbstractFormOffCanvasComponent.php | 667 +
Livewire/Permissions/PermissionIndex.php | 28 +
Livewire/Permissions/Permissions.php | 35 +
Livewire/Roles/RoleCards.php | 182 +
Livewire/Roles/RoleIndex.php | 61 +
Livewire/Table/AbstractIndexComponent.php | 174 +
Livewire/Users/UserCount.php | 31 +
Livewire/Users/UserForm.php | 306 +
Livewire/Users/UserIndex.copy.php | 115 +
Livewire/Users/UserIndex.php | 299 +
Livewire/Users/UserOffCanvasForm.php | 295 +
Livewire/Users/UserShow.php | 283 +
Models/MediaItem.php | 62 +
Models/Setting.php | 39 +
Models/User copy.php | 377 +
Models/User.php | 237 +
Models/UserLogin.php | 14 +
.../CustomResetPasswordNotification.php | 117 +
Providers/ConfigServiceProvider.php | 31 +
Providers/FortifyServiceProvider.php | 124 +
Providers/VuexyAdminServiceProvider.php | 132 +
Queries/BootstrapTableQueryBuilder.php | 104 +
Queries/GenericQueryBuilder.php | 8 +
README.md | 223 +-
Rules/NotEmptyHtml.php | 20 +
Services/AdminSettingsService.php | 215 +
Services/AdminTemplateService.php | 156 +
Services/AvatarImageService.php | 76 +
Services/AvatarInitialsService.php | 124 +
Services/CacheConfigService.php | 235 +
Services/CacheManagerService.php | 389 +
Services/GlobalSettingsService.php | 225 +
Services/RBACService.php | 28 +
Services/SessionManagerService.php | 153 +
Services/VuexyAdminService.php | 623 +
composer.json | 36 +-
config/fortify.php | 159 +
config/image.php | 42 +
config/koneko.php | 14 +
config/vuexy.php | 36 +
config/vuexy_menu.php | 848 +
database/data/rbac-config.json | 510 +
database/data/users.csv | 14 +
database/factories/UserFactory.php | 49 +
.../2024_12_14_030215_modify_users_table.php | 44 +
..._12_14_035487_create_user_logins_table.php | 36 +
...41_create_personal_access_tokens_table.php | 33 +
..._12_14_074756_create_permission_tables.php | 153 +
..._add_two_factor_columns_to_users_table.php | 46 +
...024_12_14_082234_create_settings_table.php | 37 +
..._12_14_083409_create_media_items_table.php | 48 +
.../2024_12_14_092026_create_audits_table.php | 52 +
database/seeders/PermissionSeeder.php | 14 +
database/seeders/SettingSeeder.php | 108 +
database/seeders/UserSeeder.php | 97 +
resources/assets/css/demo.css | 129 +
.../bootstrap-table/bootstrapTableManager.js | 245 +
.../assets/js/bootstrap-table/globalConfig.js | 132 +
.../js/bootstrap-table/globalFormatters.js | 193 +
resources/assets/js/config.js | 53 +
resources/assets/js/forms/formConvasHelper.js | 477 +
.../assets/js/forms/formCustomListener.js | 245 +
.../assets/js/layout/quicklinks-navbar.js | 56 +
resources/assets/js/layout/search-navbar.js | 201 +
resources/assets/js/main.js | 375 +
resources/assets/js/maps/LeafletMapHelper.js | 133 +
.../assets/js/maps/LocationIQSearchHelper.js | 12 +
.../js/notifications/LivewireNotification.js | 207 +
.../assets/vendor/fonts/bootstrap-icons.scss | 2090 ++
.../bootstrap-icons/bootstrap-icons.woff | Bin 0 -> 176032 bytes
.../bootstrap-icons/bootstrap-icons.woff2 | Bin 0 -> 130396 bytes
.../assets/vendor/fonts/fontawesome.scss | 7 +
.../fonts/fontawesome/fa-brands-400.ttf | Bin 0 -> 207972 bytes
.../fonts/fontawesome/fa-brands-400.woff2 | Bin 0 -> 117372 bytes
.../fonts/fontawesome/fa-regular-400.ttf | Bin 0 -> 68004 bytes
.../fonts/fontawesome/fa-regular-400.woff2 | Bin 0 -> 25452 bytes
.../vendor/fonts/fontawesome/fa-solid-900.ttf | Bin 0 -> 419720 bytes
.../fonts/fontawesome/fa-solid-900.woff2 | Bin 0 -> 156496 bytes
.../fonts/fontawesome/fa-v4compatibility.ttf | Bin 0 -> 10832 bytes
.../fontawesome/fa-v4compatibility.woff2 | Bin 0 -> 4792 bytes
.../assets/vendor/fonts/tabler-icons.scss | 21294 ++++++++++++++++
.../vendor/fonts/tabler/tabler-icons.eot | Bin 0 -> 2074368 bytes
.../vendor/fonts/tabler/tabler-icons.svg | 6384 +++++
.../vendor/fonts/tabler/tabler-icons.ttf | Bin 0 -> 2554500 bytes
.../vendor/fonts/tabler/tabler-icons.woff | Bin 0 -> 1211516 bytes
.../vendor/fonts/tabler/tabler-icons.woff2 | Bin 0 -> 834652 bytes
.../_template-customizer.html | 101 +
.../_template-customizer.scss | 342 +
resources/assets/vendor/js/bootstrap.js | 7 +
resources/assets/vendor/js/dropdown-hover.js | 73 +
resources/assets/vendor/js/helpers.js | 1151 +
resources/assets/vendor/js/mega-dropdown.js | 206 +
resources/assets/vendor/js/menu.js | 991 +
.../assets/vendor/js/template-customizer.js | 1400 +
.../libs/@form-validation/auto-focus.js | 7 +
.../libs/@form-validation/bootstrap5.js | 7 +
.../@form-validation/form-validation.scss | 4 +
.../vendor/libs/@form-validation/popular.js | 8 +
.../assets/vendor/libs/_tabler/_tabler.scss | 414 +
.../vendor/libs/animate-css/animate.scss | 1 +
.../libs/bootstrap-datepicker/_mixins.scss | 93 +
.../bootstrap-datepicker.js | 1 +
.../bootstrap-datepicker.scss | 482 +
.../bootstrap-daterangepicker/_mixins.scss | 81 +
.../bootstrap-daterangepicker.js | 18 +
.../bootstrap-daterangepicker.scss | 744 +
.../vendor/libs/bootstrap-select/_mixins.scss | 16 +
.../libs/bootstrap-select/bootstrap-select.js | 1 +
.../bootstrap-select/bootstrap-select.scss | 259 +
.../libs/bootstrap-table/bootstrap-table.js | 9 +
.../libs/bootstrap-table/bootstrap-table.scss | 2 +
.../vendor/libs/bs-stepper/_mixins.scss | 51 +
.../vendor/libs/bs-stepper/bs-stepper.js | 37 +
.../vendor/libs/bs-stepper/bs-stepper.scss | 549 +
.../vendor/libs/cleavejs/cleave-phone.js | 1 +
.../assets/vendor/libs/cleavejs/cleave.js | 1 +
.../datatables-bs5/datatables-bootstrap5.js | 28 +
.../datatables-bs5/datatables.bootstrap5.scss | 394 +
.../buttons.bootstrap5.scss | 101 +
.../datatables.checkboxes.scss | 2 +
.../fixedcolumns.bootstrap5.scss | 133 +
.../fixedheader.bootstrap5.scss | 40 +
.../libs/datatables-lang/datatable-lang-es.js | 195 +
.../datatables-responsive-bs5/_mixins.scss | 9 +
.../responsive.bootstrap5.scss | 57 +
.../rowgroup.bootstrap5.scss | 25 +
.../select.bootstrap5.scss | 39 +
.../assets/vendor/libs/dropzone/_mixins.scss | 5 +
.../assets/vendor/libs/dropzone/dropzone.js | 56 +
.../assets/vendor/libs/dropzone/dropzone.scss | 460 +
.../assets/vendor/libs/flatpickr/_mixins.scss | 114 +
.../assets/vendor/libs/flatpickr/flatpickr.js | 7 +
.../vendor/libs/flatpickr/flatpickr.scss | 1172 +
.../vendor/libs/fullcalendar/_mixins.scss | 23 +
.../vendor/libs/fullcalendar/fullcalendar.js | 22 +
.../libs/fullcalendar/fullcalendar.scss | 535 +
resources/assets/vendor/libs/hammer/hammer.js | 1 +
.../libs/jquery-timepicker/_mixins.scss | 11 +
.../jquery-timepicker/jquery-timepicker.js | 1 +
.../jquery-timepicker/jquery-timepicker.scss | 116 +
resources/assets/vendor/libs/jquery/jquery.js | 8 +
.../vendor/libs/leaflet/images/layers-2x.png | Bin 0 -> 1259 bytes
.../vendor/libs/leaflet/images/layers.png | Bin 0 -> 696 bytes
.../libs/leaflet/images/marker-icon-2x.png | Bin 0 -> 2464 bytes
.../libs/leaflet/images/marker-icon.png | Bin 0 -> 1466 bytes
.../libs/leaflet/images/marker-shadow.png | Bin 0 -> 618 bytes
.../assets/vendor/libs/leaflet/leaflet.js | 19 +
.../assets/vendor/libs/leaflet/leaflet.scss | 46 +
resources/assets/vendor/libs/moment/moment.js | 7 +
.../vendor/libs/node-waves/node-waves.js | 3 +
.../vendor/libs/node-waves/node-waves.scss | 4 +
.../vendor/libs/nouislider/_mixins.scss | 26 +
.../vendor/libs/nouislider/nouislider.js | 7 +
.../vendor/libs/nouislider/nouislider.scss | 398 +
.../perfect-scrollbar/perfect-scrollbar.js | 7 +
.../perfect-scrollbar/perfect-scrollbar.scss | 177 +
.../assets/vendor/libs/pickr/_mixins.scss | 11 +
.../vendor/libs/pickr/_pickr-classic.scss | 15 +
.../vendor/libs/pickr/_pickr-monolith.scss | 10 +
.../assets/vendor/libs/pickr/_pickr-nano.scss | 7 +
.../vendor/libs/pickr/pickr-themes.scss | 39 +
resources/assets/vendor/libs/pickr/pickr.js | 7 +
.../assets/vendor/libs/plyr/_mixins.scss | 52 +
resources/assets/vendor/libs/plyr/plyr.js | 7 +
resources/assets/vendor/libs/plyr/plyr.scss | 132 +
resources/assets/vendor/libs/popper/popper.js | 10 +
.../assets/vendor/libs/quill/_mixins.scss | 115 +
.../assets/vendor/libs/quill/editor.scss | 1115 +
resources/assets/vendor/libs/quill/katex.js | 7 +
resources/assets/vendor/libs/quill/katex.scss | 1 +
resources/assets/vendor/libs/quill/quill.js | 7 +
.../assets/vendor/libs/quill/typography.scss | 289 +
.../assets/vendor/libs/select2/_mixins.scss | 38 +
resources/assets/vendor/libs/select2/es.js | 3 +
.../assets/vendor/libs/select2/select2.js | 8 +
.../assets/vendor/libs/select2/select2.scss | 890 +
.../assets/vendor/libs/shepherd/_mixins.scss | 8 +
.../assets/vendor/libs/shepherd/shepherd.js | 7 +
.../assets/vendor/libs/shepherd/shepherd.scss | 144 +
.../assets/vendor/libs/spinkit/_mixins.scss | 14 +
.../assets/vendor/libs/spinkit/spinkit.scss | 25 +
.../vendor/libs/sweetalert2/_mixins.scss | 35 +
.../vendor/libs/sweetalert2/sweetalert2.js | 16 +
.../vendor/libs/sweetalert2/sweetalert2.scss | 314 +
.../assets/vendor/libs/swiper/_mixins.scss | 6 +
resources/assets/vendor/libs/swiper/swiper.js | 7 +
.../assets/vendor/libs/swiper/swiper.scss | 113 +
.../assets/vendor/libs/tagify/_mixins.scss | 8 +
.../libs/tagify/_tagify-email-list.scss | 105 +
.../tagify/_tagify-inline-suggestion.scss | 67 +
.../libs/tagify/_tagify-users-list.scss | 124 +
resources/assets/vendor/libs/tagify/tagify.js | 7 +
.../assets/vendor/libs/tagify/tagify.scss | 246 +
.../vendor/libs/typeahead-js/_mixins.scss | 11 +
.../vendor/libs/typeahead-js/typeahead.js | 7 +
.../vendor/libs/typeahead-js/typeahead.scss | 144 +
.../assets/vendor/scss/_bootstrap-dark.scss | 39 +
.../vendor/scss/_bootstrap-extended-dark.scss | 40 +
.../vendor/scss/_bootstrap-extended.scss | 39 +
.../scss/_bootstrap-extended/_accordion.scss | 250 +
.../scss/_bootstrap-extended/_alert.scss | 53 +
.../scss/_bootstrap-extended/_badge.scss | 53 +
.../scss/_bootstrap-extended/_breadcrumb.scss | 62 +
.../_bootstrap-extended/_button-group.scss | 161 +
.../scss/_bootstrap-extended/_buttons.scss | 178 +
.../scss/_bootstrap-extended/_card.scss | 389 +
.../scss/_bootstrap-extended/_carousel.scss | 50 +
.../scss/_bootstrap-extended/_close.scss | 12 +
.../scss/_bootstrap-extended/_dropdown.scss | 126 +
.../scss/_bootstrap-extended/_forms.scss | 12 +
.../scss/_bootstrap-extended/_functions.scss | 148 +
.../_bootstrap-extended/_include-dark.scss | 15 +
.../scss/_bootstrap-extended/_include.scss | 13 +
.../scss/_bootstrap-extended/_list-group.scss | 210 +
.../scss/_bootstrap-extended/_mixins.scss | 20 +
.../scss/_bootstrap-extended/_modal.scss | 390 +
.../vendor/scss/_bootstrap-extended/_nav.scss | 510 +
.../scss/_bootstrap-extended/_navbar.scss | 87 +
.../scss/_bootstrap-extended/_offcanvas.scss | 33 +
.../scss/_bootstrap-extended/_pagination.scss | 226 +
.../scss/_bootstrap-extended/_popover.scss | 42 +
.../scss/_bootstrap-extended/_progress.scss | 44 +
.../scss/_bootstrap-extended/_reboot.scss | 74 +
.../scss/_bootstrap-extended/_root.scss | 4 +
.../scss/_bootstrap-extended/_spinners.scss | 44 +
.../scss/_bootstrap-extended/_tables.scss | 136 +
.../scss/_bootstrap-extended/_toasts.scss | 51 +
.../scss/_bootstrap-extended/_tooltip.scss | 58 +
.../scss/_bootstrap-extended/_type.scss | 13 +
.../_bootstrap-extended/_utilities-ltr.scss | 299 +
.../_bootstrap-extended/_utilities-rtl.scss | 301 +
.../scss/_bootstrap-extended/_utilities.scss | 1081 +
.../_bootstrap-extended/_variables-dark.scss | 236 +
.../scss/_bootstrap-extended/_variables.scss | 1090 +
.../forms/_floating-labels.scss | 36 +
.../forms/_form-check.scss | 79 +
.../forms/_form-control.scss | 73 +
.../forms/_form-range.scss | 49 +
.../forms/_form-select.scss | 109 +
.../_bootstrap-extended/forms/_form-text.scss | 2 +
.../forms/_input-group.scss | 393 +
.../_bootstrap-extended/forms/_labels.scss | 17 +
.../forms/_validation.scss | 130 +
.../_bootstrap-extended/mixins/_alert.scss | 101 +
.../_bootstrap-extended/mixins/_badge.scss | 9 +
.../_bootstrap-extended/mixins/_buttons.scss | 360 +
.../_bootstrap-extended/mixins/_card.scss | 32 +
.../_bootstrap-extended/mixins/_caret.scss | 64 +
.../_bootstrap-extended/mixins/_dropdown.scss | 33 +
.../_bootstrap-extended/mixins/_forms.scss | 361 +
.../mixins/_list-group.scss | 86 +
.../_bootstrap-extended/mixins/_misc.scss | 158 +
.../_bootstrap-extended/mixins/_modal.scss | 20 +
.../_bootstrap-extended/mixins/_navs.scss | 101 +
.../mixins/_pagination.scss | 73 +
.../_bootstrap-extended/mixins/_popover.scss | 53 +
.../_bootstrap-extended/mixins/_progress.scss | 14 +
.../mixins/_table-variants.scss | 26 +
.../_bootstrap-extended/mixins/_tooltip.scss | 35 +
resources/assets/vendor/scss/_bootstrap.scss | 43 +
.../assets/vendor/scss/_colors-dark.scss | 136 +
resources/assets/vendor/scss/_colors.scss | 126 +
.../assets/vendor/scss/_components-dark.scss | 16 +
resources/assets/vendor/scss/_components.scss | 16 +
.../vendor/scss/_components/_app-brand.scss | 75 +
.../vendor/scss/_components/_avatar.scss | 157 +
.../assets/vendor/scss/_components/_base.scss | 183 +
.../vendor/scss/_components/_blockui.scss | 10 +
.../vendor/scss/_components/_common.scss | 255 +
.../scss/_components/_custom-options.scss | 161 +
.../vendor/scss/_components/_drag-drop.scss | 13 +
.../vendor/scss/_components/_footer.scss | 78 +
.../scss/_components/_include-dark.scss | 13 +
.../vendor/scss/_components/_include.scss | 11 +
.../vendor/scss/_components/_layout.scss | 1181 +
.../assets/vendor/scss/_components/_menu.scss | 784 +
.../vendor/scss/_components/_mixins.scss | 9 +
.../vendor/scss/_components/_switch.scss | 239 +
.../scss/_components/_text-divider.scss | 180 +
.../vendor/scss/_components/_timeline.scss | 363 +
.../scss/_components/_variables-dark.scss | 25 +
.../vendor/scss/_components/_variables.scss | 168 +
.../scss/_components/mixins/_app-brand.scss | 29 +
.../scss/_components/mixins/_avatar.scss | 22 +
.../scss/_components/mixins/_footer.scss | 47 +
.../vendor/scss/_components/mixins/_menu.scss | 133 +
.../vendor/scss/_components/mixins/_misc.scss | 6 +
.../scss/_components/mixins/_navbar.scss | 90 +
.../scss/_components/mixins/_switch.scss | 179 +
.../_components/mixins/_text-divider.scss | 17 +
.../scss/_components/mixins/_timeline.scss | 33 +
.../scss/_components/mixins/_treeview.scss | 30 +
.../_bootstrap-extended-dark.scss | 6 +
.../_bootstrap-extended.scss | 7 +
.../_custom-variables/_components-dark.scss | 5 +
.../scss/_custom-variables/_components.scss | 6 +
.../vendor/scss/_custom-variables/_libs.scss | 8 +
.../vendor/scss/_custom-variables/_pages.scss | 8 +
.../scss/_custom-variables/_support.scss | 51 +
.../assets/vendor/scss/_theme/_common.scss | 75 +
.../assets/vendor/scss/_theme/_libs.scss | 75 +
.../assets/vendor/scss/_theme/_pages.scss | 10 +
.../assets/vendor/scss/_theme/_theme.scss | 103 +
resources/assets/vendor/scss/core-dark.scss | 4 +
resources/assets/vendor/scss/core.scss | 4 +
.../assets/vendor/scss/pages/_mixins.scss | 78 +
.../vendor/scss/theme-bordered-dark.scss | 343 +
.../assets/vendor/scss/theme-bordered.scss | 343 +
.../vendor/scss/theme-default-dark.scss | 94 +
.../assets/vendor/scss/theme-default.scss | 93 +
.../vendor/scss/theme-raspberry-dark.scss | 136 +
.../assets/vendor/scss/theme-raspberry.scss | 137 +
.../vendor/scss/theme-semi-dark-dark.scss | 87 +
.../assets/vendor/scss/theme-semi-dark.scss | 89 +
resources/img/avatar/generic.svg | 26 +
resources/img/customizer/border-dark.svg | 16 +
resources/img/customizer/border.svg | 16 +
resources/img/customizer/collapsed-dark.svg | 17 +
resources/img/customizer/collapsed.svg | 17 +
resources/img/customizer/compact-dark.svg | 11 +
resources/img/customizer/compact.svg | 15 +
resources/img/customizer/dark-dark.svg | 5 +
resources/img/customizer/dark.svg | 5 +
resources/img/customizer/default-dark.svg | 17 +
resources/img/customizer/default.svg | 26 +
resources/img/customizer/expanded-dark.svg | 17 +
resources/img/customizer/expanded.svg | 26 +
resources/img/customizer/hidden-dark.svg | 12 +
resources/img/customizer/hidden.svg | 12 +
.../img/customizer/horizontal-fixed-dark.svg | 12 +
resources/img/customizer/horizontal-fixed.svg | 12 +
.../img/customizer/horizontal-static-dark.svg | 12 +
.../img/customizer/horizontal-static.svg | 12 +
resources/img/customizer/light-dark.svg | 4 +
resources/img/customizer/light.svg | 4 +
resources/img/customizer/ltr-dark.svg | 15 +
resources/img/customizer/ltr.svg | 25 +
resources/img/customizer/rtl-dark.svg | 15 +
resources/img/customizer/rtl.svg | 15 +
resources/img/customizer/semi-dark-dark.svg | 23 +
resources/img/customizer/semi-dark.svg | 23 +
resources/img/customizer/static-dark.svg | 17 +
resources/img/customizer/static.svg | 17 +
resources/img/customizer/sticky-dark.svg | 17 +
resources/img/customizer/sticky.svg | 17 +
resources/img/customizer/system-dark.svg | 10 +
resources/img/customizer/system.svg | 10 +
resources/img/customizer/vertical-dark.svg | 17 +
resources/img/customizer/vertical.svg | 26 +
resources/img/customizer/wide-dark.svg | 11 +
resources/img/customizer/wide.svg | 11 +
...auth-forgot-password-illustration-dark.png | Bin 0 -> 49459 bytes
...uth-forgot-password-illustration-light.png | Bin 0 -> 48508 bytes
.../auth-login-illustration-dark.png | Bin 0 -> 57789 bytes
.../auth-login-illustration-light.png | Bin 0 -> 55767 bytes
.../auth-register-illustration-dark.png | Bin 0 -> 89300 bytes
.../auth-register-illustration-light.png | Bin 0 -> 90351 bytes
.../auth-register-multisteps-illustration.png | Bin 0 -> 33754 bytes
.../auth-register-multisteps-shape-dark.png | Bin 0 -> 3083 bytes
.../auth-register-multisteps-shape-light.png | Bin 0 -> 2934 bytes
.../auth-reset-password-illustration-dark.png | Bin 0 -> 45127 bytes
...auth-reset-password-illustration-light.png | Bin 0 -> 45817 bytes
.../auth-two-step-illustration-dark.png | Bin 0 -> 52283 bytes
.../auth-two-step-illustration-light.png | Bin 0 -> 67640 bytes
.../auth-verify-email-illustration-dark.png | Bin 0 -> 60363 bytes
.../auth-verify-email-illustration-light.png | Bin 0 -> 60830 bytes
.../img/illustrations/bg-shape-image-dark.png | Bin 0 -> 1902 bytes
.../illustrations/bg-shape-image-light.png | Bin 0 -> 1756 bytes
.../img/illustrations/page-misc-error.png | Bin 0 -> 34200 bytes
.../page-misc-launching-soon.png | Bin 0 -> 33046 bytes
.../page-misc-under-maintenance.png | Bin 0 -> 97471 bytes
.../page-misc-you-are-not-authorized.png | Bin 0 -> 30627 bytes
resources/img/logo/horizontal-02.png | Bin 0 -> 224617 bytes
resources/img/logo/horizontal-04.png | Bin 0 -> 224629 bytes
resources/img/logo/horizontal-circulo-01.png | Bin 0 -> 126161 bytes
resources/img/logo/horizontal-circulo-03.png | Bin 0 -> 127637 bytes
resources/img/logo/koneko-01.png | Bin 0 -> 81082 bytes
resources/img/logo/koneko-02.png | Bin 0 -> 98693 bytes
resources/img/logo/koneko-03.png | Bin 0 -> 83847 bytes
resources/img/logo/koneko-04.png | Bin 0 -> 98102 bytes
resources/img/logo/vertical-02.png | Bin 0 -> 613798 bytes
resources/img/logo/vertical-04.png | Bin 0 -> 613309 bytes
resources/js/app.js | 83 +
resources/js/auth/app-access-permission.js | 206 +
resources/js/auth/app-access-roles.js | 428 +
resources/js/auth/app-user-list.js | 533 +
resources/js/auth/app-user-view-account.js | 222 +
resources/js/auth/app-user-view-billing.js | 57 +
resources/js/auth/app-user-view-security.js | 63 +
resources/js/auth/app-user-view.js | 89 +
resources/js/auth/modal-add-new-address.js | 73 +
resources/js/auth/modal-add-new-cc.js | 107 +
resources/js/auth/modal-add-permission.js | 35 +
resources/js/auth/modal-add-role.js | 44 +
resources/js/auth/modal-edit-cc.js | 79 +
resources/js/auth/modal-edit-permission.js | 35 +
resources/js/auth/modal-edit-user.js | 103 +
resources/js/auth/modal-enable-otp.js | 53 +
.../js/auth/pages-account-settings-account.js | 189 +
.../js/auth/pages-account-settings-billing.js | 194 +
.../auth/pages-account-settings-security.js | 125 +
resources/js/auth/pages-auth-multisteps.js | 305 +
resources/js/auth/pages-auth-two-steps.js | 83 +
resources/js/auth/pages-auth.js | 112 +
resources/js/bootstrap.js | 4 +
resources/js/pages/admin-settings-scripts.js | 27 +
resources/js/pages/cache-manager-scripts.js | 99 +
resources/js/pages/permissions-scripts.js | 197 +
resources/js/pages/roles-scripts.js | 1 +
resources/js/pages/smtp-settings-scripts.js | 25 +
.../js/smtp-settings/SenderResponseForm.js | 220 +
.../js/smtp-settings/SmtpSettingsForm.js | 239 +
resources/lang/es/auth.php | 18 +
resources/lang/es/errors.php | 10 +
resources/lang/es/locale.php | 291 +
resources/lang/es/messages.php | 740 +
resources/lang/es/pagination.php | 17 +
resources/lang/es/passwords.php | 20 +
resources/lang/es/validation-inline.php | 133 +
resources/lang/es/validation.php | 196 +
resources/lang/es_MX.json | 732 +
resources/scss/app.scss | 415 +
.../scss/pages/page-account-settings.scss | 16 +
resources/scss/pages/page-auth.scss | 169 +
resources/scss/pages/page-misc.scss | 34 +
resources/scss/pages/page-profile.scss | 66 +
resources/scss/pages/page-user-view.scss | 104 +
.../admin-settings/smtp-settings.blade.php | 36 +
.../webapp-general-settings.blade.php | 30 +
.../auth/auth-register-multisteps.blade.php | 356 +
.../views/auth/auth-two-steps-basic.blade.php | 86 +
.../views/auth/auth-two-steps-cover.blade.php | 96 +
.../auth/auth-verify-email-basic.blade.php | 47 +
.../auth/auth-verify-email-cover.blade.php | 57 +
.../auth/forgot-password-basic.blade.php | 97 +
.../auth/forgot-password-cover.blade.php | 104 +
resources/views/auth/login-basic.blade.php | 134 +
resources/views/auth/login-cover.blade.php | 133 +
resources/views/auth/register-basic.blade.php | 151 +
resources/views/auth/register-cover.blade.php | 132 +
.../views/auth/reset-password-basic.blade.php | 130 +
.../views/auth/reset-password-cover.blade.php | 137 +
resources/views/cache-manager/index.blade.php | 41 +
.../views/components/button/basic.blade.php | 63 +
.../views/components/button/group.blade.php | 46 +
.../button/index-off-canvas.blade.php | 30 +
.../button/offcanvas-buttons.blade.php | 18 +
.../views/components/card/basic.blade.php | 76 +
.../views/components/file/dropzone.blade.php | 70 +
.../components/form/checkbox-group.blade.php | 148 +
.../views/components/form/checkbox.blade.php | 133 +
.../components/form/custom-option.blade.php | 54 +
.../views/components/form/form.blade.php | 48 +
.../components/form/input.blade copy.php | 184 +
.../views/components/form/input.blade.php | 169 +
.../components/form/radio-group.blade.php | 149 +
.../views/components/form/radio.blade.php | 125 +
.../components/form/select.blade copy.php | 86 +
.../views/components/form/select.blade.php | 151 +
.../views/components/form/textarea.blade.php | 178 +
.../components/offcanvas/basic.blade.php | 78 +
.../table/bootstrap/manager.blade.php | 66 +
resources/views/errors/400.blade.php | 38 +
resources/views/errors/401.blade.php | 38 +
resources/views/errors/403.blade.php | 38 +
resources/views/errors/404.blade.php | 34 +
.../views/layouts/vuexy/blankLayout.blade.php | 17 +
.../layouts/vuexy/commonMaster.blade.php | 58 +
.../vuexy/contentNavbarLayout.blade.php | 89 +
.../layouts/vuexy/horizontalLayout.blade.php | 87 +
.../layouts/vuexy/layoutMaster.blade.php | 16 +
.../sections/content/breadcrumbs.blade.php | 21 +
.../vuexy/sections/footer/footer.blade.php | 23 +
.../sections/menu/horizontalMenu.blade.php | 28 +
.../vuexy/sections/menu/submenu.blade.php | 18 +
.../sections/menu/verticalMenu.blade.php | 51 +
.../vuexy/sections/navbar/navbar.blade.php | 233 +
.../layouts/vuexy/sections/scripts.blade.php | 24 +
.../vuexy/sections/scriptsIncludes.blade.php | 45 +
.../layouts/vuexy/sections/styles.blade.php | 30 +
.../application-settings.blade.php | 70 +
.../admin-settings/general-settings.blade.php | 105 +
.../interface-settings.blade.php | 196 +
.../mail-sender-response-settings.blade.php | 67 +
.../mail-smtp-settings.blade.php | 86 +
.../livewire/cache/cache-functions.blade.php | 204 +
.../livewire/cache/cache-stats.blade.php | 105 +
.../livewire/cache/memcached-stats.blade.php | 121 +
.../livewire/cache/redis-stats.blade.php | 144 +
.../livewire/cache/session-stats.blade.php | 121 +
.../livewire/permissions/index.blade.php | 152 +
.../permissions/permissions.blade.php | 17 +
.../views/livewire/roles/cards.blade.php | 347 +
.../views/livewire/roles/index.blade.php | 46 +
.../views/livewire/users/count.blade.php | 60 +
resources/views/livewire/users/form.blade.php | 165 +
.../views/livewire/users/index.blade.copy.php | 704 +
.../views/livewire/users/index.blade.php | 7 +
.../livewire/users/offcanvas-form.blade.php | 52 +
resources/views/livewire/users/show.blade.php | 1685 ++
resources/views/notifications/email.blade.php | 65 +
resources/views/pages/about.blade.php | 9 +
resources/views/pages/comingsoon.blade.php | 37 +
resources/views/pages/home.blade.php | 49 +
.../views/pages/under-maintenance.blade.php | 30 +
resources/views/permissions/index.blade.php | 30 +
resources/views/profile/index.blade.php | 38 +
resources/views/roles/_delete_modal.blade.php | 25 +
resources/views/roles/_form_modal.blade.php | 116 +
resources/views/roles/index.blade.php | 21 +
.../app-access-permission.blade.php | 57 +
.../user-profile/app-access-roles.blade.php | 239 +
.../user-profile/app-user-list.blade.php | 226 +
.../app-user-view-account.blade.php | 305 +
.../app-user-view-connections.blade.php | 345 +
.../app-user-view-notifications.blade.php | 277 +
.../app-user-view-security.blade.php | 280 +
resources/views/user-profile/index.blade.php | 7 +
resources/views/users/index.blade.php | 29 +
resources/views/users/show.blade.php | 214 +
routes/admin.php | 79 +
storage/fonts/OpenSans-Bold.ttf | Bin 0 -> 130860 bytes
vendor/autoload.php | 2 +-
vendor/composer/autoload_classmap.php | 8164 ++++++
vendor/composer/autoload_namespaces.php | 1 +
vendor/composer/autoload_psr4.php | 115 +-
vendor/composer/autoload_real.php | 20 +-
vendor/composer/autoload_static.php | 8856 ++++++-
vendor/composer/platform_check.php | 8 +-
570 files changed, 111124 insertions(+), 175 deletions(-)
create mode 100644 .editorconfig
create mode 100644 .gitattributes
create mode 100644 .gitignore
create mode 100644 .prettierignore
create mode 100644 .prettierrc.json
create mode 100644 Actions/Fortify/CreateNewUser.php
create mode 100644 Actions/Fortify/PasswordValidationRules.php
create mode 100644 Actions/Fortify/ResetUserPassword.php
create mode 100644 Actions/Fortify/UpdateUserPassword.php
create mode 100644 Actions/Fortify/UpdateUserProfileInformation.php
create mode 100644 CHANGELOG.md
create mode 100644 CONTRIBUTING.md
create mode 100644 Console/Commands/CleanInitialAvatars.php
create mode 100644 Console/Commands/SyncRBAC.php
create mode 100644 Helpers/CatalogHelper.php
create mode 100644 Helpers/VuexyHelper.php
create mode 100644 Http/Controllers/AdminController.php
create mode 100644 Http/Controllers/AuthController.php
create mode 100644 Http/Controllers/CacheController.php
create mode 100644 Http/Controllers/HomeController.php
create mode 100644 Http/Controllers/LanguageController.php
create mode 100644 Http/Controllers/PermissionController.php
create mode 100644 Http/Controllers/RoleController.php
create mode 100644 Http/Controllers/RolePermissionController.php
create mode 100644 Http/Controllers/UserController copy.php
create mode 100644 Http/Controllers/UserController.php
create mode 100644 Http/Controllers/UserProfileController.php
create mode 100644 Http/Middleware/AdminTemplateMiddleware.php
create mode 100644 Listeners/ClearUserCache.php
create mode 100644 Listeners/HandleUserLogin.php
create mode 100644 Livewire/AdminSettings/ApplicationSettings.php
create mode 100644 Livewire/AdminSettings/GeneralSettings.php
create mode 100644 Livewire/AdminSettings/InterfaceSettings.php
create mode 100644 Livewire/AdminSettings/MailSenderResponseSettings.php
create mode 100644 Livewire/AdminSettings/MailSmtpSettings.php
create mode 100644 Livewire/Cache/CacheFunctions.php
create mode 100644 Livewire/Cache/CacheStats.php
create mode 100644 Livewire/Cache/MemcachedStats.php
create mode 100644 Livewire/Cache/RedisStats.php
create mode 100644 Livewire/Cache/SessionStats.php
create mode 100644 Livewire/Form/AbstractFormComponent.php
create mode 100644 Livewire/Form/AbstractFormOffCanvasComponent.php
create mode 100644 Livewire/Permissions/PermissionIndex.php
create mode 100644 Livewire/Permissions/Permissions.php
create mode 100644 Livewire/Roles/RoleCards.php
create mode 100644 Livewire/Roles/RoleIndex.php
create mode 100644 Livewire/Table/AbstractIndexComponent.php
create mode 100644 Livewire/Users/UserCount.php
create mode 100644 Livewire/Users/UserForm.php
create mode 100644 Livewire/Users/UserIndex.copy.php
create mode 100644 Livewire/Users/UserIndex.php
create mode 100644 Livewire/Users/UserOffCanvasForm.php
create mode 100644 Livewire/Users/UserShow.php
create mode 100644 Models/MediaItem.php
create mode 100644 Models/Setting.php
create mode 100644 Models/User copy.php
create mode 100644 Models/User.php
create mode 100644 Models/UserLogin.php
create mode 100644 Notifications/CustomResetPasswordNotification.php
create mode 100644 Providers/ConfigServiceProvider.php
create mode 100644 Providers/FortifyServiceProvider.php
create mode 100644 Providers/VuexyAdminServiceProvider.php
create mode 100644 Queries/BootstrapTableQueryBuilder.php
create mode 100644 Queries/GenericQueryBuilder.php
create mode 100644 Rules/NotEmptyHtml.php
create mode 100644 Services/AdminSettingsService.php
create mode 100644 Services/AdminTemplateService.php
create mode 100644 Services/AvatarImageService.php
create mode 100644 Services/AvatarInitialsService.php
create mode 100644 Services/CacheConfigService.php
create mode 100644 Services/CacheManagerService.php
create mode 100644 Services/GlobalSettingsService.php
create mode 100644 Services/RBACService.php
create mode 100644 Services/SessionManagerService.php
create mode 100644 Services/VuexyAdminService.php
create mode 100644 config/fortify.php
create mode 100644 config/image.php
create mode 100644 config/koneko.php
create mode 100644 config/vuexy.php
create mode 100644 config/vuexy_menu.php
create mode 100644 database/data/rbac-config.json
create mode 100644 database/data/users.csv
create mode 100644 database/factories/UserFactory.php
create mode 100644 database/migrations/2024_12_14_030215_modify_users_table.php
create mode 100644 database/migrations/2024_12_14_035487_create_user_logins_table.php
create mode 100644 database/migrations/2024_12_14_073441_create_personal_access_tokens_table.php
create mode 100644 database/migrations/2024_12_14_074756_create_permission_tables.php
create mode 100644 database/migrations/2024_12_14_081739_add_two_factor_columns_to_users_table.php
create mode 100644 database/migrations/2024_12_14_082234_create_settings_table.php
create mode 100644 database/migrations/2024_12_14_083409_create_media_items_table.php
create mode 100644 database/migrations/2024_12_14_092026_create_audits_table.php
create mode 100644 database/seeders/PermissionSeeder.php
create mode 100644 database/seeders/SettingSeeder.php
create mode 100644 database/seeders/UserSeeder.php
create mode 100644 resources/assets/css/demo.css
create mode 100644 resources/assets/js/bootstrap-table/bootstrapTableManager.js
create mode 100644 resources/assets/js/bootstrap-table/globalConfig.js
create mode 100644 resources/assets/js/bootstrap-table/globalFormatters.js
create mode 100644 resources/assets/js/config.js
create mode 100644 resources/assets/js/forms/formConvasHelper.js
create mode 100644 resources/assets/js/forms/formCustomListener.js
create mode 100644 resources/assets/js/layout/quicklinks-navbar.js
create mode 100644 resources/assets/js/layout/search-navbar.js
create mode 100644 resources/assets/js/main.js
create mode 100644 resources/assets/js/maps/LeafletMapHelper.js
create mode 100644 resources/assets/js/maps/LocationIQSearchHelper.js
create mode 100644 resources/assets/js/notifications/LivewireNotification.js
create mode 100644 resources/assets/vendor/fonts/bootstrap-icons.scss
create mode 100644 resources/assets/vendor/fonts/bootstrap-icons/bootstrap-icons.woff
create mode 100644 resources/assets/vendor/fonts/bootstrap-icons/bootstrap-icons.woff2
create mode 100644 resources/assets/vendor/fonts/fontawesome.scss
create mode 100644 resources/assets/vendor/fonts/fontawesome/fa-brands-400.ttf
create mode 100644 resources/assets/vendor/fonts/fontawesome/fa-brands-400.woff2
create mode 100644 resources/assets/vendor/fonts/fontawesome/fa-regular-400.ttf
create mode 100644 resources/assets/vendor/fonts/fontawesome/fa-regular-400.woff2
create mode 100644 resources/assets/vendor/fonts/fontawesome/fa-solid-900.ttf
create mode 100644 resources/assets/vendor/fonts/fontawesome/fa-solid-900.woff2
create mode 100644 resources/assets/vendor/fonts/fontawesome/fa-v4compatibility.ttf
create mode 100644 resources/assets/vendor/fonts/fontawesome/fa-v4compatibility.woff2
create mode 100644 resources/assets/vendor/fonts/tabler-icons.scss
create mode 100644 resources/assets/vendor/fonts/tabler/tabler-icons.eot
create mode 100644 resources/assets/vendor/fonts/tabler/tabler-icons.svg
create mode 100644 resources/assets/vendor/fonts/tabler/tabler-icons.ttf
create mode 100644 resources/assets/vendor/fonts/tabler/tabler-icons.woff
create mode 100644 resources/assets/vendor/fonts/tabler/tabler-icons.woff2
create mode 100644 resources/assets/vendor/js/_template-customizer/_template-customizer.html
create mode 100644 resources/assets/vendor/js/_template-customizer/_template-customizer.scss
create mode 100644 resources/assets/vendor/js/bootstrap.js
create mode 100644 resources/assets/vendor/js/dropdown-hover.js
create mode 100644 resources/assets/vendor/js/helpers.js
create mode 100644 resources/assets/vendor/js/mega-dropdown.js
create mode 100644 resources/assets/vendor/js/menu.js
create mode 100644 resources/assets/vendor/js/template-customizer.js
create mode 100644 resources/assets/vendor/libs/@form-validation/auto-focus.js
create mode 100644 resources/assets/vendor/libs/@form-validation/bootstrap5.js
create mode 100644 resources/assets/vendor/libs/@form-validation/form-validation.scss
create mode 100644 resources/assets/vendor/libs/@form-validation/popular.js
create mode 100644 resources/assets/vendor/libs/_tabler/_tabler.scss
create mode 100644 resources/assets/vendor/libs/animate-css/animate.scss
create mode 100644 resources/assets/vendor/libs/bootstrap-datepicker/_mixins.scss
create mode 100644 resources/assets/vendor/libs/bootstrap-datepicker/bootstrap-datepicker.js
create mode 100644 resources/assets/vendor/libs/bootstrap-datepicker/bootstrap-datepicker.scss
create mode 100644 resources/assets/vendor/libs/bootstrap-daterangepicker/_mixins.scss
create mode 100644 resources/assets/vendor/libs/bootstrap-daterangepicker/bootstrap-daterangepicker.js
create mode 100644 resources/assets/vendor/libs/bootstrap-daterangepicker/bootstrap-daterangepicker.scss
create mode 100644 resources/assets/vendor/libs/bootstrap-select/_mixins.scss
create mode 100644 resources/assets/vendor/libs/bootstrap-select/bootstrap-select.js
create mode 100644 resources/assets/vendor/libs/bootstrap-select/bootstrap-select.scss
create mode 100644 resources/assets/vendor/libs/bootstrap-table/bootstrap-table.js
create mode 100644 resources/assets/vendor/libs/bootstrap-table/bootstrap-table.scss
create mode 100644 resources/assets/vendor/libs/bs-stepper/_mixins.scss
create mode 100644 resources/assets/vendor/libs/bs-stepper/bs-stepper.js
create mode 100644 resources/assets/vendor/libs/bs-stepper/bs-stepper.scss
create mode 100644 resources/assets/vendor/libs/cleavejs/cleave-phone.js
create mode 100644 resources/assets/vendor/libs/cleavejs/cleave.js
create mode 100644 resources/assets/vendor/libs/datatables-bs5/datatables-bootstrap5.js
create mode 100644 resources/assets/vendor/libs/datatables-bs5/datatables.bootstrap5.scss
create mode 100644 resources/assets/vendor/libs/datatables-buttons-bs5/buttons.bootstrap5.scss
create mode 100644 resources/assets/vendor/libs/datatables-checkboxes-jquery/datatables.checkboxes.scss
create mode 100644 resources/assets/vendor/libs/datatables-fixedcolumns-bs5/fixedcolumns.bootstrap5.scss
create mode 100644 resources/assets/vendor/libs/datatables-fixedheader-bs5/fixedheader.bootstrap5.scss
create mode 100644 resources/assets/vendor/libs/datatables-lang/datatable-lang-es.js
create mode 100644 resources/assets/vendor/libs/datatables-responsive-bs5/_mixins.scss
create mode 100644 resources/assets/vendor/libs/datatables-responsive-bs5/responsive.bootstrap5.scss
create mode 100644 resources/assets/vendor/libs/datatables-rowgroup-bs5/rowgroup.bootstrap5.scss
create mode 100644 resources/assets/vendor/libs/datatables-select-bs5/select.bootstrap5.scss
create mode 100644 resources/assets/vendor/libs/dropzone/_mixins.scss
create mode 100644 resources/assets/vendor/libs/dropzone/dropzone.js
create mode 100644 resources/assets/vendor/libs/dropzone/dropzone.scss
create mode 100644 resources/assets/vendor/libs/flatpickr/_mixins.scss
create mode 100644 resources/assets/vendor/libs/flatpickr/flatpickr.js
create mode 100644 resources/assets/vendor/libs/flatpickr/flatpickr.scss
create mode 100644 resources/assets/vendor/libs/fullcalendar/_mixins.scss
create mode 100644 resources/assets/vendor/libs/fullcalendar/fullcalendar.js
create mode 100644 resources/assets/vendor/libs/fullcalendar/fullcalendar.scss
create mode 100644 resources/assets/vendor/libs/hammer/hammer.js
create mode 100644 resources/assets/vendor/libs/jquery-timepicker/_mixins.scss
create mode 100644 resources/assets/vendor/libs/jquery-timepicker/jquery-timepicker.js
create mode 100644 resources/assets/vendor/libs/jquery-timepicker/jquery-timepicker.scss
create mode 100644 resources/assets/vendor/libs/jquery/jquery.js
create mode 100644 resources/assets/vendor/libs/leaflet/images/layers-2x.png
create mode 100644 resources/assets/vendor/libs/leaflet/images/layers.png
create mode 100644 resources/assets/vendor/libs/leaflet/images/marker-icon-2x.png
create mode 100644 resources/assets/vendor/libs/leaflet/images/marker-icon.png
create mode 100644 resources/assets/vendor/libs/leaflet/images/marker-shadow.png
create mode 100644 resources/assets/vendor/libs/leaflet/leaflet.js
create mode 100644 resources/assets/vendor/libs/leaflet/leaflet.scss
create mode 100644 resources/assets/vendor/libs/moment/moment.js
create mode 100644 resources/assets/vendor/libs/node-waves/node-waves.js
create mode 100644 resources/assets/vendor/libs/node-waves/node-waves.scss
create mode 100644 resources/assets/vendor/libs/nouislider/_mixins.scss
create mode 100644 resources/assets/vendor/libs/nouislider/nouislider.js
create mode 100644 resources/assets/vendor/libs/nouislider/nouislider.scss
create mode 100644 resources/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.js
create mode 100644 resources/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.scss
create mode 100644 resources/assets/vendor/libs/pickr/_mixins.scss
create mode 100644 resources/assets/vendor/libs/pickr/_pickr-classic.scss
create mode 100644 resources/assets/vendor/libs/pickr/_pickr-monolith.scss
create mode 100644 resources/assets/vendor/libs/pickr/_pickr-nano.scss
create mode 100644 resources/assets/vendor/libs/pickr/pickr-themes.scss
create mode 100644 resources/assets/vendor/libs/pickr/pickr.js
create mode 100644 resources/assets/vendor/libs/plyr/_mixins.scss
create mode 100644 resources/assets/vendor/libs/plyr/plyr.js
create mode 100644 resources/assets/vendor/libs/plyr/plyr.scss
create mode 100644 resources/assets/vendor/libs/popper/popper.js
create mode 100644 resources/assets/vendor/libs/quill/_mixins.scss
create mode 100644 resources/assets/vendor/libs/quill/editor.scss
create mode 100644 resources/assets/vendor/libs/quill/katex.js
create mode 100644 resources/assets/vendor/libs/quill/katex.scss
create mode 100644 resources/assets/vendor/libs/quill/quill.js
create mode 100644 resources/assets/vendor/libs/quill/typography.scss
create mode 100644 resources/assets/vendor/libs/select2/_mixins.scss
create mode 100644 resources/assets/vendor/libs/select2/es.js
create mode 100644 resources/assets/vendor/libs/select2/select2.js
create mode 100644 resources/assets/vendor/libs/select2/select2.scss
create mode 100644 resources/assets/vendor/libs/shepherd/_mixins.scss
create mode 100644 resources/assets/vendor/libs/shepherd/shepherd.js
create mode 100644 resources/assets/vendor/libs/shepherd/shepherd.scss
create mode 100644 resources/assets/vendor/libs/spinkit/_mixins.scss
create mode 100644 resources/assets/vendor/libs/spinkit/spinkit.scss
create mode 100644 resources/assets/vendor/libs/sweetalert2/_mixins.scss
create mode 100644 resources/assets/vendor/libs/sweetalert2/sweetalert2.js
create mode 100644 resources/assets/vendor/libs/sweetalert2/sweetalert2.scss
create mode 100644 resources/assets/vendor/libs/swiper/_mixins.scss
create mode 100644 resources/assets/vendor/libs/swiper/swiper.js
create mode 100644 resources/assets/vendor/libs/swiper/swiper.scss
create mode 100644 resources/assets/vendor/libs/tagify/_mixins.scss
create mode 100644 resources/assets/vendor/libs/tagify/_tagify-email-list.scss
create mode 100644 resources/assets/vendor/libs/tagify/_tagify-inline-suggestion.scss
create mode 100644 resources/assets/vendor/libs/tagify/_tagify-users-list.scss
create mode 100644 resources/assets/vendor/libs/tagify/tagify.js
create mode 100644 resources/assets/vendor/libs/tagify/tagify.scss
create mode 100644 resources/assets/vendor/libs/typeahead-js/_mixins.scss
create mode 100644 resources/assets/vendor/libs/typeahead-js/typeahead.js
create mode 100644 resources/assets/vendor/libs/typeahead-js/typeahead.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-dark.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended-dark.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_accordion.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_alert.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_badge.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_breadcrumb.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_button-group.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_buttons.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_card.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_carousel.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_close.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_dropdown.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_forms.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_functions.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_include-dark.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_include.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_list-group.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_mixins.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_modal.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_nav.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_navbar.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_offcanvas.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_pagination.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_popover.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_progress.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_reboot.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_root.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_spinners.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_tables.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_toasts.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_tooltip.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_type.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_utilities-ltr.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_utilities-rtl.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_utilities.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_variables-dark.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/_variables.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_floating-labels.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_form-check.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_form-control.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_form-range.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_form-select.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_form-text.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_input-group.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_labels.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/forms/_validation.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_alert.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_badge.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_buttons.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_card.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_caret.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_dropdown.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_forms.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_list-group.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_misc.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_modal.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_navs.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_pagination.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_popover.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_progress.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_table-variants.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap-extended/mixins/_tooltip.scss
create mode 100644 resources/assets/vendor/scss/_bootstrap.scss
create mode 100644 resources/assets/vendor/scss/_colors-dark.scss
create mode 100644 resources/assets/vendor/scss/_colors.scss
create mode 100644 resources/assets/vendor/scss/_components-dark.scss
create mode 100644 resources/assets/vendor/scss/_components.scss
create mode 100644 resources/assets/vendor/scss/_components/_app-brand.scss
create mode 100644 resources/assets/vendor/scss/_components/_avatar.scss
create mode 100644 resources/assets/vendor/scss/_components/_base.scss
create mode 100644 resources/assets/vendor/scss/_components/_blockui.scss
create mode 100644 resources/assets/vendor/scss/_components/_common.scss
create mode 100644 resources/assets/vendor/scss/_components/_custom-options.scss
create mode 100644 resources/assets/vendor/scss/_components/_drag-drop.scss
create mode 100644 resources/assets/vendor/scss/_components/_footer.scss
create mode 100644 resources/assets/vendor/scss/_components/_include-dark.scss
create mode 100644 resources/assets/vendor/scss/_components/_include.scss
create mode 100644 resources/assets/vendor/scss/_components/_layout.scss
create mode 100644 resources/assets/vendor/scss/_components/_menu.scss
create mode 100644 resources/assets/vendor/scss/_components/_mixins.scss
create mode 100644 resources/assets/vendor/scss/_components/_switch.scss
create mode 100644 resources/assets/vendor/scss/_components/_text-divider.scss
create mode 100644 resources/assets/vendor/scss/_components/_timeline.scss
create mode 100644 resources/assets/vendor/scss/_components/_variables-dark.scss
create mode 100644 resources/assets/vendor/scss/_components/_variables.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_app-brand.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_avatar.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_footer.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_menu.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_misc.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_navbar.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_switch.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_text-divider.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_timeline.scss
create mode 100644 resources/assets/vendor/scss/_components/mixins/_treeview.scss
create mode 100644 resources/assets/vendor/scss/_custom-variables/_bootstrap-extended-dark.scss
create mode 100644 resources/assets/vendor/scss/_custom-variables/_bootstrap-extended.scss
create mode 100644 resources/assets/vendor/scss/_custom-variables/_components-dark.scss
create mode 100644 resources/assets/vendor/scss/_custom-variables/_components.scss
create mode 100644 resources/assets/vendor/scss/_custom-variables/_libs.scss
create mode 100644 resources/assets/vendor/scss/_custom-variables/_pages.scss
create mode 100644 resources/assets/vendor/scss/_custom-variables/_support.scss
create mode 100644 resources/assets/vendor/scss/_theme/_common.scss
create mode 100644 resources/assets/vendor/scss/_theme/_libs.scss
create mode 100644 resources/assets/vendor/scss/_theme/_pages.scss
create mode 100644 resources/assets/vendor/scss/_theme/_theme.scss
create mode 100644 resources/assets/vendor/scss/core-dark.scss
create mode 100644 resources/assets/vendor/scss/core.scss
create mode 100644 resources/assets/vendor/scss/pages/_mixins.scss
create mode 100644 resources/assets/vendor/scss/theme-bordered-dark.scss
create mode 100644 resources/assets/vendor/scss/theme-bordered.scss
create mode 100644 resources/assets/vendor/scss/theme-default-dark.scss
create mode 100644 resources/assets/vendor/scss/theme-default.scss
create mode 100644 resources/assets/vendor/scss/theme-raspberry-dark.scss
create mode 100644 resources/assets/vendor/scss/theme-raspberry.scss
create mode 100644 resources/assets/vendor/scss/theme-semi-dark-dark.scss
create mode 100644 resources/assets/vendor/scss/theme-semi-dark.scss
create mode 100644 resources/img/avatar/generic.svg
create mode 100644 resources/img/customizer/border-dark.svg
create mode 100644 resources/img/customizer/border.svg
create mode 100644 resources/img/customizer/collapsed-dark.svg
create mode 100644 resources/img/customizer/collapsed.svg
create mode 100644 resources/img/customizer/compact-dark.svg
create mode 100644 resources/img/customizer/compact.svg
create mode 100644 resources/img/customizer/dark-dark.svg
create mode 100644 resources/img/customizer/dark.svg
create mode 100644 resources/img/customizer/default-dark.svg
create mode 100644 resources/img/customizer/default.svg
create mode 100644 resources/img/customizer/expanded-dark.svg
create mode 100644 resources/img/customizer/expanded.svg
create mode 100644 resources/img/customizer/hidden-dark.svg
create mode 100644 resources/img/customizer/hidden.svg
create mode 100644 resources/img/customizer/horizontal-fixed-dark.svg
create mode 100644 resources/img/customizer/horizontal-fixed.svg
create mode 100644 resources/img/customizer/horizontal-static-dark.svg
create mode 100644 resources/img/customizer/horizontal-static.svg
create mode 100644 resources/img/customizer/light-dark.svg
create mode 100644 resources/img/customizer/light.svg
create mode 100644 resources/img/customizer/ltr-dark.svg
create mode 100644 resources/img/customizer/ltr.svg
create mode 100644 resources/img/customizer/rtl-dark.svg
create mode 100644 resources/img/customizer/rtl.svg
create mode 100644 resources/img/customizer/semi-dark-dark.svg
create mode 100644 resources/img/customizer/semi-dark.svg
create mode 100644 resources/img/customizer/static-dark.svg
create mode 100644 resources/img/customizer/static.svg
create mode 100644 resources/img/customizer/sticky-dark.svg
create mode 100644 resources/img/customizer/sticky.svg
create mode 100644 resources/img/customizer/system-dark.svg
create mode 100644 resources/img/customizer/system.svg
create mode 100644 resources/img/customizer/vertical-dark.svg
create mode 100644 resources/img/customizer/vertical.svg
create mode 100644 resources/img/customizer/wide-dark.svg
create mode 100644 resources/img/customizer/wide.svg
create mode 100644 resources/img/illustrations/auth-forgot-password-illustration-dark.png
create mode 100644 resources/img/illustrations/auth-forgot-password-illustration-light.png
create mode 100644 resources/img/illustrations/auth-login-illustration-dark.png
create mode 100644 resources/img/illustrations/auth-login-illustration-light.png
create mode 100644 resources/img/illustrations/auth-register-illustration-dark.png
create mode 100644 resources/img/illustrations/auth-register-illustration-light.png
create mode 100644 resources/img/illustrations/auth-register-multisteps-illustration.png
create mode 100644 resources/img/illustrations/auth-register-multisteps-shape-dark.png
create mode 100644 resources/img/illustrations/auth-register-multisteps-shape-light.png
create mode 100644 resources/img/illustrations/auth-reset-password-illustration-dark.png
create mode 100644 resources/img/illustrations/auth-reset-password-illustration-light.png
create mode 100644 resources/img/illustrations/auth-two-step-illustration-dark.png
create mode 100644 resources/img/illustrations/auth-two-step-illustration-light.png
create mode 100644 resources/img/illustrations/auth-verify-email-illustration-dark.png
create mode 100644 resources/img/illustrations/auth-verify-email-illustration-light.png
create mode 100644 resources/img/illustrations/bg-shape-image-dark.png
create mode 100644 resources/img/illustrations/bg-shape-image-light.png
create mode 100644 resources/img/illustrations/page-misc-error.png
create mode 100644 resources/img/illustrations/page-misc-launching-soon.png
create mode 100644 resources/img/illustrations/page-misc-under-maintenance.png
create mode 100644 resources/img/illustrations/page-misc-you-are-not-authorized.png
create mode 100644 resources/img/logo/horizontal-02.png
create mode 100644 resources/img/logo/horizontal-04.png
create mode 100644 resources/img/logo/horizontal-circulo-01.png
create mode 100644 resources/img/logo/horizontal-circulo-03.png
create mode 100644 resources/img/logo/koneko-01.png
create mode 100644 resources/img/logo/koneko-02.png
create mode 100644 resources/img/logo/koneko-03.png
create mode 100644 resources/img/logo/koneko-04.png
create mode 100644 resources/img/logo/vertical-02.png
create mode 100644 resources/img/logo/vertical-04.png
create mode 100644 resources/js/app.js
create mode 100644 resources/js/auth/app-access-permission.js
create mode 100644 resources/js/auth/app-access-roles.js
create mode 100644 resources/js/auth/app-user-list.js
create mode 100644 resources/js/auth/app-user-view-account.js
create mode 100644 resources/js/auth/app-user-view-billing.js
create mode 100644 resources/js/auth/app-user-view-security.js
create mode 100644 resources/js/auth/app-user-view.js
create mode 100644 resources/js/auth/modal-add-new-address.js
create mode 100644 resources/js/auth/modal-add-new-cc.js
create mode 100644 resources/js/auth/modal-add-permission.js
create mode 100644 resources/js/auth/modal-add-role.js
create mode 100644 resources/js/auth/modal-edit-cc.js
create mode 100644 resources/js/auth/modal-edit-permission.js
create mode 100644 resources/js/auth/modal-edit-user.js
create mode 100644 resources/js/auth/modal-enable-otp.js
create mode 100644 resources/js/auth/pages-account-settings-account.js
create mode 100644 resources/js/auth/pages-account-settings-billing.js
create mode 100644 resources/js/auth/pages-account-settings-security.js
create mode 100644 resources/js/auth/pages-auth-multisteps.js
create mode 100644 resources/js/auth/pages-auth-two-steps.js
create mode 100644 resources/js/auth/pages-auth.js
create mode 100644 resources/js/bootstrap.js
create mode 100644 resources/js/pages/admin-settings-scripts.js
create mode 100644 resources/js/pages/cache-manager-scripts.js
create mode 100644 resources/js/pages/permissions-scripts.js
create mode 100644 resources/js/pages/roles-scripts.js
create mode 100644 resources/js/pages/smtp-settings-scripts.js
create mode 100644 resources/js/smtp-settings/SenderResponseForm.js
create mode 100644 resources/js/smtp-settings/SmtpSettingsForm.js
create mode 100644 resources/lang/es/auth.php
create mode 100644 resources/lang/es/errors.php
create mode 100644 resources/lang/es/locale.php
create mode 100644 resources/lang/es/messages.php
create mode 100644 resources/lang/es/pagination.php
create mode 100644 resources/lang/es/passwords.php
create mode 100644 resources/lang/es/validation-inline.php
create mode 100644 resources/lang/es/validation.php
create mode 100644 resources/lang/es_MX.json
create mode 100644 resources/scss/app.scss
create mode 100644 resources/scss/pages/page-account-settings.scss
create mode 100644 resources/scss/pages/page-auth.scss
create mode 100644 resources/scss/pages/page-misc.scss
create mode 100644 resources/scss/pages/page-profile.scss
create mode 100644 resources/scss/pages/page-user-view.scss
create mode 100644 resources/views/admin-settings/smtp-settings.blade.php
create mode 100644 resources/views/admin-settings/webapp-general-settings.blade.php
create mode 100644 resources/views/auth/auth-register-multisteps.blade.php
create mode 100644 resources/views/auth/auth-two-steps-basic.blade.php
create mode 100644 resources/views/auth/auth-two-steps-cover.blade.php
create mode 100644 resources/views/auth/auth-verify-email-basic.blade.php
create mode 100644 resources/views/auth/auth-verify-email-cover.blade.php
create mode 100644 resources/views/auth/forgot-password-basic.blade.php
create mode 100644 resources/views/auth/forgot-password-cover.blade.php
create mode 100644 resources/views/auth/login-basic.blade.php
create mode 100644 resources/views/auth/login-cover.blade.php
create mode 100644 resources/views/auth/register-basic.blade.php
create mode 100644 resources/views/auth/register-cover.blade.php
create mode 100644 resources/views/auth/reset-password-basic.blade.php
create mode 100644 resources/views/auth/reset-password-cover.blade.php
create mode 100644 resources/views/cache-manager/index.blade.php
create mode 100644 resources/views/components/button/basic.blade.php
create mode 100644 resources/views/components/button/group.blade.php
create mode 100644 resources/views/components/button/index-off-canvas.blade.php
create mode 100644 resources/views/components/button/offcanvas-buttons.blade.php
create mode 100644 resources/views/components/card/basic.blade.php
create mode 100644 resources/views/components/file/dropzone.blade.php
create mode 100644 resources/views/components/form/checkbox-group.blade.php
create mode 100644 resources/views/components/form/checkbox.blade.php
create mode 100644 resources/views/components/form/custom-option.blade.php
create mode 100644 resources/views/components/form/form.blade.php
create mode 100644 resources/views/components/form/input.blade copy.php
create mode 100644 resources/views/components/form/input.blade.php
create mode 100644 resources/views/components/form/radio-group.blade.php
create mode 100644 resources/views/components/form/radio.blade.php
create mode 100644 resources/views/components/form/select.blade copy.php
create mode 100644 resources/views/components/form/select.blade.php
create mode 100644 resources/views/components/form/textarea.blade.php
create mode 100644 resources/views/components/offcanvas/basic.blade.php
create mode 100644 resources/views/components/table/bootstrap/manager.blade.php
create mode 100644 resources/views/errors/400.blade.php
create mode 100644 resources/views/errors/401.blade.php
create mode 100644 resources/views/errors/403.blade.php
create mode 100644 resources/views/errors/404.blade.php
create mode 100644 resources/views/layouts/vuexy/blankLayout.blade.php
create mode 100644 resources/views/layouts/vuexy/commonMaster.blade.php
create mode 100644 resources/views/layouts/vuexy/contentNavbarLayout.blade.php
create mode 100644 resources/views/layouts/vuexy/horizontalLayout.blade.php
create mode 100644 resources/views/layouts/vuexy/layoutMaster.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/content/breadcrumbs.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/footer/footer.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/menu/horizontalMenu.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/menu/submenu.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/menu/verticalMenu.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/navbar/navbar.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/scripts.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/scriptsIncludes.blade.php
create mode 100644 resources/views/layouts/vuexy/sections/styles.blade.php
create mode 100644 resources/views/livewire/admin-settings/application-settings.blade.php
create mode 100644 resources/views/livewire/admin-settings/general-settings.blade.php
create mode 100644 resources/views/livewire/admin-settings/interface-settings.blade.php
create mode 100644 resources/views/livewire/admin-settings/mail-sender-response-settings.blade.php
create mode 100644 resources/views/livewire/admin-settings/mail-smtp-settings.blade.php
create mode 100644 resources/views/livewire/cache/cache-functions.blade.php
create mode 100644 resources/views/livewire/cache/cache-stats.blade.php
create mode 100644 resources/views/livewire/cache/memcached-stats.blade.php
create mode 100644 resources/views/livewire/cache/redis-stats.blade.php
create mode 100644 resources/views/livewire/cache/session-stats.blade.php
create mode 100644 resources/views/livewire/permissions/index.blade.php
create mode 100644 resources/views/livewire/permissions/permissions.blade.php
create mode 100644 resources/views/livewire/roles/cards.blade.php
create mode 100644 resources/views/livewire/roles/index.blade.php
create mode 100644 resources/views/livewire/users/count.blade.php
create mode 100644 resources/views/livewire/users/form.blade.php
create mode 100644 resources/views/livewire/users/index.blade.copy.php
create mode 100644 resources/views/livewire/users/index.blade.php
create mode 100644 resources/views/livewire/users/offcanvas-form.blade.php
create mode 100644 resources/views/livewire/users/show.blade.php
create mode 100644 resources/views/notifications/email.blade.php
create mode 100644 resources/views/pages/about.blade.php
create mode 100644 resources/views/pages/comingsoon.blade.php
create mode 100644 resources/views/pages/home.blade.php
create mode 100644 resources/views/pages/under-maintenance.blade.php
create mode 100644 resources/views/permissions/index.blade.php
create mode 100644 resources/views/profile/index.blade.php
create mode 100644 resources/views/roles/_delete_modal.blade.php
create mode 100644 resources/views/roles/_form_modal.blade.php
create mode 100644 resources/views/roles/index.blade.php
create mode 100644 resources/views/user-profile/app-access-permission.blade.php
create mode 100644 resources/views/user-profile/app-access-roles.blade.php
create mode 100644 resources/views/user-profile/app-user-list.blade.php
create mode 100644 resources/views/user-profile/app-user-view-account.blade.php
create mode 100644 resources/views/user-profile/app-user-view-connections.blade.php
create mode 100644 resources/views/user-profile/app-user-view-notifications.blade.php
create mode 100644 resources/views/user-profile/app-user-view-security.blade.php
create mode 100644 resources/views/user-profile/index.blade.php
create mode 100644 resources/views/users/index.blade.php
create mode 100644 resources/views/users/show.blade.php
create mode 100644 routes/admin.php
create mode 100644 storage/fonts/OpenSans-Bold.ttf
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..8f0de65
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[docker-compose.yml]
+indent_size = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..7333620
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,38 @@
+* text=auto eol=lf
+
+*.blade.php diff=html
+*.css diff=css
+*.html diff=html
+*.md diff=markdown
+*.php diff=php
+
+/.github export-ignore
+CHANGELOG.md export-ignore
+.styleci.yml export-ignore
+
+# Ignorar archivos de configuración y herramientas de desarrollo
+.editorconfig export-ignore
+.prettierrc.json export-ignore
+.prettierignore export-ignore
+.eslintrc.json export-ignore
+
+# Ignorar node_modules y dependencias locales
+node_modules/ export-ignore
+vendor/ export-ignore
+
+# Ignorar archivos de build
+npm-debug.log export-ignore
+
+# Ignorar carpetas de logs y caché
+storage/logs/ export-ignore
+storage/framework/ export-ignore
+
+# Ignorar carpetas de compilación de frontend
+public/build/ export-ignore
+dist/ export-ignore
+
+# Ignorar archivos de CI/CD
+.github/ export-ignore
+.gitlab-ci.yml export-ignore
+.vscode/ export-ignore
+.idea/ export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d07bec2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+/node_modules
+/vendor
+/.vscode
+/.nova
+/.fleet
+/.phpactor.json
+/.phpunit.cache
+/.phpunit.result.cache
+/.zed
+/.idea
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..5d3dfee
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,16 @@
+# Dependencias de Composer y Node.js
+/vendor/
+/node_modules/
+
+# Caché y logs
+/storage/
+*.log
+*.cache
+
+# Archivos del sistema
+.DS_Store
+Thumbs.db
+
+# Configuración de editores
+.idea/
+.vscode/
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..5f11c9c
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,29 @@
+{
+ "arrowParens": "avoid",
+ "bracketSpacing": true,
+ "bracketSameLine": true,
+ "htmlWhitespaceSensitivity": "css",
+ "insertPragma": false,
+ "jsxSingleQuote": true,
+ "printWidth": 120,
+ "proseWrap": "preserve",
+ "quoteProps": "as-needed",
+ "requirePragma": false,
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 4,
+ "trailingComma": "none",
+ "useTabs": false,
+ "endOfLine": "lf",
+ "embeddedLanguageFormatting": "auto",
+ "overrides": [
+ {
+ "files": [
+ "resources/assets/**/*.js"
+ ],
+ "options": {
+ "semi": false
+ }
+ }
+ ]
+}
diff --git a/Actions/Fortify/CreateNewUser.php b/Actions/Fortify/CreateNewUser.php
new file mode 100644
index 0000000..89525c6
--- /dev/null
+++ b/Actions/Fortify/CreateNewUser.php
@@ -0,0 +1,40 @@
+ $input
+ */
+ public function create(array $input): User
+ {
+ Validator::make($input, [
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => [
+ 'required',
+ 'string',
+ 'email',
+ 'max:255',
+ Rule::unique(User::class),
+ ],
+ 'password' => $this->passwordRules(),
+ ])->validate();
+
+ return User::create([
+ 'name' => $input['name'],
+ 'email' => $input['email'],
+ 'password' => Hash::make($input['password']),
+ ]);
+ }
+}
diff --git a/Actions/Fortify/PasswordValidationRules.php b/Actions/Fortify/PasswordValidationRules.php
new file mode 100644
index 0000000..a2edbce
--- /dev/null
+++ b/Actions/Fortify/PasswordValidationRules.php
@@ -0,0 +1,18 @@
+|string>
+ */
+ protected function passwordRules(): array
+ {
+ return ['required', 'string', Password::default(), 'confirmed'];
+ }
+}
diff --git a/Actions/Fortify/ResetUserPassword.php b/Actions/Fortify/ResetUserPassword.php
new file mode 100644
index 0000000..9017b2c
--- /dev/null
+++ b/Actions/Fortify/ResetUserPassword.php
@@ -0,0 +1,29 @@
+ $input
+ */
+ public function reset(User $user, array $input): void
+ {
+ Validator::make($input, [
+ 'password' => $this->passwordRules(),
+ ])->validate();
+
+ $user->forceFill([
+ 'password' => Hash::make($input['password']),
+ ])->save();
+ }
+}
diff --git a/Actions/Fortify/UpdateUserPassword.php b/Actions/Fortify/UpdateUserPassword.php
new file mode 100644
index 0000000..35b0fc3
--- /dev/null
+++ b/Actions/Fortify/UpdateUserPassword.php
@@ -0,0 +1,32 @@
+ $input
+ */
+ public function update(User $user, array $input): void
+ {
+ Validator::make($input, [
+ 'current_password' => ['required', 'string', 'current_password:web'],
+ 'password' => $this->passwordRules(),
+ ], [
+ 'current_password.current_password' => __('The provided password does not match your current password.'),
+ ])->validateWithBag('updatePassword');
+
+ $user->forceFill([
+ 'password' => Hash::make($input['password']),
+ ])->save();
+ }
+}
diff --git a/Actions/Fortify/UpdateUserProfileInformation.php b/Actions/Fortify/UpdateUserProfileInformation.php
new file mode 100644
index 0000000..2367171
--- /dev/null
+++ b/Actions/Fortify/UpdateUserProfileInformation.php
@@ -0,0 +1,60 @@
+ $input
+ */
+ public function update(User $user, array $input): void
+ {
+ Validator::make($input, [
+ 'name' => ['required', 'string', 'max:255'],
+
+ 'email' => [
+ 'required',
+ 'string',
+ 'email',
+ 'max:255',
+ Rule::unique('users')->ignore($user->id),
+ ],
+ ])->validateWithBag('updateProfileInformation');
+
+ if (
+ $input['email'] !== $user->email &&
+ $user instanceof MustVerifyEmail
+ ) {
+ $this->updateVerifiedUser($user, $input);
+ } else {
+ $user->forceFill([
+ 'name' => $input['name'],
+ 'email' => $input['email'],
+ ])->save();
+ }
+ }
+
+ /**
+ * Update the given verified user's profile information.
+ *
+ * @param array $input
+ */
+ protected function updateVerifiedUser(User $user, array $input): void
+ {
+ $user->forceFill([
+ 'name' => $input['name'],
+ 'email' => $input['email'],
+ 'email_verified_at' => null,
+ ])->save();
+
+ $user->sendEmailVerificationNotification();
+ }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0cc0f79
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,41 @@
+# 📜 CHANGELOG - Laravel Vuexy Admin
+
+Este documento sigue el formato [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+
+## [0.1.0] - ALPHA - 2025-03-05
+
+### ✨ Added (Agregado)
+- 📌 **Integración con los catálogos SAT (CFDI 4.0)**:
+ - `sat_banco`, `sat_clave_prod_serv`, `sat_clave_unidad`, `sat_codigo_postal`, `sat_colonia`, `sat_deduccion`, `sat_estado`, `sat_forma_pago`, `sat_localidad`, `sat_municipio`, `sat_moneda`, `sat_pais`, `sat_percepcion`, `sat_regimen_contratacion`, `sat_regimen_fiscal`, `sat_uso_cfdi`.
+- 🎨 **Interfaz basada en Vuexy Admin** con integración de Laravel Blade + Livewire.
+- 🔑 **Sistema de autenticación y RBAC** con Laravel Fortify y Spatie Permissions.
+- 🔄 **Módulo de tipos de cambio** con integración de la API de Banxico.
+- 📦 **Manejo de almacenamiento y gestión de activos**.
+- 🚀 **Publicación inicial del repositorio en Packagist y Git Tea**.
+
+### 🛠 Changed (Modificado)
+- **Optimización del sistema de permisos y roles** para mayor flexibilidad.
+
+### 🐛 Fixed (Correcciones)
+- Se corrigieron errores en migraciones de catálogos SAT.
+
+---
+
+## 📅 Próximos Cambios Planeados
+- 📊 **Módulo de Reportes** con Laravel Excel y Charts.
+- 🏪 **Módulo de Inventarios y Punto de Venta (POS)**.
+- 📍 **Mejor integración con APIs de geolocalización**.
+
+---
+
+**📌 Nota:** Esta es la primera versión **ALPHA**, aún en desarrollo.
+
+---
+
+## 🔄 Sincronización de Cambios
+Este `CHANGELOG.md` se actualiza primero en nuestro repositorio principal en **[Tea - Koneko Git](https://git.koneko.mx/koneko/laravel-vuexy-admin)** y luego se refleja en GitHub.
+Los cambios recientes pueden verse antes en **Tea** que en **GitHub** debido a la sincronización automática.
+
+---
+
+📅 Última actualización: **2024-03-05**.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ab16327
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,9 @@
+## 🔐 Acceso al Repositorio Privado
+
+Nuestro servidor Git en **Tea** tiene un registro cerrado. Para contribuir:
+
+1. Abre un **Issue** en [GitHub](https://github.com/koneko-mx/laravel-vuexy-admin/issues) indicando tu interés en contribuir.
+2. Alternativamente, envía un correo a **contacto@koneko.mx** solicitando acceso.
+3. Una vez aprobado, recibirás una invitación para registrarte y clonar el repositorio.
+
+Si solo necesitas acceso de lectura, puedes clonar la versión pública en **GitHub**.
diff --git a/Console/Commands/CleanInitialAvatars.php b/Console/Commands/CleanInitialAvatars.php
new file mode 100644
index 0000000..0f03d7f
--- /dev/null
+++ b/Console/Commands/CleanInitialAvatars.php
@@ -0,0 +1,43 @@
+files($directory);
+
+ foreach ($files as $file) {
+ $lastModified = Storage::disk('public')->lastModified($file);
+
+ // Elimina archivos no accedidos en los últimos 30 días
+ if (now()->timestamp - $lastModified > 30 * 24 * 60 * 60) {
+ Storage::disk('public')->delete($file);
+ }
+ }
+
+ $this->info('Avatares iniciales antiguos eliminados.');
+ }
+}
diff --git a/Console/Commands/SyncRBAC.php b/Console/Commands/SyncRBAC.php
new file mode 100644
index 0000000..45ce8ed
--- /dev/null
+++ b/Console/Commands/SyncRBAC.php
@@ -0,0 +1,26 @@
+argument('action');
+ if ($action === 'import') {
+ RBACService::loadRolesAndPermissions();
+ $this->info('Roles y permisos importados correctamente.');
+ } elseif ($action === 'export') {
+ // Implementación para exportar los roles a JSON
+ $this->info('Exportación de roles y permisos completada.');
+ } else {
+ $this->error('Acción no válida. Usa "import" o "export".');
+ }
+ }
+}
diff --git a/Helpers/CatalogHelper.php b/Helpers/CatalogHelper.php
new file mode 100644
index 0000000..c8375f0
--- /dev/null
+++ b/Helpers/CatalogHelper.php
@@ -0,0 +1,72 @@
+find($id);
+ return response()->json($data);
+ }
+
+ // Aplicar filtros personalizados
+ foreach ($customFilters as $field => $value) {
+ if (!is_null($value)) {
+ $query->where($field, $value);
+ }
+ }
+
+ // Aplicar filtro de búsqueda si hay searchTerm
+ if ($searchTerm) {
+ $query->where($valueField, 'like', '%' . $searchTerm . '%');
+ }
+
+ // Limitar resultados si el límite no es falso
+ if ($limit > 0) {
+ $query->limit($limit);
+ }
+
+ $results = $query->get([$keyField, $valueField]);
+
+ // Devolver según el tipo de respuesta
+ switch ($responseType) {
+ case 'keyValue':
+ $data = $results->pluck($valueField, $keyField)->toArray();
+ break;
+
+ case 'select2':
+ $data = $results->map(function ($item) use ($keyField, $valueField) {
+ return [
+ 'id' => $item->{$keyField},
+ 'text' => $item->{$valueField},
+ ];
+ })->toArray();
+ break;
+
+ default:
+ $data = $results->map(function ($item) use ($keyField, $valueField) {
+ return [
+ 'id' => $item->{$keyField},
+ 'text' => $item->{$valueField},
+ ];
+ })->toArray();
+ break;
+ }
+
+ return response()->json($data);
+ }
+}
diff --git a/Helpers/VuexyHelper.php b/Helpers/VuexyHelper.php
new file mode 100644
index 0000000..847e8b4
--- /dev/null
+++ b/Helpers/VuexyHelper.php
@@ -0,0 +1,209 @@
+ 'vertical',
+ 'myTheme' => 'theme-default',
+ 'myStyle' => 'light',
+ 'myRTLSupport' => false,
+ 'myRTLMode' => true,
+ 'hasCustomizer' => true,
+ 'showDropdownOnHover' => true,
+ 'displayCustomizer' => true,
+ 'contentLayout' => 'compact',
+ 'headerType' => 'fixed',
+ 'navbarType' => 'fixed',
+ 'menuFixed' => true,
+ 'menuCollapsed' => false,
+ 'footerFixed' => false,
+ 'customizerControls' => [
+ 'rtl',
+ 'style',
+ 'headerType',
+ 'contentLayout',
+ 'layoutCollapsed',
+ 'showDropdownOnHover',
+ 'layoutNavbarOptions',
+ 'themes',
+ ],
+ // 'defaultLanguage'=>'en',
+ ];
+
+ // if any key missing of array from custom.php file it will be merge and set a default value from dataDefault array and store in data variable
+ $data = array_merge($DefaultData, $data);
+
+ // All options available in the template
+ $allOptions = [
+ 'myLayout' => ['vertical', 'horizontal', 'blank', 'front'],
+ 'menuCollapsed' => [true, false],
+ 'hasCustomizer' => [true, false],
+ 'showDropdownOnHover' => [true, false],
+ 'displayCustomizer' => [true, false],
+ 'contentLayout' => ['compact', 'wide'],
+ 'headerType' => ['fixed', 'static'],
+ 'navbarType' => ['fixed', 'static', 'hidden'],
+ 'myStyle' => ['light', 'dark', 'system'],
+ 'myTheme' => ['theme-default', 'theme-bordered', 'theme-semi-dark'],
+ 'myRTLSupport' => [true, false],
+ 'myRTLMode' => [true, false],
+ 'menuFixed' => [true, false],
+ 'footerFixed' => [true, false],
+ 'customizerControls' => [],
+ // 'defaultLanguage'=>array('en'=>'en','fr'=>'fr','de'=>'de','ar'=>'ar'),
+ ];
+
+ //if myLayout value empty or not match with default options in custom.php config file then set a default value
+ foreach ($allOptions as $key => $value) {
+ if (array_key_exists($key, $DefaultData)) {
+ if (gettype($DefaultData[$key]) === gettype($data[$key])) {
+ // data key should be string
+ if (is_string($data[$key])) {
+ // data key should not be empty
+ if (isset($data[$key]) && $data[$key] !== null) {
+ // data key should not be exist inside allOptions array's sub array
+ if (!array_key_exists($data[$key], $value)) {
+ // ensure that passed value should be match with any of allOptions array value
+ $result = array_search($data[$key], $value, 'strict');
+ if (empty($result) && $result !== 0) {
+ $data[$key] = $DefaultData[$key];
+ }
+ }
+ } else {
+ // if data key not set or
+ $data[$key] = $DefaultData[$key];
+ }
+ }
+ } else {
+ $data[$key] = $DefaultData[$key];
+ }
+ }
+ }
+ $styleVal = $data['myStyle'] == "dark" ? "dark" : "light";
+ $styleUpdatedVal = $data['myStyle'] == "dark" ? "dark" : $data['myStyle'];
+ // Determine if the layout is admin or front based on cookies
+ $layoutName = $data['myLayout'];
+ $isAdmin = Str::contains($layoutName, 'front') ? false : true;
+
+ $modeCookieName = $isAdmin ? 'admin-mode' : 'front-mode';
+ $colorPrefCookieName = $isAdmin ? 'admin-colorPref' : 'front-colorPref';
+
+ // Determine style based on cookies, only if not 'blank-layout'
+ if ($layoutName !== 'blank') {
+ if (isset($_COOKIE[$modeCookieName])) {
+ $styleVal = $_COOKIE[$modeCookieName];
+ if ($styleVal === 'system') {
+ $styleVal = isset($_COOKIE[$colorPrefCookieName]) ? $_COOKIE[$colorPrefCookieName] : 'light';
+ }
+ $styleUpdatedVal = $_COOKIE[$modeCookieName];
+ }
+ }
+
+ isset($_COOKIE['theme']) ? $themeVal = $_COOKIE['theme'] : $themeVal = $data['myTheme'];
+
+ $directionVal = isset($_COOKIE['direction']) ? ($_COOKIE['direction'] === "true" ? 'rtl' : 'ltr') : $data['myRTLMode'];
+
+ //layout classes
+ $layoutClasses = [
+ 'layout' => $data['myLayout'],
+ 'theme' => $themeVal,
+ 'themeOpt' => $data['myTheme'],
+ 'style' => $styleVal,
+ 'styleOpt' => $data['myStyle'],
+ 'styleOptVal' => $styleUpdatedVal,
+ 'rtlSupport' => $data['myRTLSupport'],
+ 'rtlMode' => $data['myRTLMode'],
+ 'textDirection' => $directionVal, //$data['myRTLMode'],
+ 'menuCollapsed' => $data['menuCollapsed'],
+ 'hasCustomizer' => $data['hasCustomizer'],
+ 'showDropdownOnHover' => $data['showDropdownOnHover'],
+ 'displayCustomizer' => $data['displayCustomizer'],
+ 'contentLayout' => $data['contentLayout'],
+ 'headerType' => $data['headerType'],
+ 'navbarType' => $data['navbarType'],
+ 'menuFixed' => $data['menuFixed'],
+ 'footerFixed' => $data['footerFixed'],
+ 'customizerControls' => $data['customizerControls'],
+ ];
+
+ // sidebar Collapsed
+ if ($layoutClasses['menuCollapsed'] == true) {
+ $layoutClasses['menuCollapsed'] = 'layout-menu-collapsed';
+ }
+
+ // Header Type
+ if ($layoutClasses['headerType'] == 'fixed') {
+ $layoutClasses['headerType'] = 'layout-menu-fixed';
+ }
+ // Navbar Type
+ if ($layoutClasses['navbarType'] == 'fixed') {
+ $layoutClasses['navbarType'] = 'layout-navbar-fixed';
+ } elseif ($layoutClasses['navbarType'] == 'static') {
+ $layoutClasses['navbarType'] = '';
+ } else {
+ $layoutClasses['navbarType'] = 'layout-navbar-hidden';
+ }
+
+ // Menu Fixed
+ if ($layoutClasses['menuFixed'] == true) {
+ $layoutClasses['menuFixed'] = 'layout-menu-fixed';
+ }
+
+ // Footer Fixed
+ if ($layoutClasses['footerFixed'] == true) {
+ $layoutClasses['footerFixed'] = 'layout-footer-fixed';
+ }
+
+ // RTL Supported template
+ if ($layoutClasses['rtlSupport'] == true) {
+ $layoutClasses['rtlSupport'] = '/rtl';
+ }
+
+ // RTL Layout/Mode
+ if ($layoutClasses['rtlMode'] == true) {
+ $layoutClasses['rtlMode'] = 'rtl';
+ $layoutClasses['textDirection'] = isset($_COOKIE['direction']) ? ($_COOKIE['direction'] === "true" ? 'rtl' : 'ltr') : 'rtl';
+ } else {
+ $layoutClasses['rtlMode'] = 'ltr';
+ $layoutClasses['textDirection'] = isset($_COOKIE['direction']) && $_COOKIE['direction'] === "true" ? 'rtl' : 'ltr';
+ }
+
+ // Show DropdownOnHover for Horizontal Menu
+ if ($layoutClasses['showDropdownOnHover'] == true) {
+ $layoutClasses['showDropdownOnHover'] = true;
+ } else {
+ $layoutClasses['showDropdownOnHover'] = false;
+ }
+
+ // To hide/show display customizer UI, not js
+ if ($layoutClasses['displayCustomizer'] == true) {
+ $layoutClasses['displayCustomizer'] = true;
+ } else {
+ $layoutClasses['displayCustomizer'] = false;
+ }
+
+ return $layoutClasses;
+ }
+
+ public static function updatePageConfig($pageConfigs)
+ {
+ $demo = 'custom';
+ if (isset($pageConfigs)) {
+ if (count($pageConfigs) > 0) {
+ foreach ($pageConfigs as $config => $val) {
+ Config::set('vuexy.' . $demo . '.' . $config, $val);
+ }
+ }
+ }
+ }
+}
diff --git a/Http/Controllers/AdminController.php b/Http/Controllers/AdminController.php
new file mode 100644
index 0000000..edd14e7
--- /dev/null
+++ b/Http/Controllers/AdminController.php
@@ -0,0 +1,62 @@
+expectsJson(), 403, __('errors.ajax_only'));
+
+ $VuexyAdminService = app(VuexyAdminService::class);
+
+ return response()->json($VuexyAdminService->getVuexySearchData());
+ }
+
+ public function quickLinksUpdate(Request $request)
+ {
+ abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
+
+ $validated = $request->validate([
+ 'action' => 'required|in:update,remove',
+ 'route' => 'required|string',
+ ]);
+
+ $quickLinks = Setting::where('user_id', Auth::user()->id)
+ ->where('key', 'quicklinks')
+ ->first();
+
+ $quickLinks = $quickLinks ? json_decode($quickLinks->value, true) : [];
+
+ if ($validated['action'] === 'update') {
+ // Verificar si ya existe
+ if (!in_array($validated['route'], $quickLinks))
+ $quickLinks[] = $validated['route'];
+ } elseif ($validated['action'] === 'remove') {
+ // Eliminar la ruta si existe
+ $quickLinks = array_filter($quickLinks, function ($route) use ($validated) {
+ return $route !== $validated['route'];
+ });
+ }
+
+ Setting::updateOrCreate(['user_id' => Auth::user()->id, 'key' => 'quicklinks'], ['value' => json_encode($quickLinks)]);
+
+ VuexyAdminService::clearQuickLinksCache();
+ }
+
+ public function generalSettings()
+ {
+ return view('vuexy-admin::admin-settings.webapp-general-settings');
+ }
+
+ public function smtpSettings()
+ {
+ return view('vuexy-admin::admin-settings.smtp-settings');
+ }
+}
diff --git a/Http/Controllers/AuthController.php b/Http/Controllers/AuthController.php
new file mode 100644
index 0000000..49495e4
--- /dev/null
+++ b/Http/Controllers/AuthController.php
@@ -0,0 +1,144 @@
+ 'blank'];
+
+ return view("vuexy-admin::auth.login-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+ public function registerView()
+ {
+ if (!Features::enabled(Features::registration()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+
+
+ public function confirmPasswordView()
+ {
+ if (!Features::enabled(Features::registration()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.confirm-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+ public function resetPasswordView()
+ {
+ if (!Features::enabled(Features::resetPasswords()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+ public function requestPasswordResetLinkView(Request $request)
+ {
+ if (!Features::enabled(Features::resetPasswords()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs, 'request' => $request]);
+ }
+
+
+
+
+
+
+
+
+ public function twoFactorChallengeView()
+ {
+ if (!Features::enabled(Features::registration()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.two-factor-challenge-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+ public function twoFactorRecoveryCodesView()
+ {
+ if (!Features::enabled(Features::registration()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+ public function twoFactorAuthenticationView()
+ {
+ if (!Features::enabled(Features::registration()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+
+
+
+
+ public function verifyEmailView()
+ {
+ if (!Features::enabled(Features::registration()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.verify-email-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+ public function showEmailVerificationForm()
+ {
+ if (!Features::enabled(Features::registration()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+
+ public function userProfileView()
+ {
+ if (!Features::enabled(Features::registration()))
+ abort(403, 'El registro está deshabilitado.');
+
+ $viewMode = config('vuexy.custom.authViewMode');
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ }
+ */
+}
diff --git a/Http/Controllers/CacheController.php b/Http/Controllers/CacheController.php
new file mode 100644
index 0000000..fd365d6
--- /dev/null
+++ b/Http/Controllers/CacheController.php
@@ -0,0 +1,41 @@
+json(['success' => true, 'message' => 'Cache generado correctamente.']);
+ } catch (\Exception $e) {
+ return response()->json(['success' => false, 'message' => 'Error al generar el cache.', 'error' => $e->getMessage()], 500);
+ }
+ }
+
+ public function generateRouteCache()
+ {
+ try {
+ // Lógica para generar cache de rutas
+ Artisan::call('route:cache');
+
+ return response()->json(['success' => true, 'message' => 'Cache de rutas generado correctamente.']);
+ } catch (\Exception $e) {
+ return response()->json(['success' => false, 'message' => 'Error al generar el cache de rutas.', 'error' => $e->getMessage()], 500);
+ }
+ }
+
+ public function cacheManager(CacheConfigService $cacheConfigService)
+ {
+ $configCache = $cacheConfigService->getConfig();
+
+ return view('vuexy-admin::cache-manager.index', compact('configCache'));
+ }
+}
diff --git a/Http/Controllers/HomeController.php b/Http/Controllers/HomeController.php
new file mode 100644
index 0000000..86820df
--- /dev/null
+++ b/Http/Controllers/HomeController.php
@@ -0,0 +1,32 @@
+ 'blank'];
+
+ return view('vuexy-admin::pages.comingsoon', compact('pageConfigs'));
+ }
+
+ public function underMaintenance()
+ {
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ return view('vuexy-admin::pages.under-maintenance', compact('pageConfigs'));
+ }
+}
diff --git a/Http/Controllers/LanguageController.php b/Http/Controllers/LanguageController.php
new file mode 100644
index 0000000..80b4874
--- /dev/null
+++ b/Http/Controllers/LanguageController.php
@@ -0,0 +1,21 @@
+session()->put('locale', $locale);
+ }
+ App::setLocale($locale);
+ return redirect()->back();
+ }
+}
diff --git a/Http/Controllers/PermissionController.php b/Http/Controllers/PermissionController.php
new file mode 100644
index 0000000..87fa3ca
--- /dev/null
+++ b/Http/Controllers/PermissionController.php
@@ -0,0 +1,37 @@
+ajax()) {
+ $permissions = Permission::latest()->get();
+
+ return DataTables::of($permissions)
+ ->addIndexColumn()
+ ->addColumn('assigned_to', function ($row) {
+ return (Arr::pluck($row->roles, ['name']));
+ })
+ ->editColumn('created_at', function ($request) {
+ return $request->created_at->format('Y-m-d h:i:s a');
+ })
+ ->make(true);
+ }
+
+ return view('vuexy-admin::permissions.index');
+ }
+}
diff --git a/Http/Controllers/RoleController.php b/Http/Controllers/RoleController.php
new file mode 100644
index 0000000..f1158e1
--- /dev/null
+++ b/Http/Controllers/RoleController.php
@@ -0,0 +1,38 @@
+input('id');
+ $name = $request->input('name');
+
+ // Verificar si el nombre ya existe en la base de datos
+ $existingRole = Role::where('name', $name)
+ ->whereNot('id', $id)
+ ->first();
+
+ if ($existingRole) {
+ return response()->json(['valid' => false]);
+ }
+
+ return response()->json(['valid' => true]);
+ }
+}
diff --git a/Http/Controllers/RolePermissionController.php b/Http/Controllers/RolePermissionController.php
new file mode 100644
index 0000000..d3d9c78
--- /dev/null
+++ b/Http/Controllers/RolePermissionController.php
@@ -0,0 +1,76 @@
+json([
+ 'roles' => Role::with('permissions')->get(),
+ 'permissions' => Permission::all()
+ ]);
+ }
+
+ public function storeRole(Request $request)
+ {
+ $request->validate(['name' => 'required|string|unique:roles,name']);
+ $role = Role::create(['name' => $request->name]);
+ return response()->json(['message' => 'Rol creado con éxito', 'role' => $role]);
+ }
+
+ public function storePermission(Request $request)
+ {
+ $request->validate(['name' => 'required|string|unique:permissions,name']);
+ $permission = Permission::create(['name' => $request->name]);
+ return response()->json(['message' => 'Permiso creado con éxito', 'permission' => $permission]);
+ }
+
+ public function assignPermissionToRole(Request $request)
+ {
+ $request->validate([
+ 'role_id' => 'required|exists:roles,id',
+ 'permission_id' => 'required|exists:permissions,id'
+ ]);
+
+ $role = Role::findById($request->role_id);
+ $permission = Permission::findById($request->permission_id);
+
+ $role->givePermissionTo($permission->name);
+
+ return response()->json(['message' => 'Permiso asignado con éxito']);
+ }
+
+ public function removePermissionFromRole(Request $request)
+ {
+ $request->validate([
+ 'role_id' => 'required|exists:roles,id',
+ 'permission_id' => 'required|exists:permissions,id'
+ ]);
+
+ $role = Role::findById($request->role_id);
+ $permission = Permission::findById($request->permission_id);
+
+ $role->revokePermissionTo($permission->name);
+
+ return response()->json(['message' => 'Permiso eliminado con éxito']);
+ }
+
+ public function deleteRole($id)
+ {
+ $role = Role::findOrFail($id);
+ $role->delete();
+ return response()->json(['message' => 'Rol eliminado con éxito']);
+ }
+
+ public function deletePermission($id)
+ {
+ $permission = Permission::findOrFail($id);
+ $permission->delete();
+ return response()->json(['message' => 'Permiso eliminado con éxito']);
+ }
+}
diff --git a/Http/Controllers/UserController copy.php b/Http/Controllers/UserController copy.php
new file mode 100644
index 0000000..117d910
--- /dev/null
+++ b/Http/Controllers/UserController copy.php
@@ -0,0 +1,188 @@
+ajax()) {
+ $users = User::when(!Auth::user()->hasRole('SuperAdmin'), function ($query) {
+ $query->where('id', '>', 1);
+ })
+ ->latest()
+ ->get();
+
+ return DataTables::of($users)
+ ->only(['id', 'name', 'email', 'avatar', 'roles', 'status', 'created_at'])
+ ->addIndexColumn()
+ ->addColumn('avatar', function ($user) {
+ return $user->profile_photo_url;
+ })
+ ->addColumn('roles', function ($user) {
+ return (Arr::pluck($user->roles, ['name']));
+ })
+ /*
+ ->addColumn('stores', function ($user) {
+ return (Arr::pluck($user->stores, ['nombre']));
+ })
+ y*/
+ ->editColumn('created_at', function ($user) {
+ return $user->created_at->format('Y-m-d');
+ })
+ ->make(true);
+ }
+
+ return view('vuexy-admin::users.index');
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @return \Illuminate\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'name' => 'required|max:255',
+ 'email' => 'required|max:255|unique:users',
+ 'photo' => 'nullable|mimes:jpg,jpeg,png|max:1024',
+ 'password' => 'required',
+ ]);
+
+ if ($validator->fails())
+ return response()->json(['errors' => $validator->errors()->all()]);
+
+ // Preparamos los datos
+ $user_request = array_merge_recursive($request->all(), [
+ 'remember_token' => Str::random(10),
+ 'created_by' => Auth::user()->id,
+ ]);
+
+ $user_request['password'] = bcrypt($request->password);
+
+ // Guardamos el nuevo usuario
+ $user = User::create($user_request);
+
+ // Asignmos los permisos
+ $user->assignRole($request->roles);
+
+ // Asignamos Sucursals
+ //$user->stores()->attach($request->stores);
+
+ if ($request->file('photo')){
+ $avatarImageService = new AvatarImageService();
+
+ $avatarImageService->updateProfilePhoto($user, $request->file('photo'));
+ }
+
+ return response()->json(['success' => 'Se agrego correctamente el usuario']);
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int User $user
+ * @return \Illuminate\Http\Response
+ */
+ public function show(User $user)
+ {
+ return view('vuexy-admin::users.show', compact('user'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param int User $user
+ * @return \Illuminate\Http\Response
+ */
+ public function updateAjax(Request $request, User $user)
+ {
+ // Validamos los datos
+ $validator = Validator::make($request->all(), [
+ 'name' => 'required|max:191',
+ 'email' => "required|max:191|unique:users,email," . $user->id,
+ 'photo' => 'nullable|mimes:jpg,jpeg,png|max:2048'
+ ]);
+
+ if ($validator->fails())
+ return response()->json(['errors' => $validator->errors()->all()]);
+
+ // Preparamos los datos
+ $user_request = $request->all();
+
+ if ($request->password) {
+ $user_request['password'] = bcrypt($request->password);
+ } else {
+ unset($user_request['password']);
+ }
+
+ // Guardamos los cambios
+ $user->update($user_request);
+
+ // Sincronizamos Roles
+ $user->syncRoles($request->roles);
+
+ // Sincronizamos Sucursals
+ //$user->stores()->sync($request->stores);
+
+ // Actualizamos foto de perfil
+ if ($request->file('photo'))
+ $avatarImageService = new AvatarImageService();
+
+ $avatarImageService->updateProfilePhoto($user, $request->file('photo'));
+
+ return response()->json(['success' => 'Se guardo correctamente los cambios.']);
+ }
+
+
+ public function userSettings(User $user)
+ {
+ return view('vuexy-admin::users.user-settings', compact('user'));
+ }
+
+ public function generateAvatar(Request $request)
+ {
+ // Validación de entrada
+ $request->validate([
+ 'name' => 'nullable|string',
+ 'color' => 'nullable|string|size:6',
+ 'background' => 'nullable|string|size:6',
+ 'size' => 'nullable|integer|min:20|max:1024'
+ ]);
+
+ $name = $request->get('name', 'NA');
+ $color = $request->get('color', '7F9CF5');
+ $background = $request->get('background', 'EBF4FF');
+ $size = $request->get('size', 100);
+
+ return User::getAvatarImage($name, $color, $background, $size);
+
+ try {
+ } catch (\Exception $e) {
+ // String base64 de una imagen PNG transparente de 1x1 píxel
+ $transparentBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==';
+
+ return response()->make(base64_decode($transparentBase64), 200, [
+ 'Content-Type' => 'image/png'
+ ]);
+ }
+ }
+}
diff --git a/Http/Controllers/UserController.php b/Http/Controllers/UserController.php
new file mode 100644
index 0000000..f5af305
--- /dev/null
+++ b/Http/Controllers/UserController.php
@@ -0,0 +1,234 @@
+ajax()) {
+ $bootstrapTableIndexConfig = [
+ 'table' => 'users',
+ 'columns' => [
+ 'users.id',
+ 'users.code',
+ DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS full_name"),
+ 'users.email',
+ 'users.birth_date',
+ 'users.hire_date',
+ 'users.curp',
+ 'users.nss',
+ 'users.job_title',
+ 'users.profile_photo_path',
+ DB::raw("(SELECT GROUP_CONCAT(roles.name SEPARATOR ';') as roles FROM model_has_roles INNER JOIN roles ON (model_has_roles.role_id = roles.id) WHERE model_has_roles.model_id = 1) as roles"),
+ 'users.is_partner',
+ 'users.is_employee',
+ 'users.is_prospect',
+ 'users.is_customer',
+ 'users.is_provider',
+ 'users.is_user',
+ 'users.status',
+ DB::raw("CONCAT_WS(' ', created.name, created.last_name) AS creator"),
+ 'created.email AS creator_email',
+ 'users.created_at',
+ 'users.updated_at',
+ ],
+ 'joins' => [
+ [
+ 'table' => 'users as parent',
+ 'first' => 'users.parent_id',
+ 'second' => 'parent.id',
+ 'type' => 'leftJoin',
+ ],
+ [
+ 'table' => 'users as agent',
+ 'first' => 'users.agent_id',
+ 'second' => 'agent.id',
+ 'type' => 'leftJoin',
+ ],
+ [
+ 'table' => 'users as created',
+ 'first' => 'users.created_by',
+ 'second' => 'created.id',
+ 'type' => 'leftJoin',
+ ],
+ [
+ 'table' => 'sat_codigo_postal',
+ 'first' => 'users.domicilio_fiscal',
+ 'second' => 'sat_codigo_postal.c_codigo_postal',
+ 'type' => 'leftJoin',
+ ],
+ [
+ 'table' => 'sat_estado',
+ 'first' => 'sat_codigo_postal.c_estado',
+ 'second' => 'sat_estado.c_estado',
+ 'type' => 'leftJoin',
+ 'and' => [
+ 'sat_estado.c_pais = "MEX"',
+ ],
+ ],
+ [
+ 'table' => 'sat_localidad',
+ 'first' => 'sat_codigo_postal.c_localidad',
+ 'second' => 'sat_localidad.c_localidad',
+ 'type' => 'leftJoin',
+ 'and' => [
+ 'sat_codigo_postal.c_estado = sat_localidad.c_estado',
+ ],
+ ],
+ [
+ 'table' => 'sat_municipio',
+ 'first' => 'sat_codigo_postal.c_municipio',
+ 'second' => 'sat_municipio.c_municipio',
+ 'type' => 'leftJoin',
+ 'and' => [
+ 'sat_codigo_postal.c_estado = sat_municipio.c_estado',
+ ],
+ ],
+ [
+ 'table' => 'sat_regimen_fiscal',
+ 'first' => 'users.c_regimen_fiscal',
+ 'second' => 'sat_regimen_fiscal.c_regimen_fiscal',
+ 'type' => 'leftJoin',
+ ],
+ [
+ 'table' => 'sat_uso_cfdi',
+ 'first' => 'users.c_uso_cfdi',
+ 'second' => 'sat_uso_cfdi.c_uso_cfdi',
+ 'type' => 'leftJoin',
+ ],
+ ],
+ 'filters' => [
+ 'search' => ['users.name', 'users.email', 'users.code', 'parent.name', 'created.name'],
+ ],
+ 'sort_column' => 'users.name',
+ 'default_sort_order' => 'asc',
+ ];
+
+ return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson();
+ }
+
+ return view('vuexy-admin::users.index');
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @return \Illuminate\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'name' => 'required|max:255',
+ 'email' => 'required|max:255|unique:users',
+ 'photo' => 'nullable|mimes:jpg,jpeg,png|max:1024',
+ 'password' => 'required',
+ ]);
+
+ if ($validator->fails())
+ return response()->json(['errors' => $validator->errors()->all()]);
+
+ // Preparamos los datos
+ $user_request = array_merge_recursive($request->all(), [
+ 'remember_token' => Str::random(10),
+ 'created_by' => Auth::user()->id,
+ ]);
+
+ $user_request['password'] = bcrypt($request->password);
+
+ // Guardamos el nuevo usuario
+ $user = User::create($user_request);
+
+ // Asignmos los permisos
+ $user->assignRole($request->roles);
+
+ // Asignamos Sucursals
+ //$user->stores()->attach($request->stores);
+
+ if ($request->file('photo')){
+ $avatarImageService = new AvatarImageService();
+
+ $avatarImageService->updateProfilePhoto($user, $request->file('photo'));
+ }
+
+ return response()->json(['success' => 'Se agrego correctamente el usuario']);
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int User $user
+ * @return \Illuminate\Http\Response
+ */
+ public function show(User $user)
+ {
+ return view('vuexy-admin::users.show', compact('user'));
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param int User $user
+ * @return \Illuminate\Http\Response
+ */
+ public function updateAjax(Request $request, User $user)
+ {
+ // Validamos los datos
+ $validator = Validator::make($request->all(), [
+ 'name' => 'required|max:191',
+ 'email' => "required|max:191|unique:users,email," . $user->id,
+ 'photo' => 'nullable|mimes:jpg,jpeg,png|max:2048'
+ ]);
+
+ if ($validator->fails())
+ return response()->json(['errors' => $validator->errors()->all()]);
+
+ // Preparamos los datos
+ $user_request = $request->all();
+
+ if ($request->password) {
+ $user_request['password'] = bcrypt($request->password);
+ } else {
+ unset($user_request['password']);
+ }
+
+ // Guardamos los cambios
+ $user->update($user_request);
+
+ // Sincronizamos Roles
+ $user->syncRoles($request->roles);
+
+ // Sincronizamos Sucursals
+ //$user->stores()->sync($request->stores);
+
+ // Actualizamos foto de perfil
+ if ($request->file('photo'))
+ $avatarImageService = new AvatarImageService();
+
+ $avatarImageService->updateProfilePhoto($user, $request->file('photo'));
+
+ return response()->json(['success' => 'Se guardo correctamente los cambios.']);
+ }
+
+
+ public function userSettings(User $user)
+ {
+ return view('vuexy-admin::users.user-settings', compact('user'));
+ }
+
+}
diff --git a/Http/Controllers/UserProfileController.php b/Http/Controllers/UserProfileController.php
new file mode 100644
index 0000000..ac36c68
--- /dev/null
+++ b/Http/Controllers/UserProfileController.php
@@ -0,0 +1,54 @@
+validate([
+ 'name' => 'nullable|string',
+ 'color' => 'nullable|string|size:6',
+ 'background' => 'nullable|string|size:6',
+ 'size' => 'nullable|integer|min:20|max:1024'
+ ]);
+
+ $name = $request->get('name', 'NA');
+ $color = $request->get('color', '7F9CF5');
+ $background = $request->get('background', 'EBF4FF');
+ $size = $request->get('size', 100);
+
+ $avatarService = new AvatarInitialsService();
+
+ try {
+ return $avatarService->getAvatarImage($name, $color, $background, $size);
+
+ } catch (\Exception $e) {
+ // String base64 de una imagen PNG transparente de 1x1 píxel
+ $transparentBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==';
+
+ return response()->make(base64_decode($transparentBase64), 200, [
+ 'Content-Type' => 'image/png'
+ ]);
+ }
+ }
+
+
+}
diff --git a/Http/Middleware/AdminTemplateMiddleware.php b/Http/Middleware/AdminTemplateMiddleware.php
new file mode 100644
index 0000000..b6ce5b6
--- /dev/null
+++ b/Http/Middleware/AdminTemplateMiddleware.php
@@ -0,0 +1,37 @@
+header('Accept'), 'text/html')) {
+ $adminVars = app(AdminTemplateService::class)->getAdminVars();
+ $vuexyAdminService = app(VuexyAdminService::class);
+
+ View::share([
+ '_admin' => $adminVars,
+ 'vuexyMenu' => $vuexyAdminService->getMenu(),
+ 'vuexySearch' => $vuexyAdminService->getSearch(),
+ 'vuexyQuickLinks' => $vuexyAdminService->getQuickLinks(),
+ 'vuexyNotifications' => $vuexyAdminService->getNotifications(),
+ 'vuexyBreadcrumbs' => $vuexyAdminService->getBreadcrumbs(),
+ ]);
+
+ }
+
+ return $next($request);
+ }
+}
diff --git a/Listeners/ClearUserCache.php b/Listeners/ClearUserCache.php
new file mode 100644
index 0000000..41eb55f
--- /dev/null
+++ b/Listeners/ClearUserCache.php
@@ -0,0 +1,25 @@
+user) {
+ VuexyAdminService::clearUserMenuCache();
+ VuexyAdminService::clearSearchMenuCache();
+ VuexyAdminService::clearQuickLinksCache();
+ VuexyAdminService::clearNotificationsCache();
+ }
+ }
+}
diff --git a/Listeners/HandleUserLogin.php b/Listeners/HandleUserLogin.php
new file mode 100644
index 0000000..840669b
--- /dev/null
+++ b/Listeners/HandleUserLogin.php
@@ -0,0 +1,26 @@
+ $event->user->id,
+ 'ip_address' => request()->ip(),
+ 'user_agent' => request()->header('User-Agent'),
+ ]);
+
+ // Actualizar el último login
+ $event->user->update(['last_login_at' => now(), 'last_login_ip' => request()->ip()]);
+
+ // Enviar notificación de inicio de sesión
+ //Mail::to($event->user->email)->send(new LoginNotification($event->user));
+ }
+}
diff --git a/Livewire/AdminSettings/ApplicationSettings.php b/Livewire/AdminSettings/ApplicationSettings.php
new file mode 100644
index 0000000..4c5e131
--- /dev/null
+++ b/Livewire/AdminSettings/ApplicationSettings.php
@@ -0,0 +1,83 @@
+loadSettings();
+ }
+
+ public function loadSettings($clearcache = false)
+ {
+ $this->upload_image_logo = null;
+ $this->upload_image_logo_dark = null;
+
+ $adminTemplateService = app(AdminTemplateService::class);
+
+ if ($clearcache) {
+ $adminTemplateService->clearAdminVarsCache();
+ }
+
+ // Obtener los valores de las configuraciones de la base de datos
+ $settings = $adminTemplateService->getAdminVars();
+
+ $this->admin_app_name = $settings['app_name'];
+ $this->admin_image_logo = $settings['image_logo']['large'];
+ $this->admin_image_logo_dark = $settings['image_logo']['large_dark'];
+ }
+
+ public function save()
+ {
+ $this->validate([
+ 'admin_app_name' => 'required|string|max:255',
+ 'upload_image_logo' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
+ 'upload_image_logo_dark' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
+ ]);
+
+ $adminSettingsService = app(AdminSettingsService::class);
+
+ // Guardar título del App en configuraciones
+ $adminSettingsService->updateSetting('admin_app_name', $this->admin_app_name);
+
+ // Procesar favicon si se ha cargado una imagen
+ if ($this->upload_image_logo) {
+ $adminSettingsService->processAndSaveImageLogo($this->upload_image_logo);
+ }
+
+ if ($this->upload_image_logo_dark) {
+ $adminSettingsService->processAndSaveImageLogo($this->upload_image_logo_dark, 'dark');
+ }
+
+ $this->loadSettings(true);
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'success',
+ message: 'Se han guardado los cambios en las configuraciones.'
+ );
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.admin-settings.application-settings');
+ }
+}
diff --git a/Livewire/AdminSettings/GeneralSettings.php b/Livewire/AdminSettings/GeneralSettings.php
new file mode 100644
index 0000000..e1a1cf1
--- /dev/null
+++ b/Livewire/AdminSettings/GeneralSettings.php
@@ -0,0 +1,84 @@
+loadSettings();
+ }
+
+ public function loadSettings($clearcache = false)
+ {
+ $this->upload_image_favicon = null;
+
+ $adminTemplateService = app(AdminTemplateService::class);
+
+ if ($clearcache) {
+ $adminTemplateService->clearAdminVarsCache();
+ }
+
+ // Obtener los valores de las configuraciones de la base de datos
+ $settings = $adminTemplateService->getAdminVars();
+
+ $this->admin_title = $settings['title'];
+ $this->admin_favicon_16x16 = $settings['favicon']['16x16'];
+ $this->admin_favicon_76x76 = $settings['favicon']['76x76'];
+ $this->admin_favicon_120x120 = $settings['favicon']['120x120'];
+ $this->admin_favicon_152x152 = $settings['favicon']['152x152'];
+ $this->admin_favicon_180x180 = $settings['favicon']['180x180'];
+ $this->admin_favicon_192x192 = $settings['favicon']['192x192'];
+ }
+
+ public function save()
+ {
+ $this->validate([
+ 'admin_title' => 'required|string|max:255',
+ 'upload_image_favicon' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
+ ]);
+
+ $adminSettingsService = app(AdminSettingsService::class);
+
+ // Guardar título del sitio en configuraciones
+ $adminSettingsService->updateSetting('admin_title', $this->admin_title);
+
+ // Procesar favicon si se ha cargado una imagen
+ if ($this->upload_image_favicon) {
+ $adminSettingsService->processAndSaveFavicon($this->upload_image_favicon);
+ }
+
+ $this->loadSettings(true);
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'success',
+ message: 'Se han guardado los cambios en las configuraciones.'
+ );
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.admin-settings.general-settings');
+ }
+}
diff --git a/Livewire/AdminSettings/InterfaceSettings.php b/Livewire/AdminSettings/InterfaceSettings.php
new file mode 100644
index 0000000..33ea5b1
--- /dev/null
+++ b/Livewire/AdminSettings/InterfaceSettings.php
@@ -0,0 +1,118 @@
+loadSettings();
+ }
+
+
+ public function loadSettings()
+ {
+ $adminTemplateService = app(AdminTemplateService::class);
+
+ // Obtener los valores de las configuraciones de la base de datos
+ $settings = $adminTemplateService->getVuexyCustomizerVars();
+
+ $this->vuexy_myLayout = $settings['myLayout'];
+ $this->vuexy_myTheme = $settings['myTheme'];
+ $this->vuexy_myStyle = $settings['myStyle'];
+ $this->vuexy_hasCustomizer = $settings['hasCustomizer'];
+ $this->vuexy_displayCustomizer = $settings['displayCustomizer'];
+ $this->vuexy_contentLayout = $settings['contentLayout'];
+ $this->vuexy_navbarType = $settings['navbarType'];
+ $this->vuexy_footerFixed = $settings['footerFixed'];
+ $this->vuexy_menuFixed = $settings['menuFixed'];
+ $this->vuexy_menuCollapsed = $settings['menuCollapsed'];
+ $this->vuexy_headerType = $settings['headerType'];
+ $this->vuexy_showDropdownOnHover = $settings['showDropdownOnHover'];
+ $this->vuexy_authViewMode = $settings['authViewMode'];
+ $this->vuexy_maxQuickLinks = $settings['maxQuickLinks'];
+ }
+
+ public function save()
+ {
+ $this->validate([
+ 'vuexy_maxQuickLinks' => 'required|integer|min:2|max:20',
+ ]);
+
+ $globalSettingsService = app(GlobalSettingsService::class);
+
+ // Guardar configuraciones
+ $globalSettingsService->updateSetting('config.vuexy.custom.myLayout', $this->vuexy_myLayout);
+ $globalSettingsService->updateSetting('config.vuexy.custom.myTheme', $this->vuexy_myTheme);
+ $globalSettingsService->updateSetting('config.vuexy.custom.myStyle', $this->vuexy_myStyle);
+ $globalSettingsService->updateSetting('config.vuexy.custom.hasCustomizer', $this->vuexy_hasCustomizer);
+ $globalSettingsService->updateSetting('config.vuexy.custom.displayCustomizer', $this->vuexy_displayCustomizer);
+ $globalSettingsService->updateSetting('config.vuexy.custom.contentLayout', $this->vuexy_contentLayout);
+ $globalSettingsService->updateSetting('config.vuexy.custom.navbarType', $this->vuexy_navbarType);
+ $globalSettingsService->updateSetting('config.vuexy.custom.footerFixed', $this->vuexy_footerFixed);
+ $globalSettingsService->updateSetting('config.vuexy.custom.menuFixed', $this->vuexy_menuFixed);
+ $globalSettingsService->updateSetting('config.vuexy.custom.menuCollapsed', $this->vuexy_menuCollapsed);
+ $globalSettingsService->updateSetting('config.vuexy.custom.headerType', $this->vuexy_headerType);
+ $globalSettingsService->updateSetting('config.vuexy.custom.showDropdownOnHover', $this->vuexy_showDropdownOnHover);
+ $globalSettingsService->updateSetting('config.vuexy.custom.authViewMode', $this->vuexy_authViewMode);
+ $globalSettingsService->updateSetting('config.vuexy.custom.maxQuickLinks', $this->vuexy_maxQuickLinks);
+
+ $globalSettingsService->clearSystemConfigCache();
+
+ // Refrescar el componente actual
+ $this->dispatch('clearLocalStoregeTemplateCustomizer');
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'success',
+ message: 'Se han guardado los cambios en las configuraciones.',
+ deferReload: true
+ );
+ }
+
+ public function clearCustomConfig()
+ {
+ $globalSettingsService = app(GlobalSettingsService::class);
+
+ $globalSettingsService->clearVuexyConfig();
+
+ // Refrescar el componente actual
+ $this->dispatch('clearLocalStoregeTemplateCustomizer');
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'success',
+ message: 'Se han guardado los cambios en las configuraciones.',
+ deferReload: true
+ );
+ }
+
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.admin-settings.interface-settings');
+ }
+}
diff --git a/Livewire/AdminSettings/MailSenderResponseSettings.php b/Livewire/AdminSettings/MailSenderResponseSettings.php
new file mode 100644
index 0000000..a6a1d35
--- /dev/null
+++ b/Livewire/AdminSettings/MailSenderResponseSettings.php
@@ -0,0 +1,106 @@
+ 'save'];
+
+ const REPLY_EMAIL_CREATOR = 1;
+ const REPLY_EMAIL_SENDER = 2;
+ const REPLY_EMAIL_CUSTOM = 3;
+
+ public $reply_email_options = [
+ self::REPLY_EMAIL_CREATOR => 'Responder al creador del documento',
+ self::REPLY_EMAIL_SENDER => 'Responder a quien envía el documento',
+ self::REPLY_EMAIL_CUSTOM => 'Definir dirección de correo electrónico',
+ ];
+
+
+ public function mount()
+ {
+ $this->loadSettings();
+ }
+
+
+ public function loadSettings()
+ {
+ $globalSettingsService = app(GlobalSettingsService::class);
+
+ // Obtener los valores de las configuraciones de la base de datos
+ $settings = $globalSettingsService->getMailSystemConfig();
+
+ $this->from_address = $settings['from']['address'];
+ $this->from_name = $settings['from']['name'];
+ $this->reply_to_method = $settings['reply_to']['method'];
+ $this->reply_to_email = $settings['reply_to']['email'];
+ $this->reply_to_name = $settings['reply_to']['name'];
+ }
+
+ public function save()
+ {
+ $this->validate([
+ 'from_address' => 'required|email',
+ 'from_name' => 'required|string|max:255',
+ 'reply_to_method' => 'required|string|max:255',
+ ], [
+ 'from_address.required' => 'El campo de correo electrónico es obligatorio.',
+ 'from_address.email' => 'El formato del correo electrónico no es válido.',
+ 'from_name.required' => 'El nombre es obligatorio.',
+ 'from_name.string' => 'El nombre debe ser una cadena de texto.',
+ 'from_name.max' => 'El nombre no puede tener más de 255 caracteres.',
+ 'reply_to_method.required' => 'El método de respuesta es obligatorio.',
+ 'reply_to_method.string' => 'El método de respuesta debe ser una cadena de texto.',
+ 'reply_to_method.max' => 'El método de respuesta no puede tener más de 255 caracteres.',
+ ]);
+
+ if ($this->reply_to_method == self::REPLY_EMAIL_CUSTOM) {
+ $this->validate([
+ 'reply_to_email' => ['required', 'email'],
+ 'reply_to_name' => ['required', 'string', 'max:255'],
+ ], [
+ 'reply_to_email.required' => 'El correo de respuesta es obligatorio.',
+ 'reply_to_email.email' => 'El formato del correo de respuesta no es válido.',
+ 'reply_to_name.required' => 'El nombre de respuesta es obligatorio.',
+ 'reply_to_name.string' => 'El nombre de respuesta debe ser una cadena de texto.',
+ 'reply_to_name.max' => 'El nombre de respuesta no puede tener más de 255 caracteres.',
+ ]);
+ }
+
+ $globalSettingsService = app(GlobalSettingsService::class);
+
+ // Guardar título del App en configuraciones
+ $globalSettingsService->updateSetting('mail.from.address', $this->from_address);
+ $globalSettingsService->updateSetting('mail.from.name', $this->from_name);
+ $globalSettingsService->updateSetting('mail.reply_to.method', $this->reply_to_method);
+ $globalSettingsService->updateSetting('mail.reply_to.email', $this->reply_to_method == self::REPLY_EMAIL_CUSTOM ? $this->reply_to_email : '');
+ $globalSettingsService->updateSetting('mail.reply_to.name', $this->reply_to_method == self::REPLY_EMAIL_CUSTOM ? $this->reply_to_name : '');
+
+ $globalSettingsService->clearMailSystemConfigCache();
+
+ $this->loadSettings();
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'success',
+ message: 'Se han guardado los cambios en las configuraciones.',
+ );
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.admin-settings.mail-sender-response-settings');
+ }
+}
diff --git a/Livewire/AdminSettings/MailSmtpSettings.php b/Livewire/AdminSettings/MailSmtpSettings.php
new file mode 100644
index 0000000..3ddc256
--- /dev/null
+++ b/Livewire/AdminSettings/MailSmtpSettings.php
@@ -0,0 +1,175 @@
+ 'SSL (Secure Sockets Layer)',
+ self::SMTP_ENCRYPTION_TLS => 'TLS (Transport Layer Security)',
+ self::SMTP_ENCRYPTION_NONE => 'Sin encriptación (No recomendado)',
+ ];
+
+ public $rules = [
+ [
+ 'host' => 'nullable|string|max:255',
+ 'port' => 'nullable|integer',
+ 'encryption' => 'nullable|string',
+ 'username' => 'nullable|string|max:255',
+ 'password' => 'nullable|string|max:255',
+ ],
+ [
+ 'host.string' => 'El servidor SMTP debe ser una cadena de texto.',
+ 'host.max' => 'El servidor SMTP no puede exceder los 255 caracteres.',
+ 'port.integer' => 'El puerto SMTP debe ser un número entero.',
+ 'encryption.string' => 'El tipo de encriptación SMTP debe ser una cadena de texto.',
+ 'username.string' => 'El nombre de usuario SMTP debe ser una cadena de texto.',
+ 'username.max' => 'El nombre de usuario SMTP no puede exceder los 255 caracteres.',
+ 'password.string' => 'La contraseña SMTP debe ser una cadena de texto.',
+ 'password.max' => 'La contraseña SMTP no puede exceder los 255 caracteres.',
+ ]
+ ];
+
+
+ public function mount()
+ {
+ $this->loadSettings();
+ }
+
+ public function loadSettings()
+ {
+ $globalSettingsService = app(GlobalSettingsService::class);
+
+ // Obtener los valores de las configuraciones de la base de datos
+ $settings = $globalSettingsService->getMailSystemConfig();
+
+ $this->change_smtp_settings = false;
+ $this->save_button_disabled = true;
+
+ $this->host = $settings['mailers']['smtp']['host'];
+ $this->port = $settings['mailers']['smtp']['port'];
+ $this->encryption = $settings['mailers']['smtp']['encryption'];
+ $this->username = $settings['mailers']['smtp']['username'];
+ $this->password = null;
+ }
+
+ public function save()
+ {
+ $this->validate($this->rules[0]);
+
+ $globalSettingsService = app(GlobalSettingsService::class);
+
+ // Guardar título del App en configuraciones
+ $globalSettingsService->updateSetting('mail.mailers.smtp.host', $this->host);
+ $globalSettingsService->updateSetting('mail.mailers.smtp.port', $this->port);
+ $globalSettingsService->updateSetting('mail.mailers.smtp.encryption', $this->encryption);
+ $globalSettingsService->updateSetting('mail.mailers.smtp.username', $this->username);
+ $globalSettingsService->updateSetting('mail.mailers.smtp.password', Crypt::encryptString($this->password));
+
+ $globalSettingsService->clearMailSystemConfigCache();
+
+ $this->loadSettings();
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'success',
+ message: 'Se han guardado los cambios en las configuraciones.'
+ );
+ }
+
+ public function testSmtpConnection()
+ {
+ // Validar los datos del formulario
+ $this->validate($this->rules[0]);
+
+ try {
+ // Verificar la conexión SMTP
+ if ($this->validateSMTPConnection()) {
+ $this->save_button_disabled = false;
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'success',
+ message: 'Conexión SMTP exitosa, se guardó los cambios exitosamente.',
+ );
+ }
+ } catch (\Exception $e) {
+ // Captura y maneja errores de conexión SMTP
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'danger',
+ message: 'Error en la conexión SMTP: ' . $e->getMessage(),
+ delay: 15000 // Timeout personalizado
+ );
+ }
+ }
+
+ private function validateSMTPConnection()
+ {
+ $dsn = sprintf(
+ 'smtp://%s:%s@%s:%s?encryption=%s',
+ urlencode($this->username), // Codificar nombre de usuario
+ urlencode($this->password), // Codificar contraseña
+ $this->host, // Host SMTP
+ $this->port, // Puerto SMTP
+ $this->encryption // Encriptación (tls o ssl)
+ );
+
+ // Crear el transportador usando el DSN
+ $transport = Transport::fromDsn($dsn);
+
+ // Crear el mailer con el transportador personalizado
+ $mailer = new Mailer($transport);
+
+ // Enviar un correo de prueba
+ $email = (new Email())
+ ->from($this->username) // Dirección de correo del remitente
+ ->to(env('MAIL_SANDBOX')) // Dirección de correo de destino
+ ->subject(Config::get('app.name') . ' - Correo de prueba')
+ ->text('Este es un correo de prueba para verificar la conexión SMTP.');
+
+ // Enviar el correo
+ $mailer->send($email);
+
+ return true;
+ }
+
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.admin-settings.mail-smtp-settings');
+ }
+}
diff --git a/Livewire/Cache/CacheFunctions.php b/Livewire/Cache/CacheFunctions.php
new file mode 100644
index 0000000..1ec47da
--- /dev/null
+++ b/Livewire/Cache/CacheFunctions.php
@@ -0,0 +1,212 @@
+ 0,
+ 'config' => 0,
+ 'routes' => 0,
+ 'views' => 0,
+ 'events' => 0,
+ ];
+
+ protected $listeners = [
+ 'reloadCacheFunctionsStatsEvent' => 'reloadCacheStats',
+ ];
+
+ public function mount()
+ {
+ $this->reloadCacheStats(false);
+ }
+
+ public function reloadCacheStats($notify = true)
+ {
+ $cacheDriver = config('cache.default'); // Obtiene el driver configurado para caché
+
+ // Caché General
+ switch ($cacheDriver) {
+ case 'memcached':
+ try {
+ $cacheStore = Cache::getStore()->getMemcached();
+ $stats = $cacheStore->getStats();
+
+ $this->cacheCounts['general'] = array_sum(array_column($stats, 'curr_items')); // Total de claves en Memcached
+ } catch (\Exception $e) {
+ $this->cacheCounts['general'] = 'Error obteniendo datos de Memcached';
+ }
+ break;
+
+ case 'redis':
+ try {
+ $prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
+ $keys = Redis::connection('cache')->keys($prefix . '*');
+
+ $this->cacheCounts['general'] = count($keys); // Total de claves en Redis
+ } catch (\Exception $e) {
+ $this->cacheCounts['general'] = 'Error obteniendo datos de Redis';
+ }
+ break;
+
+ case 'database':
+ try {
+ $this->cacheCounts['general'] = DB::table('cache')->count(); // Total de registros en la tabla de caché
+ } catch (\Exception $e) {
+ $this->cacheCounts['general'] = 'Error obteniendo datos de la base de datos';
+ }
+ break;
+
+ case 'file':
+ try {
+ $cachePath = config('cache.stores.file.path');
+ $files = glob($cachePath . '/*');
+
+ $this->cacheCounts['general'] = count($files);
+ } catch (\Exception $e) {
+ $this->cacheCounts['general'] = 'Error obteniendo datos de archivos';
+ }
+ break;
+
+ default:
+ $this->cacheCounts['general'] = 'Driver de caché no soportado';
+ }
+
+ // Configuración
+ $this->cacheCounts['config'] = file_exists(base_path('bootstrap/cache/config.php')) ? 1 : 0;
+
+ // Rutas
+ $this->cacheCounts['routes'] = count(glob(base_path('bootstrap/cache/routes-*.php'))) > 0 ? 1 : 0;
+
+ // Vistas
+ $this->cacheCounts['views'] = count(glob(storage_path('framework/views/*')));
+
+ // Configuración
+ $this->cacheCounts['events'] = file_exists(base_path('bootstrap/cache/events.php')) ? 1 : 0;
+
+ if ($notify) {
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: 'success',
+ message: 'Se han recargado los estadísticos de caché.'
+ );
+ }
+ }
+
+
+ public function clearLaravelCache()
+ {
+ Artisan::call('cache:clear');
+
+ sleep(1);
+
+ $this->response('Se han limpiado las cachés de la aplicación.', 'warning');
+ }
+
+ public function clearConfigCache()
+ {
+ Artisan::call('config:clear');
+
+ $this->response('Se ha limpiado la cache de la configuración de Laravel.', 'warning');
+ }
+
+ public function configCache()
+ {
+ Artisan::call('config:cache');
+ }
+
+ public function clearRouteCache()
+ {
+ Artisan::call('route:clear');
+
+ $this->response('Se han limpiado las rutas de Laravel.', 'warning');
+ }
+
+ public function cacheRoutes()
+ {
+ Artisan::call('route:cache');
+ }
+
+ public function clearViewCache()
+ {
+ Artisan::call('view:clear');
+
+ $this->response('Se han limpiado las vistas de Laravel.', 'warning');
+ }
+
+ public function cacheViews()
+ {
+ Artisan::call('view:cache');
+
+ $this->response('Se han cacheado las vistas de Laravel.');
+ }
+
+ public function clearEventCache()
+ {
+ Artisan::call('event:clear');
+
+ $this->response('Se han limpiado los eventos de Laravel.', 'warning');
+ }
+
+ public function cacheEvents()
+ {
+ Artisan::call('event:cache');
+
+ $this->response('Se han cacheado los eventos de Laravel.');
+ }
+
+ public function optimizeClear()
+ {
+ Artisan::call('optimize:clear');
+
+ $this->response('Se han optimizado todos los cachés de Laravel.');
+ }
+
+ public function resetPermissionCache()
+ {
+ Artisan::call('permission:cache-reset');
+
+ $this->response('Se han limpiado los cachés de permisos.', 'warning');
+ }
+
+ public function clearResetTokens()
+ {
+ Artisan::call('auth:clear-resets');
+
+ $this->response('Se han limpiado los tokens de reseteo de contraseña.', 'warning');
+ }
+
+ /**
+ * Genera una respuesta estandarizada.
+ */
+ private function response(string $message, string $type = 'success'): void
+ {
+ $this->reloadCacheStats(false);
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $type,
+ message: $message,
+ );
+
+ $this->dispatch('reloadCacheStatsEvent', notify: false);
+ $this->dispatch('reloadSessionStatsEvent', notify: false);
+ $this->dispatch('reloadRedisStatsEvent', notify: false);
+ $this->dispatch('reloadMemcachedStatsEvent', notify: false);
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.cache.cache-functions');
+ }
+}
diff --git a/Livewire/Cache/CacheStats.php b/Livewire/Cache/CacheStats.php
new file mode 100644
index 0000000..ab54e66
--- /dev/null
+++ b/Livewire/Cache/CacheStats.php
@@ -0,0 +1,65 @@
+ 'reloadCacheStats'];
+
+ public function mount(CacheConfigService $cacheConfigService)
+ {
+ $this->cacheConfig = $cacheConfigService->getConfig();
+
+ $this->reloadCacheStats(false);
+ }
+
+ public function reloadCacheStats($notify = true)
+ {
+ $cacheManagerService = new CacheManagerService();
+
+ $this->cacheStats = $cacheManagerService->getCacheStats();
+
+ if ($notify) {
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $this->cacheStats['status'],
+ message: $this->cacheStats['message']
+ );
+ }
+ }
+
+ public function clearCache()
+ {
+ $cacheManagerService = new CacheManagerService();
+
+ $message = $cacheManagerService->clearCache();
+
+ $this->reloadCacheStats(false);
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $message['status'],
+ message: $message['message'],
+ );
+
+ $this->dispatch('reloadRedisStatsEvent', notify: false);
+ $this->dispatch('reloadMemcachedStatsEvent', notify: false);
+ $this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.cache.cache-stats');
+ }
+}
diff --git a/Livewire/Cache/MemcachedStats.php b/Livewire/Cache/MemcachedStats.php
new file mode 100644
index 0000000..456b108
--- /dev/null
+++ b/Livewire/Cache/MemcachedStats.php
@@ -0,0 +1,64 @@
+ 'reloadCacheStats'];
+
+ public function mount()
+ {
+ $this->reloadCacheStats(false);
+ }
+
+ public function reloadCacheStats($notify = true)
+ {
+ $cacheManagerService = new CacheManagerService($this->driver);
+
+ $memcachedStats = $cacheManagerService->getMemcachedStats();
+
+ $this->memcachedStats = $memcachedStats['info'];
+
+ if ($notify) {
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $memcachedStats['status'],
+ message: $memcachedStats['message']
+ );
+ }
+ }
+
+ public function clearCache()
+ {
+ $cacheManagerService = new CacheManagerService($this->driver);
+
+ $message = $cacheManagerService->clearCache();
+
+ $this->reloadCacheStats(false);
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $message['status'],
+ message: $message['message'],
+ );
+
+ $this->dispatch('reloadCacheStatsEvent', notify: false);
+ $this->dispatch('reloadSessionStatsEvent', notify: false);
+ $this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.cache.memcached-stats');
+ }
+}
diff --git a/Livewire/Cache/RedisStats.php b/Livewire/Cache/RedisStats.php
new file mode 100644
index 0000000..25946b0
--- /dev/null
+++ b/Livewire/Cache/RedisStats.php
@@ -0,0 +1,64 @@
+ 'reloadCacheStats'];
+
+ public function mount()
+ {
+ $this->reloadCacheStats(false);
+ }
+
+ public function reloadCacheStats($notify = true)
+ {
+ $cacheManagerService = new CacheManagerService($this->driver);
+
+ $redisStats = $cacheManagerService->getRedisStats();
+
+ $this->redisStats = $redisStats['info'];
+
+ if ($notify) {
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $redisStats['status'],
+ message: $redisStats['message']
+ );
+ }
+ }
+
+ public function clearCache()
+ {
+ $cacheManagerService = new CacheManagerService($this->driver);
+
+ $message = $cacheManagerService->clearCache();
+
+ $this->reloadCacheStats(false);
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $message['status'],
+ message: $message['message'],
+ );
+
+ $this->dispatch('reloadCacheStatsEvent', notify: false);
+ $this->dispatch('reloadSessionStatsEvent', notify: false);
+ $this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.cache.redis-stats');
+ }
+}
diff --git a/Livewire/Cache/SessionStats.php b/Livewire/Cache/SessionStats.php
new file mode 100644
index 0000000..c6fb063
--- /dev/null
+++ b/Livewire/Cache/SessionStats.php
@@ -0,0 +1,63 @@
+ 'reloadSessionStats'];
+
+ public function mount(CacheConfigService $cacheConfigService)
+ {
+ $this->cacheConfig = $cacheConfigService->getConfig();
+ $this->reloadSessionStats(false);
+ }
+
+ public function reloadSessionStats($notify = true)
+ {
+ $sessionManagerService = new SessionManagerService();
+
+ $this->sessionStats = $sessionManagerService->getSessionStats();
+
+ if ($notify) {
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $this->sessionStats['status'],
+ message: $this->sessionStats['message']
+ );
+ }
+ }
+
+ public function clearSessions()
+ {
+ $sessionManagerService = new SessionManagerService();
+
+ $message = $sessionManagerService->clearSessions();
+
+ $this->reloadSessionStats(false);
+
+ $this->dispatch(
+ 'notification',
+ target: $this->targetNotify,
+ type: $message['status'],
+ message: $message['message'],
+ );
+
+ $this->dispatch('reloadRedisStatsEvent', notify: false);
+ $this->dispatch('reloadMemcachedStatsEvent', notify: false);
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.cache.session-stats');
+ }
+}
diff --git a/Livewire/Form/AbstractFormComponent.php b/Livewire/Form/AbstractFormComponent.php
new file mode 100644
index 0000000..eceaca3
--- /dev/null
+++ b/Livewire/Form/AbstractFormComponent.php
@@ -0,0 +1,515 @@
+uniqueId = uniqid();
+ $this->mode = $mode;
+ $this->id = $id;
+
+ $model = new ($this->model());
+
+ $this->tagName = $model->tagName;
+ $this->columnNameLabel = $model->columnNameLabel;
+ $this->singularName = $model->singularName;
+ $this->formId = Str::camel($model->tagName) .'Form';
+
+ $this->setBtnSubmitText();
+
+ if ($this->mode !== 'create' && $this->id) {
+ // Si no es modo 'create', cargamos el registro desde la BD
+ $record = $this->model()::findOrFail($this->id);
+
+ $this->initializeFormData($record, $mode);
+
+ } else {
+ // Modo 'create', o sin ID: iniciamos datos vacíos
+ $this->initializeFormData(null, $mode);
+ }
+ }
+
+ /**
+ * Configura el texto del botón principal de envío, basado en la propiedad $mode.
+ *
+ * @return void
+ */
+ private function setBtnSubmitText(): void
+ {
+ $this->btnSubmitText = match ($this->mode) {
+ 'create' => 'Crear ' . $this->singularName(),
+ 'edit' => 'Guardar cambios',
+ 'delete' => 'Eliminar ' . $this->singularName(),
+ default => 'Enviar'
+ };
+ }
+
+ /**
+ * Retorna el "singularName" definido en el modelo asociado.
+ * Permite también decidir si se devuelve con la primera letra en mayúscula
+ * o en minúscula.
+ *
+ * @param string $type Puede ser 'uppercase' o 'lowercase'. Por defecto, 'lowercase'.
+ * @return string Nombre en singular del modelo, formateado.
+ */
+ private function singularName($type = 'lowercase'): string
+ {
+ /** @var Model $model */
+ $model = new ($this->model());
+
+ return $type === 'uppercase'
+ ? ucfirst($model->singularName)
+ : lcfirst($model->singularName);
+ }
+
+ /**
+ * Método del ciclo de vida de Livewire que se llama en cada hidratación.
+ * Puedes disparar eventos o manejar lógica que suceda en cada request
+ * una vez que Livewire 'rehidrate' el componente en el servidor.
+ *
+ * @return void
+ */
+ public function hydrate(): void
+ {
+ $this->dispatch($this->dispatches()['on-hydrate']);
+ }
+
+ // ======================================================================
+ // OPERACIONES CRUD
+ // ======================================================================
+
+ /**
+ * Método principal de envío del formulario (submit). Gestiona los flujos
+ * de crear, editar o eliminar un registro dentro de una transacción de BD.
+ *
+ * @return void
+ */
+ public function onSubmit(): void
+ {
+ DB::beginTransaction();
+
+ try {
+ if ($this->mode === 'delete') {
+ $this->delete();
+ } else {
+ $this->save();
+ }
+
+ DB::commit();
+
+ } catch (ValidationException $e) {
+ DB::rollBack();
+ $this->handleValidationException($e);
+
+ } catch (QueryException $e) {
+ DB::rollBack();
+ $this->handleDatabaseException($e);
+
+ } catch (ModelNotFoundException $e) {
+ DB::rollBack();
+ $this->handleException('danger', 'Registro no encontrado.');
+
+ } catch (Exception $e) {
+ DB::rollBack();
+ $this->handleException('danger', 'Error al eliminar el registro: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * Crea o actualiza un registro en la base de datos,
+ * aplicando validaciones y llamadas a hooks antes y después de guardar.
+ *
+ * @return void
+ * @throws ValidationException
+ */
+ protected function save(): void
+ {
+ // Validamos los datos, con posibles atributos y mensajes personalizados
+ $validatedData = $this->validate(
+ $this->dynamicRules($this->mode),
+ $this->messages(),
+ $this->attributes()
+ );
+
+ // Hook previo (por referencia)
+ $this->beforeSave($validatedData);
+
+ // Ajustamos/convertimos los datos finales
+ $data = $this->prepareData($validatedData);
+ $record = $this->model()::updateOrCreate(['id' => $this->id], $data);
+
+ // Hook posterior
+ $this->afterSave($record);
+
+ // Notificamos éxito
+ $this->handleSuccess('success', $this->singularName('uppercase') . " guardado correctamente.");
+ }
+
+ /**
+ * Elimina un registro de la base de datos (modo 'delete'),
+ * aplicando validaciones y hooks antes y después de la eliminación.
+ *
+ * @return void
+ * @throws ValidationException
+ */
+ protected function delete(): void
+ {
+ $this->validate($this->dynamicRules('delete', $this->messages(), $this->attributes()));
+
+ $record = $this->model()::findOrFail($this->id);
+
+ // Hook antes de la eliminación
+ $this->beforeDelete($record);
+
+ $record->delete();
+
+ // Hook después de la eliminación
+ $this->afterDelete($record);
+
+ $this->handleSuccess('warning', $this->singularName('uppercase') . " eliminado.");
+ }
+
+ // ======================================================================
+ // HOOKS DE ACCIONES
+ // ======================================================================
+
+ /**
+ * Hook que se ejecuta antes de guardar o actualizar un registro.
+ * Puede usarse para ajustar o limpiar datos antes de la operación en base de datos.
+ *
+ * @param array $data Datos validados que se van a guardar.
+ * Se pasa por referencia para permitir cambios.
+ * @return void
+ */
+ protected function beforeSave(array &$data): void {}
+
+ /**
+ * Hook que se ejecuta después de guardar o actualizar un registro.
+ * Puede usarse para acciones como disparar eventos, notificaciones a otros sistemas, etc.
+ *
+ * @param mixed $record Instancia del modelo recién creado o actualizado.
+ * @return void
+ */
+ protected function afterSave($record): void {}
+
+ /**
+ * Hook que se ejecuta antes de eliminar un registro.
+ * Puede emplearse para validaciones adicionales o limpieza de datos relacionados.
+ *
+ * @param mixed $record Instancia del modelo que se eliminará.
+ * @return void
+ */
+ protected function beforeDelete($record): void {}
+
+ /**
+ * Hook que se ejecuta después de eliminar un registro.
+ * Útil para operaciones finales, como remover archivos relacionados o
+ * disparar un evento de "elemento eliminado".
+ *
+ * @param mixed $record Instancia del modelo que se acaba de eliminar.
+ * @return void
+ */
+ protected function afterDelete($record): void {}
+
+ // ======================================================================
+ // MANEJO DE VALIDACIONES Y ERRORES
+ // ======================================================================
+
+ /**
+ * Maneja las excepciones de validación (ValidationException).
+ * Asigna los errores al error bag de Livewire y muestra notificaciones.
+ *
+ * @param ValidationException $e Excepción de validación.
+ * @return void
+ */
+ protected function handleValidationException(ValidationException $e): void
+ {
+ $this->setErrorBag($e->validator->errors());
+ $this->handleException('danger', 'Error en la validación de los datos.');
+ $this->dispatch($this->dispatches()['on-failed-validation']);
+ }
+
+ /**
+ * Maneja las excepciones de base de datos (QueryException).
+ * Incluye casos especiales para claves foráneas y duplicadas.
+ *
+ * @param QueryException $e Excepción de consulta a la base de datos.
+ * @return void
+ */
+ protected function handleDatabaseException(QueryException $e): void
+ {
+ $errorMessage = match ($e->errorInfo[1]) {
+ 1452 => "Una clave foránea no es válida.",
+ 1062 => $this->extractDuplicateField($e->getMessage()),
+ 1451 => "No se puede eliminar el registro porque está en uso.",
+ default => env('APP_DEBUG') ? $e->getMessage() : "Error inesperado en la base de datos.",
+ };
+
+ $this->handleException('danger', $errorMessage, 'form', 120000);
+ }
+
+ /**
+ * Maneja excepciones o errores generales, mostrando una notificación al usuario.
+ *
+ * @param string $type Tipo de notificación (por ejemplo, 'success', 'warning', 'danger').
+ * @param string $message Mensaje que se mostrará en la notificación.
+ * @param string $target Objetivo/área donde se mostrará la notificación ('form', 'index', etc.).
+ * @param int $delay Tiempo en milisegundos que la notificación permanecerá visible.
+ * @return void
+ */
+ protected function handleException($type, $message, $target = 'form', $delay = 9000): void
+ {
+ $this->dispatchNotification($type, $message, $target, $delay);
+ }
+
+ /**
+ * Extrae el campo duplicado de un mensaje de error MySQL, para mostrar un mensaje amigable.
+ *
+ * @param string $errorMessage Mensaje de error completo de la base de datos.
+ * @return string Mensaje simplificado indicando cuál campo está duplicado.
+ */
+ private function extractDuplicateField($errorMessage): string
+ {
+ preg_match("/for key 'unique_(.*?)'/", $errorMessage, $matches);
+
+ return isset($matches[1])
+ ? "El valor ingresado para '" . str_replace('_', ' ', $matches[1]) . "' ya está en uso."
+ : "Ya existe un registro con este valor.";
+ }
+
+ // ======================================================================
+ // NOTIFICACIONES Y REDIRECCIONAMIENTOS
+ // ======================================================================
+
+ /**
+ * Maneja el flujo de notificación y redirección cuando una operación
+ * (guardar, eliminar) finaliza satisfactoriamente.
+ *
+ * @param string $type Tipo de notificación ('success', 'warning', etc.).
+ * @param string $message Mensaje a mostrar.
+ * @return void
+ */
+ protected function handleSuccess($type, $message): void
+ {
+ $this->dispatchNotification($type, $message, 'index');
+ $this->redirectRoute($this->getRedirectRoute());
+ }
+
+ /**
+ * Envía una notificación al navegador (mediante eventos de Livewire)
+ * indicando el tipo, el mensaje y el destino donde debe visualizarse.
+ *
+ * @param string $type Tipo de notificación (success, danger, etc.).
+ * @param string $message Mensaje de la notificación.
+ * @param string $target Destino para mostrarla ('form', 'index', etc.).
+ * @param int $delay Duración de la notificación en milisegundos.
+ * @return void
+ */
+ protected function dispatchNotification($type, $message, $target = 'form', $delay = 9000): void
+ {
+ $this->dispatch(
+ $target == 'index' ? 'store-notification' : 'notification',
+ target: $target === 'index' ? $this->targetNotifies()['index'] : $this->targetNotifies()['form'],
+ type: $type,
+ message: $message,
+ delay: $delay
+ );
+ }
+
+ // ======================================================================
+ // RENDERIZACIÓN
+ // ======================================================================
+
+ /**
+ * Renderiza la vista Blade asociada a este componente.
+ * Retorna un objeto Illuminate\View\View.
+ *
+ * @return View
+ */
+ public function render(): View
+ {
+ return view($this->viewPath());
+ }
+}
diff --git a/Livewire/Form/AbstractFormOffCanvasComponent.php b/Livewire/Form/AbstractFormOffCanvasComponent.php
new file mode 100644
index 0000000..8512a26
--- /dev/null
+++ b/Livewire/Form/AbstractFormOffCanvasComponent.php
@@ -0,0 +1,667 @@
+
+ */
+ protected $casts = [];
+
+ // ===================== MÉTODOS ABSTRACTOS =====================
+
+ /**
+ * Define el modelo Eloquent asociado con el formulario.
+ *
+ * @return string
+ */
+ abstract protected function model(): string;
+
+ /**
+ * Define los campos del formulario.
+ *
+ * @return array
+ */
+ abstract protected function fields(): array;
+
+ /**
+ * Retorna los valores por defecto para los campos del formulario.
+ *
+ * @return array Valores predeterminados.
+ */
+ abstract protected function defaults(): array;
+
+ /**
+ * Campo que se debe enfocar cuando se abra el formulario.
+ *
+ * @return string
+ */
+ abstract protected function focusOnOpen(): string;
+
+ /**
+ * Define reglas de validación dinámicas según el modo del formulario.
+ *
+ * @param string $mode Modo actual del formulario ('create', 'edit', 'delete').
+ * @return array Reglas de validación.
+ */
+ abstract protected function dynamicRules(string $mode): array;
+
+ /**
+ * Devuelve las opciones que se mostrarán en los selectores del formulario.
+ *
+ * @return array Opciones para los campos del formulario.
+ */
+ abstract protected function options(): array;
+
+ /**
+ * Retorna la ruta de la vista asociada al formulario.
+ *
+ * @return string Ruta de la vista Blade.
+ */
+ abstract protected function viewPath(): string;
+
+ // ===================== VALIDACIONES =====================
+
+ protected function attributes(): array
+ {
+ return [];
+ }
+
+ protected function messages(): array
+ {
+ return [];
+ }
+
+ // ===================== INICIALIZACIÓN DEL COMPONENTE =====================
+
+ /**
+ * Se ejecuta cuando el componente se monta por primera vez.
+ *
+ * Inicializa propiedades y carga datos iniciales.
+ *
+ * @return void
+ */
+ public function mount(): void
+ {
+ $this->uniqueId = uniqid();
+
+ $model = new ($this->model());
+
+ $this->tagName = $model->tagName;
+ $this->columnNameLabel = $model->columnNameLabel;
+ $this->singularName = $model->singularName;
+ $this->offcanvasId = 'offcanvas' . ucfirst(Str::camel($model->tagName));
+ $this->formId = Str::camel($model->tagName) .'Form';
+ $this->focusOnOpen = "{$this->focusOnOpen()}_{$this->uniqueId}";
+
+ $this->loadDefaults();
+ $this->loadOptions();
+ }
+
+ // ===================== INICIALIZACIÓN Y CONFIGURACIÓN =====================
+
+ /**
+ * Devuelve los valores por defecto para los campos del formulario.
+ *
+ * @return array Valores por defecto.
+ */
+ private function loadDefaults(): void
+ {
+ $this->defaultValues = $this->defaults();
+ }
+
+ /**
+ * Carga las opciones necesarias para los campos del formulario.
+ *
+ * @return void
+ */
+ private function loadOptions(): void
+ {
+ foreach ($this->options() as $key => $value) {
+ $this->$key = $value;
+ }
+ }
+
+ /**
+ * Carga los datos de un modelo específico en el formulario para su edición.
+ *
+ * @param int $id ID del registro a editar.
+ * @return void
+ */
+ public function loadFormModel(int $id): void
+ {
+ if ($this->loadData($id)) {
+ $this->mode = 'edit';
+
+ $this->dispatch($this->getDispatche('refresh-offcanvas'));
+ }
+ }
+
+ /**
+ * Carga el modelo para confirmar su eliminación.
+ *
+ * @param int $id ID del registro a eliminar.
+ * @return void
+ */
+ public function loadFormModelForDeletion(int $id): void
+ {
+ if ($this->loadData($id)) {
+ $this->mode = 'delete';
+ $this->confirmDeletion = false;
+
+ $this->dispatch($this->getDispatche('refresh-offcanvas'));
+ }
+ }
+
+ private function getDispatche(string $name): string
+ {
+ $model = new ($this->model());
+
+ $dispatches = [
+ 'refresh-offcanvas' => 'refresh-' . Str::kebab($model->tagName) . '-offcanvas',
+ 'reload-table' => 'reload-bt-' . Str::kebab($model->tagName) . 's',
+ ];
+
+ return $dispatches[$name] ?? null;
+ }
+
+
+ /**
+ * Carga los datos del modelo según el ID proporcionado.
+ *
+ * @param int $id ID del modelo.
+ * @return bool True si los datos fueron cargados correctamente.
+ */
+ protected function loadData(int $id): bool
+ {
+ $model = $this->model()::find($id);
+
+ if ($model) {
+ $data = $model->only(['id', ...$this->fields()]);
+
+ $this->applyCasts($data);
+ $this->fill($data);
+
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // ===================== OPERACIONES CRUD =====================
+
+ /**
+ * Método principal para enviar el formulario.
+ *
+ * @return void
+ */
+ public function onSubmit(): void
+ {
+ $this->successProcess = false;
+ $this->validationError = false;
+
+ if(!$this->mode)
+ $this->mode = 'create';
+
+ DB::beginTransaction(); // Iniciar transacción
+
+ try {
+ if($this->mode === 'delete'){
+ $this->delete();
+
+ }else{
+ $this->save();
+ }
+
+ DB::commit();
+
+ } catch (ValidationException $e) {
+ DB::rollBack();
+ $this->handleValidationException($e);
+
+ } catch (QueryException $e) {
+ DB::rollBack();
+ $this->handleDatabaseException($e);
+
+ } catch (ModelNotFoundException $e) {
+ DB::rollBack();
+ $this->handleException('danger', 'Registro no encontrado.');
+
+ } catch (Exception $e) {
+ DB::rollBack(); // Revertir la transacción si ocurre un error
+ $this->handleException('danger', 'Error al eliminar el registro: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * Guarda o actualiza un registro en la base de datos.
+ *
+ * @return void
+ * @throws ValidationException
+ */
+ protected function save(): void
+ {
+ // Valida incluyendo atributos personalizados
+ $validatedData = $this->validate(
+ $this->dynamicRules($this->mode),
+ $this->messages(),
+ $this->attributes()
+ );
+
+ $this->convertEmptyValuesToNull($validatedData);
+ $this->applyCasts($validatedData);
+
+ $this->beforeSave($validatedData);
+ $record = $this->model()::updateOrCreate(['id' => $this->id], $validatedData);
+ $this->afterSave($record);
+
+ $this->handleSuccess('success', ucfirst($this->singularName) . " guardado correctamente.");
+ }
+
+ /**
+ * Elimina un registro en la base de datos.
+ *
+ * @return void
+ */
+ protected function delete(): void
+ {
+ $this->validate($this->dynamicRules(
+ 'delete',
+ $this->messages(),
+ $this->attributes()
+ ));
+
+ $record = $this->model()::findOrFail($this->id);
+
+ $this->beforeDelete($record);
+ $record->delete();
+ $this->afterDelete($record);
+
+ $this->handleSuccess('warning', ucfirst($this->singularName) . " eliminado.");
+ }
+
+ // ===================== HOOKS DE ACCIONES CRUD =====================
+
+ /**
+ * Hook que se ejecuta antes de guardar datos en la base de datos.
+ *
+ * Este método permite realizar modificaciones o preparar los datos antes de ser validados
+ * y almacenados. Es útil para formatear datos, agregar valores calculados o realizar
+ * operaciones previas a la persistencia.
+ *
+ * @param array $data Datos validados que se almacenarán. Se pasan por referencia,
+ * por lo que cualquier cambio aquí afectará directamente los datos guardados.
+ *
+ * @return void
+ */
+ protected function beforeSave(array &$data): void {}
+
+ /**
+ * Hook que se ejecuta después de guardar o actualizar un registro en la base de datos.
+ *
+ * Ideal para ejecutar tareas posteriores al guardado, como enviar notificaciones,
+ * registrar auditorías o realizar acciones en otros modelos relacionados.
+ *
+ * @param \Illuminate\Database\Eloquent\Model $record El modelo que fue guardado, conteniendo
+ * los datos actualizados.
+ *
+ * @return void
+ */
+ protected function afterSave($record): void {}
+
+ /**
+ * Hook que se ejecuta antes de eliminar un registro de la base de datos.
+ *
+ * Permite validar si el registro puede ser eliminado o realizar tareas previas
+ * como desasociar relaciones, eliminar archivos relacionados o verificar restricciones.
+ *
+ * @param \Illuminate\Database\Eloquent\Model $record El modelo que está por ser eliminado.
+ *
+ * @return void
+ */
+ protected function beforeDelete($record): void {}
+
+ /**
+ * Hook que se ejecuta después de eliminar un registro de la base de datos.
+ *
+ * Útil para realizar acciones adicionales tras la eliminación, como limpiar datos relacionados,
+ * eliminar archivos vinculados o registrar eventos de auditoría.
+ *
+ * @param \Illuminate\Database\Eloquent\Model $record El modelo eliminado. Aunque ya no existe en la base de datos,
+ * se conserva la información del registro en memoria.
+ *
+ * @return void
+ */
+ protected function afterDelete($record): void {}
+
+ // ===================== MANEJO DE VALIDACIONES Y EXCEPCIONES =====================
+
+ /**
+ * Maneja las excepciones de validación.
+ *
+ * Este método captura los errores de validación, los agrega al error bag de Livewire
+ * y dispara un evento para manejar el fallo de validación, útil en formularios modales.
+ *
+ * @param ValidationException $e Excepción de validación capturada.
+ * @return void
+ */
+ protected function handleValidationException(ValidationException $e): void
+ {
+ $this->setErrorBag($e->validator->errors());
+
+ // Notifica al usuario que ocurrió un error de validación
+ $this->handleException('danger', 'Error en la validación de los datos.');
+ }
+
+ /**
+ * Maneja las excepciones relacionadas con la base de datos.
+ *
+ * Analiza el código de error de la base de datos y genera un mensaje de error específico
+ * para la situación. También se encarga de enviar una notificación de error.
+ *
+ * @param QueryException $e Excepción capturada durante la ejecución de una consulta.
+ * @return void
+ */
+ protected function handleDatabaseException(QueryException $e): void
+ {
+ $errorMessage = match ($e->errorInfo[1]) {
+ 1452 => "Una clave foránea no es válida.",
+ 1062 => $this->extractDuplicateField($e->getMessage()),
+ 1451 => "No se puede eliminar el registro porque está en uso.",
+ default => env('APP_DEBUG') ? $e->getMessage() : "Error inesperado en la base de datos.",
+ };
+
+ $this->handleException('danger', $errorMessage, 'form', 120000);
+ }
+
+ /**
+ * Maneja cualquier tipo de excepción general y envía una notificación al usuario.
+ *
+ * @param string $type El tipo de notificación (success, danger, warning).
+ * @param string $message El mensaje que se mostrará al usuario.
+ * @param string $target El contenedor donde se mostrará la notificación (por defecto 'form').
+ * @param int $delay Tiempo en milisegundos que durará la notificación en pantalla.
+ * @return void
+ */
+ protected function handleException($type, $message, $target = 'form', $delay = 9000): void
+ {
+ $this->validationError = true;
+
+ $this->dispatch($this->getDispatche('refresh-offcanvas'));
+ $this->dispatchNotification($type, $message, $target, $delay);
+ }
+
+ /**
+ * Extrae el nombre del campo duplicado de un error de base de datos MySQL.
+ *
+ * Esta función se utiliza para identificar el campo específico que causó un error
+ * de duplicación de clave única, y genera un mensaje personalizado para el usuario.
+ *
+ * @param string $errorMessage El mensaje de error completo proporcionado por MySQL.
+ * @return string Mensaje de error amigable para el usuario.
+ */
+ private function extractDuplicateField($errorMessage): string
+ {
+ preg_match("/for key 'unique_(.*?)'/", $errorMessage, $matches);
+
+ return isset($matches[1])
+ ? "El valor ingresado para '" . str_replace('_', ' ', $matches[1]) . "' ya está en uso."
+ : "Ya existe un registro con este valor.";
+ }
+
+ // ===================== NOTIFICACIONES Y ÉXITO =====================
+
+ /**
+ * Despacha una notificación tras el éxito de una operación.
+ *
+ * @param string $type Tipo de notificación (success, warning, danger)
+ * @param string $message Mensaje a mostrar.
+ * @return void
+ */
+ protected function handleSuccess(string $type, string $message): void
+ {
+ $this->successProcess = true;
+
+ $this->dispatch($this->getDispatche('refresh-offcanvas'));
+ $this->dispatch($this->getDispatche('reload-table'));
+
+ $this->dispatchNotification($type, $message, 'index');
+ }
+
+ /**
+ * Envía una notificación al navegador.
+ *
+ * @param string $type Tipo de notificación (success, danger, etc.)
+ * @param string $message Mensaje de la notificación
+ * @param string $target Destino (form, index)
+ * @param int $delay Duración de la notificación en milisegundos
+ */
+ protected function dispatchNotification($type, $message, $target = 'form', $delay = 9000): void
+ {
+ $model = new ($this->model());
+
+ $this->tagName = $model->tagName;
+ $this->columnNameLabel = $model->columnNameLabel;
+ $this->singularName = $model->singularName;
+
+ $tagOffcanvas = ucfirst(Str::camel($model->tagName));
+
+ $targetNotifies = [
+ "index" => '#bt-' . Str::kebab($model->tagName) . 's .notification-container',
+ "form" => "#offcanvas{$tagOffcanvas} .notification-container",
+ ];
+
+ $this->dispatch(
+ 'notification',
+ target: $target === 'index' ? $targetNotifies['index'] : $targetNotifies['form'],
+ type: $type,
+ message: $message,
+ delay: $delay
+ );
+ }
+
+ // ===================== FORMULARIO Y CONVERSIÓN DE DATOS =====================
+
+ /**
+ * Convierte los valores vacíos a `null` en los campos que son configurados como `nullable`.
+ *
+ * Esta función verifica las reglas de validación actuales y transforma todos los campos vacíos
+ * en valores `null` si las reglas permiten valores nulos. Es útil para evitar insertar cadenas vacías
+ * en la base de datos donde se espera un valor nulo.
+ *
+ * @param array $data Los datos del formulario que se deben procesar.
+ * @return void
+ */
+ protected function convertEmptyValuesToNull(array &$data): void
+ {
+ $nullableFields = array_keys(array_filter($this->dynamicRules($this->mode), function ($rules) {
+ return in_array('nullable', (array) $rules);
+ }));
+
+ foreach ($nullableFields as $field) {
+ if (isset($data[$field]) && $data[$field] === '') {
+ $data[$field] = null;
+ }
+ }
+ }
+
+ /**
+ * Aplica tipos de datos definidos en `$casts` a los campos del formulario.
+ *
+ * Esta función toma los datos de entrada y los transforma en el tipo de datos esperado según
+ * lo definido en la propiedad `$casts`. Es útil para asegurar que los datos se almacenen en
+ * el formato correcto, como convertir cadenas a números enteros o booleanos.
+ *
+ * @param array $data Los datos del formulario que necesitan ser casteados.
+ * @return void
+ */
+ protected function applyCasts(array &$data): void
+ {
+ foreach ($this->casts as $field => $type) {
+ if (array_key_exists($field, $data)) {
+ $data[$field] = $this->castValue($type, $data[$field]);
+ }
+ }
+ }
+
+ /**
+ * Castea un valor a su tipo de dato correspondiente.
+ *
+ * Convierte un valor dado al tipo especificado, manejando adecuadamente los valores vacíos
+ * o nulos. También asegura que valores como `0` o `''` sean tratados correctamente
+ * para evitar errores al almacenarlos en la base de datos.
+ *
+ * @param string $type El tipo de dato al que se debe convertir (`boolean`, `integer`, `float`, `string`, `array`).
+ * @param mixed $value El valor que se debe castear.
+ * @return mixed El valor convertido al tipo especificado.
+ */
+ protected function castValue($type, $value): mixed
+ {
+ // Convertir valores vacíos o cero a null si corresponde
+ if (is_null($value) || $value === '' || $value === '0' || $value === 0.0) {
+ return match ($type) {
+ 'boolean' => false, // No permitir null en booleanos
+ 'integer' => 0, // Valor por defecto para enteros
+ 'float', 'double' => 0.0, // Valor por defecto para decimales
+ 'string' => "", // Convertir cadena vacía en null
+ 'array' => [], // Evitar null en arrays
+ default => null, // Valor por defecto para otros tipos
+ };
+ }
+
+ // Castear el valor si no es null ni vacío
+ return match ($type) {
+ 'boolean' => (bool) $value,
+ 'integer' => (int) $value,
+ 'float', 'double' => (float) $value,
+ 'string' => (string) $value,
+ 'array' => (array) $value,
+ default => $value,
+ };
+ }
+
+
+ // ===================== RENDERIZACIÓN DE VISTA =====================
+
+ /**
+ * Renderiza la vista del formulario.
+ *
+ * @return \Illuminate\View\View
+ */
+ public function render(): View
+ {
+ return view($this->viewPath());
+ }
+}
diff --git a/Livewire/Permissions/PermissionIndex.php b/Livewire/Permissions/PermissionIndex.php
new file mode 100644
index 0000000..2aa71da
--- /dev/null
+++ b/Livewire/Permissions/PermissionIndex.php
@@ -0,0 +1,28 @@
+roles_html_select = " Selecciona un rol ";
+
+ foreach (Role::all() as $role) {
+ $this->rows_roles[$role->name] = "style} m-1\">{$role->name} ";
+ $this->roles_html_select .= "name}\" class=\"text-capitalize\">{$role->name} ";
+ }
+
+ $this->roles_html_select .= " ";
+
+ return view('vuexy-admin::livewire.permissions.index');
+ }
+}
diff --git a/Livewire/Permissions/Permissions.php b/Livewire/Permissions/Permissions.php
new file mode 100644
index 0000000..661bc7f
--- /dev/null
+++ b/Livewire/Permissions/Permissions.php
@@ -0,0 +1,35 @@
+validate([
+ 'permissionName' => 'required|unique:permissions,name'
+ ]);
+
+ Permission::create(['name' => $this->permissionName]);
+ session()->flash('message', 'Permiso creado con éxito.');
+ $this->reset('permissionName');
+ }
+
+ public function deletePermission($id)
+ {
+ Permission::find($id)->delete();
+ session()->flash('message', 'Permiso eliminado.');
+ }
+
+ public function render()
+ {
+ return view('livewire.permissions', [
+ 'permissions' => Permission::all()
+ ]);
+ }
+}
diff --git a/Livewire/Roles/RoleCards.php b/Livewire/Roles/RoleCards.php
new file mode 100644
index 0000000..613bf10
--- /dev/null
+++ b/Livewire/Roles/RoleCards.php
@@ -0,0 +1,182 @@
+loadRolesAndPermissions();
+ $this->dispatch('reloadForm');
+ }
+
+ private function loadRolesAndPermissions()
+ {
+ $this->roles = Auth::user()->hasRole('SuperAdmin') ?
+ Role::all() :
+ Role::where('name', '!=', 'SuperAdmin')->get();
+
+ // Obtener todos los permisos
+ $permissions = Permission::all()->map(function ($permission) {
+ $name = $permission->name;
+ $action = substr($name, strrpos($name, '.') + 1);
+
+ return [
+ 'group_name' => $permission->group_name,
+ 'sub_group_name' => $permission->sub_group_name,
+ $action => $name // Agregar la acción directamente al array
+ ];
+ })->groupBy('group_name'); // Agrupar los permisos por grupo
+
+
+ // Procesar los permisos agrupados para cargarlos en el componente
+ $permissionsInputs = [];
+
+ $this->permissions = $permissions->map(function ($groupPermissions) use (&$permissionsInputs) {
+ $permission = [
+ 'group_name' => $groupPermissions[0]['group_name'], // Tomar el grupo del primer permiso del grupo
+ 'sub_group_name' => $groupPermissions[0]['sub_group_name'], // Tomar la descripción del primer permiso del grupo
+ ];
+
+ // Agregar todas las acciones al permissionsInputs y al permission
+ foreach ($groupPermissions as $permissionData) {
+ foreach ($permissionData as $key => $value) {
+ if ($key !== 'sub_group_name' && $key !== 'group_name') {
+ $permissionsInputs[str_replace('.', '_', $value)] = false;
+ $permission[$key] = $value;
+ }
+ }
+ }
+
+ return $permission;
+ });
+
+ $this->permissionsInputs = $permissionsInputs;
+ }
+
+ public function loadRoleData($action, $roleId = false)
+ {
+ $this->resetForm();
+
+ $this->title = 'Agregar un nuevo rol';
+ $this->btn_submit_text = 'Crear nuevo rol';
+
+ if ($roleId) {
+ $role = Role::findOrFail($roleId);
+
+ switch ($action) {
+ case 'view':
+ $this->title = $role->name;
+ $this->name = $role->name;
+ $this->style = $role->style;
+ $this->dispatch('deshabilitarFormulario');
+ break;
+
+ case 'update':
+ $this->title = 'Editar rol';
+ $this->btn_submit_text = 'Guardar cambios';
+ $this->roleId = $roleId;
+ $this->name = $role->name;
+ $this->style = $role->style;
+ $this->dispatch('habilitarFormulario');
+ break;
+
+ case 'clone':
+ $this->style = $role->style;
+ $this->dispatch('habilitarFormulario');
+ break;
+
+ default:
+ break;
+ }
+
+ foreach ($role->permissions as $permission) {
+ $this->permissionsInputs[str_replace('.', '_', $permission->name)] = true;
+ }
+ }
+
+ $this->dispatch('reloadForm');
+ }
+
+ public function loadDestroyRoleData() {}
+
+ public function saveRole()
+ {
+ $permissions = [];
+
+ foreach ($this->permissionsInputs as $permission => $value) {
+ if ($value === true)
+ $permissions[] = str_replace('_', '.', $permission);
+ }
+
+ if ($this->roleId) {
+ $role = Role::find($this->roleId);
+
+ $role->name = $this->name;
+ $role->style = $this->style;
+
+ $role->save();
+
+ $role->syncPermissions($permissions);
+ } else {
+ $role = Role::create([
+ 'name' => $this->name,
+ 'style' => $this->style,
+ ]);
+
+ $role->syncPermissions($permissions);
+ }
+
+ $this->loadRolesAndPermissions();
+
+ $this->dispatch('modalHide');
+ $this->dispatch('reloadForm');
+ }
+
+ public function deleteRole()
+ {
+ $role = Role::find($this->destroyRoleId);
+
+ if ($role)
+ $role->delete();
+
+ $this->loadRolesAndPermissions();
+
+ $this->dispatch('modalDeleteHide');
+ $this->dispatch('reloadForm');
+ }
+
+ private function resetForm()
+ {
+ $this->roleId = '';
+ $this->name = '';
+ $this->style = '';
+
+ foreach ($this->permissionsInputs as $key => $permission) {
+ $this->permissionsInputs[$key] = false;
+ }
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.roles.cards');
+ }
+}
diff --git a/Livewire/Roles/RoleIndex.php b/Livewire/Roles/RoleIndex.php
new file mode 100644
index 0000000..10a168d
--- /dev/null
+++ b/Livewire/Roles/RoleIndex.php
@@ -0,0 +1,61 @@
+availablePermissions = Permission::all();
+ }
+
+ public function createRole()
+ {
+ $this->validate([
+ 'roleName' => 'required|unique:roles,name'
+ ]);
+
+ $role = Role::create(['name' => $this->roleName]);
+ $this->reset(['roleName']);
+ session()->flash('message', 'Rol creado con éxito.');
+ }
+
+ public function selectRole($roleId)
+ {
+ $this->selectedRole = Role::find($roleId);
+ $this->permissions = $this->selectedRole->permissions->pluck('id')->toArray();
+ }
+
+ public function updateRolePermissions()
+ {
+ if ($this->selectedRole) {
+ $this->selectedRole->syncPermissions($this->permissions);
+ session()->flash('message', 'Permisos actualizados correctamente.');
+ }
+ }
+
+ public function deleteRole($roleId)
+ {
+ Role::find($roleId)->delete();
+ session()->flash('message', 'Rol eliminado.');
+ }
+
+ public function render()
+ {
+ return view('livewire.roles', [
+ 'index' => Role::paginate(10)
+ ]);
+ }
+}
diff --git a/Livewire/Table/AbstractIndexComponent.php b/Livewire/Table/AbstractIndexComponent.php
new file mode 100644
index 0000000..93bf75f
--- /dev/null
+++ b/Livewire/Table/AbstractIndexComponent.php
@@ -0,0 +1,174 @@
+ 'id', // Campo por defecto para ordenar
+ 'exportFileName' => 'Listado', // Nombre de archivo para exportar
+ 'showFullscreen' => false,
+ 'showPaginationSwitch'=> false,
+ 'showRefresh' => false,
+ 'pagination' => false,
+ // Agrega aquí cualquier otra configuración por defecto que uses
+ ];
+ }
+
+ /**
+ * Se ejecuta al montar el componente Livewire.
+ * Configura $tagName, $singularName, $formId y $bt_datatable.
+ *
+ * @return void
+ */
+ public function mount(): void
+ {
+ // Obtenemos el modelo
+ $model = $this->model();
+ if (is_string($model)) {
+ // Si se retornó la clase en abstract protected function model(),
+ // instanciamos manualmente
+ $model = new $model;
+ }
+
+ // Usamos las propiedades definidas en el modelo
+ // (tagName, singularName, etc.), si existen en el modelo.
+ // Ajusta nombres según tu convención.
+ $this->tagName = $model->tagName ?? Str::snake(class_basename($model));
+ $this->singularName = $model->singularName ?? class_basename($model);
+ $this->formId = Str::kebab($this->tagName) . '-form';
+
+ // Inicia la configuración principal de la tabla
+ $this->setupDataTable();
+ }
+
+ /**
+ * Combina la configuración base de la tabla con las columnas y formatos
+ * definidos en las clases hijas.
+ *
+ * @return void
+ */
+ protected function setupDataTable(): void
+ {
+ $baseConfig = $this->bootstraptableConfig();
+
+ $this->bt_datatable = array_merge($baseConfig, [
+ 'header' => $this->columns(),
+ 'format' => $this->format(),
+ ]);
+ }
+
+ /**
+ * Renderiza la vista definida en viewPath().
+ *
+ * @return \Illuminate\View\View
+ */
+ public function render()
+ {
+ return view($this->viewPath());
+ }
+
+ /**
+ * Ejemplo de método para la lógica de filtrado que podrías sobreescribir en la clase hija.
+ *
+ * @param array $criteria
+ * @return \Illuminate\Database\Eloquent\Builder
+ */
+ protected function applyFilters($criteria = [])
+ {
+ // Aplica tu lógica de filtros, búsquedas, etc.
+ // La clase hija podría sobrescribir este método o llamarlo desde su propia lógica.
+ $query = $this->model()::query();
+
+ // Por ejemplo:
+ /*
+ if (!empty($criteria['store_id'])) {
+ $query->where('store_id', $criteria['store_id']);
+ }
+ */
+
+ return $query;
+ }
+}
diff --git a/Livewire/Users/UserCount.php b/Livewire/Users/UserCount.php
new file mode 100644
index 0000000..0e3eb6b
--- /dev/null
+++ b/Livewire/Users/UserCount.php
@@ -0,0 +1,31 @@
+ 'updateCounts'];
+
+ public function mount()
+ {
+ $this->updateCounts();
+ }
+
+ public function updateCounts()
+ {
+ $this->total = User::count();
+ $this->enabled = User::where('status', User::STATUS_ENABLED)->count();
+ $this->disabled = User::where('status', User::STATUS_DISABLED)->count();
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.users.count');
+ }
+}
diff --git a/Livewire/Users/UserForm.php b/Livewire/Users/UserForm.php
new file mode 100644
index 0000000..91f9ca1
--- /dev/null
+++ b/Livewire/Users/UserForm.php
@@ -0,0 +1,306 @@
+id ?? null);
+ }
+
+ /**
+ * Cargar opciones de formularios según el modo actual.
+ *
+ * @param string $mode
+ */
+ private function loadOptions(string $mode): void
+ {
+ $this->manager_id_options = User::getUsersListWithInactive($this->manager_id, ['type' => 'user', 'status' => 1]);
+ $this->c_regimen_fiscal_options = RegimenFiscal::selectList();
+ $this->c_pais_options = Pais::selectList();
+ $this->c_estado_options = Estado::selectList($this->c_pais)->toArray();
+
+ if ($mode !== 'create') {
+ $this->c_localidad_options = Localidad::selectList($this->c_estado)->toArray();
+ $this->c_municipio_options = Municipio::selectList($this->c_estado, $this->c_municipio)->toArray();
+ $this->c_colonia_options = Colonia::selectList($this->c_codigo_postal, $this->c_colonia)->toArray();
+ }
+ }
+
+ // ===================== MÉTODOS OBLIGATORIOS =====================
+
+ /**
+ * Devuelve el modelo Eloquent asociado.
+ *
+ * @return string
+ */
+ protected function model(): string
+ {
+ return Store::class;
+ }
+
+ /**
+ * Reglas de validación dinámicas según el modo actual.
+ *
+ * @param string $mode
+ * @return array
+ */
+ protected function dynamicRules(string $mode): array
+ {
+ switch ($mode) {
+ case 'create':
+ case 'edit':
+ return [
+ 'code' => [
+ 'required', 'string', 'alpha_num', 'max:16',
+ Rule::unique('stores', 'code')->ignore($this->id)
+ ],
+ 'name' => 'required|string|max:96',
+ 'description' => 'nullable|string|max:1024',
+ 'manager_id' => 'nullable|exists:users,id',
+
+ // Información fiscal
+ 'rfc' => ['nullable', 'string', 'regex:/^([A-ZÑ&]{3,4})(\d{6})([A-Z\d]{3})$/i', 'max:13'],
+ 'nombre_fiscal' => 'nullable|string|max:255',
+ 'c_regimen_fiscal' => 'nullable|exists:sat_regimen_fiscal,c_regimen_fiscal',
+ 'domicilio_fiscal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal',
+
+ // Ubicación
+ 'c_pais' => 'nullable|exists:sat_pais,c_pais|string|size:3',
+ 'c_estado' => 'nullable|exists:sat_estado,c_estado|string|min:2|max:3',
+ 'c_municipio' => 'nullable|exists:sat_municipio,c_municipio|integer',
+ 'c_localidad' => 'nullable|integer',
+ 'c_codigo_postal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal|integer',
+ 'c_colonia' => 'nullable|exists:sat_colonia,c_colonia|integer',
+ 'direccion' => 'nullable|string|max:255',
+ 'num_ext' => 'nullable|string|max:50',
+ 'num_int' => 'nullable|string|max:50',
+ 'lat' => 'nullable|numeric|between:-90,90',
+ 'lng' => 'nullable|numeric|between:-180,180',
+
+ // Contacto
+ 'email' => ['nullable', 'email', 'required_if:enable_ecommerce,true'],
+ 'tel' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'],
+ 'tel2' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'],
+
+ // Configuración web y estado
+ 'show_on_website' => 'nullable|boolean',
+ 'enable_ecommerce' => 'nullable|boolean',
+ 'status' => 'nullable|boolean',
+ ];
+
+ case 'delete':
+ return [
+ 'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación
+ ];
+
+ default:
+ return [];
+ }
+ }
+
+ /**
+ * Inicializa los datos del formulario en función del modo.
+ *
+ * @param Store|null $store
+ * @param string $mode
+ */
+ protected function initializeFormData(mixed $store, string $mode): void
+ {
+ if ($store) {
+ $this->code = $store->code;
+ $this->name = $store->name;
+ $this->description = $store->description;
+ $this->manager_id = $store->manager_id;
+ $this->rfc = $store->rfc;
+ $this->nombre_fiscal = $store->nombre_fiscal;
+ $this->c_regimen_fiscal = $store->c_regimen_fiscal;
+ $this->domicilio_fiscal = $store->domicilio_fiscal;
+ $this->c_pais = $store->c_pais;
+ $this->c_estado = $store->c_estado;
+ $this->c_municipio = $store->c_municipio;
+ $this->c_localidad = $store->c_localidad;
+ $this->c_codigo_postal = $store->c_codigo_postal;
+ $this->c_colonia = $store->c_colonia;
+ $this->direccion = $store->direccion;
+ $this->num_ext = $store->num_ext;
+ $this->num_int = $store->num_int;
+ $this->lat = $store->lat;
+ $this->lng = $store->lng;
+ $this->email = $store->email;
+ $this->tel = $store->tel;
+ $this->tel2 = $store->tel2;
+ $this->show_on_website = (bool) $store->show_on_website;
+ $this->enable_ecommerce = (bool) $store->enable_ecommerce;
+ $this->status = (bool) $store->status;
+
+ } else {
+ $this->c_pais = 'MEX';
+ $this->status = true;
+ $this->show_on_website = false;
+ $this->enable_ecommerce = false;
+ }
+
+ $this->loadOptions($mode);
+ }
+
+ /**
+ * Prepara los datos validados para su almacenamiento.
+ *
+ * @param array $validatedData
+ * @return array
+ */
+ protected function prepareData(array $validatedData): array
+ {
+ return [
+ 'code' => $validatedData['code'],
+ 'name' => $validatedData['name'],
+ 'description' => strip_tags($validatedData['description']),
+ 'manager_id' => $validatedData['manager_id'],
+ 'rfc' => $validatedData['rfc'],
+ 'nombre_fiscal' => $validatedData['nombre_fiscal'],
+ 'c_regimen_fiscal' => $validatedData['c_regimen_fiscal'],
+ 'domicilio_fiscal' => $validatedData['domicilio_fiscal'],
+ 'c_codigo_postal' => $validatedData['c_codigo_postal'],
+ 'c_pais' => $validatedData['c_pais'],
+ 'c_estado' => $validatedData['c_estado'],
+ 'c_localidad' => $validatedData['c_localidad'],
+ 'c_municipio' => $validatedData['c_municipio'],
+ 'c_colonia' => $validatedData['c_colonia'],
+ 'direccion' => $validatedData['direccion'],
+ 'num_ext' => $validatedData['num_ext'],
+ 'num_int' => $validatedData['num_int'],
+ 'email' => $validatedData['email'],
+ 'tel' => $validatedData['tel'],
+ 'tel2' => $validatedData['tel2'],
+ 'lat' => $validatedData['lat'],
+ 'lng' => $validatedData['lng'],
+ 'status' => $validatedData['status'],
+ 'show_on_website' => $validatedData['show_on_website'],
+ 'enable_ecommerce' => $validatedData['enable_ecommerce'],
+ ];
+ }
+
+ /**
+ * Definición de los contenedores de notificación.
+ *
+ * @return array
+ */
+ protected function targetNotifies(): array
+ {
+ return [
+ "index" => "#bt-stores .notification-container",
+ "form" => "#store-form .notification-container",
+ ];
+ }
+
+ /**
+ * Ruta de vista asociada al formulario.
+ *
+ * @return \Illuminate\Contracts\View\View
+ */
+ protected function viewPath(): string
+ {
+ return 'vuexy-store-manager::livewire.stores.form';
+ }
+
+ // ===================== VALIDACIONES =====================
+
+ /**
+ * Get custom attributes for validator errors.
+ *
+ * @return array
+ */
+ public function attributes(): array
+ {
+ return [
+ 'code' => 'código de sucursal',
+ 'name' => 'nombre de la sucursal',
+ ];
+ }
+
+ /**
+ * Get the error messages for the defined validation rules.
+ *
+ * @return array
+ */
+ public function messages(): array
+ {
+ return [
+ 'code.required' => 'El código de la sucursal es obligatorio.',
+ 'code.unique' => 'Este código ya está en uso por otra sucursal.',
+ 'name.required' => 'El nombre de la sucursal es obligatorio.',
+ ];
+ }
+
+ // ===================== PREPARACIÓN DE DATOS =====================
+
+ // ===================== NOTIFICACIONES Y EVENTOS =====================
+
+ /**
+ * Definición de los eventos del componente.
+ *
+ * @return array
+ */
+ protected function dispatches(): array
+ {
+ return [
+ 'on-failed-validation' => 'on-failed-validation-store',
+ 'on-hydrate' => 'on-hydrate-store-modal',
+ ];
+ }
+
+ // ===================== REDIRECCIÓN =====================
+
+ /**
+ * Define la ruta de redirección tras guardar o eliminar.
+ *
+ * @return string
+ */
+ protected function getRedirectRoute(): string
+ {
+ return 'admin.core.user.index';
+ }
+
+}
diff --git a/Livewire/Users/UserIndex.copy.php b/Livewire/Users/UserIndex.copy.php
new file mode 100644
index 0000000..9eb2963
--- /dev/null
+++ b/Livewire/Users/UserIndex.copy.php
@@ -0,0 +1,115 @@
+modalTitle = 'Crear usuario nuevo';
+ $this->btnSubmitTxt = 'Crear usuario';
+
+ $this->statuses = [
+ User::STATUS_ENABLED => ['title' => 'Activo', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_ENABLED]],
+ User::STATUS_DISABLED => ['title' => 'Deshabilitado', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_DISABLED]],
+ User::STATUS_REMOVED => ['title' => 'Eliminado', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_REMOVED]],
+ ];
+
+ $roles = Role::whereNotIn('name', ['Patient', 'Doctor'])->get();
+
+ $this->roles_html_select = " Selecciona un rol ";
+
+ foreach ($roles as $role) {
+ $this->rows_roles[$role->name] = "style . " mx-1\">" . $role->name . " ";
+
+ if (Auth::user()->hasRole('SuperAdmin') || $role->name != 'SuperAdmin') {
+ $this->roles_html_select .= "name . "\" class=\"text-capitalize\">" . $role->name . " ";
+ $this->roles_options[$role->name] = $role->name;
+ }
+ }
+
+ $this->roles_html_select .= " ";
+
+ $this->status_options = [
+ User::STATUS_ENABLED => User::$statusList[User::STATUS_ENABLED],
+ User::STATUS_DISABLED => User::$statusList[User::STATUS_DISABLED],
+ ];
+ }
+
+ public function countUsers()
+ {
+ $this->total = User::count();
+ $this->enabled = User::where('status', User::STATUS_ENABLED)->count();
+ $this->disabled = User::where('status', User::STATUS_DISABLED)->count();
+ }
+
+
+ public function edit($id)
+ {
+ $user = User::findOrFail($id);
+
+ $this->indexAlert = '';
+ $this->modalTitle = 'Editar usuario: ' . $id;
+ $this->btnSubmitTxt = 'Guardar cambios';
+
+ $this->userId = $user->id;
+ $this->name = $user->name;
+ $this->email = $user->email;
+ $this->password = '';
+ $this->roles = $user->roles->pluck('name')->toArray();
+ $this->src_photo = $user->profile_photo_url;
+ $this->status = $user->status;
+
+ $this->dispatch('openModal');
+ }
+
+ public function delete($id)
+ {
+ $user = User::find($id);
+
+ if ($user) {
+ // Eliminar la imagen de perfil si existe
+ if ($user->profile_photo_path)
+ Storage::disk('public')->delete($user->profile_photo_path);
+
+ // Eliminar el usuario
+ $user->delete();
+
+ $this->indexAlert = 'Se eliminó correctamente el usuario.
';
+
+ $this->dispatch('refreshUserCount');
+ $this->dispatch('afterDelete');
+ } else {
+ $this->indexAlert = 'Usuario no encontrado.
';
+ }
+ }
+
+ public function render()
+ {
+ return view('vuexy-admin::livewire.users.index', [
+ 'users' => User::paginate(10),
+ ]);
+ }
+}
diff --git a/Livewire/Users/UserIndex.php b/Livewire/Users/UserIndex.php
new file mode 100644
index 0000000..49e0e3f
--- /dev/null
+++ b/Livewire/Users/UserIndex.php
@@ -0,0 +1,299 @@
+ 'Acciones',
+ 'code' => 'Código personal',
+ 'full_name' => 'Nombre Completo',
+ 'email' => 'Correo Electrónico',
+ 'parent_name' => 'Responsable',
+ 'parent_email' => 'Correo Responsable',
+ 'company' => 'Empresa',
+ 'birth_date' => 'Fecha de Nacimiento',
+ 'hire_date' => 'Fecha de Contratación',
+ 'curp' => 'CURP',
+ 'nss' => 'NSS',
+ 'job_title' => 'Puesto',
+ 'rfc' => 'RFC',
+ 'nombre_fiscal' => 'Nombre Fiscal',
+ 'profile_photo_path' => 'Foto de Perfil',
+ 'is_partner' => 'Socio',
+ 'is_employee' => 'Empleado',
+ 'is_prospect' => 'Prospecto',
+ 'is_customer' => 'Cliente',
+ 'is_provider' => 'Proveedor',
+ 'is_user' => 'Usuario',
+ 'status' => 'Estatus',
+ 'creator' => 'Creado Por',
+ 'creator_email' => 'Correo Creador',
+ 'created_at' => 'Fecha de Creación',
+ 'updated_at' => 'Última Modificación',
+ ];
+ }
+
+ /**
+ * Retorna el formato (formatter) para cada columna.
+ */
+ protected function format(): array
+ {
+ return [
+ 'action' => [
+ 'formatter' => 'userActionFormatter',
+ 'onlyFormatter' => true,
+ ],
+ 'code' => [
+ 'formatter' => [
+ 'name' => 'dynamicBadgeFormatter',
+ 'params' => ['color' => 'secondary'],
+ ],
+ 'align' => 'center',
+ 'switchable' => false,
+ ],
+ 'full_name' => [
+ 'formatter' => 'userProfileFormatter',
+ ],
+ 'email' => [
+ 'formatter' => 'emailFormatter',
+ 'visible' => false,
+ ],
+ 'parent_name' => [
+ 'formatter' => 'contactParentFormatter',
+ 'visible' => false,
+ ],
+ 'agent_name' => [
+ 'formatter' => 'agentFormatter',
+ 'visible' => false,
+ ],
+ 'company' => [
+ 'formatter' => 'textNowrapFormatter',
+ ],
+ 'curp' => [
+ 'visible' => false,
+ ],
+ 'nss' => [
+ 'visible' => false,
+ ],
+ 'job_title' => [
+ 'formatter' => 'textNowrapFormatter',
+ 'visible' => false,
+ ],
+ 'rfc' => [
+ 'visible' => false,
+ ],
+ 'nombre_fiscal' => [
+ 'formatter' => 'textNowrapFormatter',
+ 'visible' => false,
+ ],
+ 'domicilio_fiscal' => [
+ 'visible' => false,
+ ],
+ 'c_uso_cfdi' => [
+ 'formatter' => 'usoCfdiFormatter',
+ 'visible' => false,
+ ],
+ 'tipo_persona' => [
+ 'formatter' => 'dynamicBadgeFormatter',
+ 'align' => 'center',
+ 'visible' => false,
+ ],
+ 'c_regimen_fiscal' => [
+ 'formatter' => 'regimenFiscalFormatter',
+ 'visible' => false,
+ ],
+ 'birth_date' => [
+ 'align' => 'center',
+ 'visible' => false,
+ ],
+ 'hire_date' => [
+ 'align' => 'center',
+ 'visible' => false,
+ ],
+ 'estado' => [
+ 'formatter' => 'textNowrapFormatter',
+ ],
+ 'municipio' => [
+ 'formatter' => 'textNowrapFormatter',
+ ],
+ 'localidad' => [
+ 'formatter' => 'textNowrapFormatter',
+ 'visible' => false,
+ ],
+ 'is_partner' => [
+ 'formatter' => [
+ 'name' => 'dynamicBooleanFormatter',
+ 'params' => ['tag' => 'checkSI'],
+ ],
+ 'align' => 'center',
+ ],
+ 'is_employee' => [
+ 'formatter' => [
+ 'name' => 'dynamicBooleanFormatter',
+ 'params' => ['tag' => 'checkSI'],
+ ],
+ 'align' => 'center',
+ ],
+ 'is_prospect' => [
+ 'formatter' => [
+ 'name' => 'dynamicBooleanFormatter',
+ 'params' => ['tag' => 'checkSI'],
+ ],
+ 'align' => 'center',
+ ],
+ 'is_customer' => [
+ 'formatter' => [
+ 'name' => 'dynamicBooleanFormatter',
+ 'params' => ['tag' => 'checkSI'],
+ ],
+ 'align' => 'center',
+ ],
+ 'is_provider' => [
+ 'formatter' => [
+ 'name' => 'dynamicBooleanFormatter',
+ 'params' => ['tag' => 'checkSI'],
+ ],
+ 'align' => 'center',
+ ],
+ 'is_user' => [
+ 'formatter' => [
+ 'name' => 'dynamicBooleanFormatter',
+ 'params' => ['tag' => 'checkSI'],
+ ],
+ 'align' => 'center',
+ ],
+ 'status' => [
+ 'formatter' => 'statusIntBadgeBgFormatter',
+ 'align' => 'center',
+ ],
+ 'creator' => [
+ 'formatter' => 'creatorFormatter',
+ 'visible' => false,
+ ],
+ 'created_at' => [
+ 'formatter' => 'textNowrapFormatter',
+ 'align' => 'center',
+ 'visible' => false,
+ ],
+ 'updated_at' => [
+ 'formatter' => 'textNowrapFormatter',
+ 'align' => 'center',
+ 'visible' => false,
+ ],
+ ];
+ }
+
+ /**
+ * Procesa el documento recibido (CFDI XML o Constancia PDF).
+ */
+ public function processDocument()
+ {
+ // Verificamos si el archivo es válido
+ if (!$this->doc_file instanceof UploadedFile) {
+ return $this->addError('doc_file', 'No se pudo recibir el archivo.');
+ }
+
+ try {
+ // Validar tipo de archivo
+ $this->validate([
+ 'doc_file' => 'required|mimes:pdf,xml|max:2048'
+ ]);
+
+
+ // **Detectar el tipo de documento**
+ $extension = strtolower($this->doc_file->getClientOriginalExtension());
+
+ // **Procesar según el tipo de archivo**
+ switch ($extension) {
+ case 'xml':
+ $service = new FacturaXmlService();
+ $data = $service->processUploadedFile($this->doc_file);
+ break;
+
+ case 'pdf':
+ $service = new ConstanciaFiscalService();
+ $data = $service->extractData($this->doc_file);
+ break;
+
+ default:
+ throw new Exception("Formato de archivo no soportado.");
+ }
+
+ dd($data);
+
+ // **Asignar los valores extraídos al formulario**
+ $this->rfc = $data['rfc'] ?? null;
+ $this->name = $data['name'] ?? null;
+ $this->email = $data['email'] ?? null;
+ $this->tel = $data['telefono'] ?? null;
+ //$this->direccion = $data['domicilio_fiscal'] ?? null;
+
+ // Ocultar el Dropzone después de procesar
+ $this->dropzoneVisible = false;
+
+ } catch (ValidationException $e) {
+ $this->handleValidationException($e);
+
+ } catch (QueryException $e) {
+ $this->handleDatabaseException($e);
+
+ } catch (ModelNotFoundException $e) {
+ $this->handleException('danger', 'Registro no encontrado.');
+
+ } catch (Exception $e) {
+ $this->handleException('danger', 'Error al procesar el archivo: ' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * Montamos el componente y llamamos al parent::mount() para configurar la tabla.
+ */
+ public function mount(): void
+ {
+ parent::mount();
+
+ // Definimos las rutas específicas de este componente
+ $this->routes = [
+ 'admin.user.show' => route('admin.core.users.show', ['user' => ':id']),
+ 'admin.user.edit' => route('admin.core.users.edit', ['user' => ':id']),
+ 'admin.user.delete' => route('admin.core.users.delete', ['user' => ':id']),
+ ];
+ }
+
+ /**
+ * Retorna la vista a renderizar por este componente.
+ */
+ protected function viewPath(): string
+ {
+ return 'vuexy-admin::livewire.users.index';
+ }
+}
diff --git a/Livewire/Users/UserOffCanvasForm.php b/Livewire/Users/UserOffCanvasForm.php
new file mode 100644
index 0000000..d65c74d
--- /dev/null
+++ b/Livewire/Users/UserOffCanvasForm.php
@@ -0,0 +1,295 @@
+ 'loadFormModel',
+ 'confirmDeletionUsers' => 'loadFormModelForDeletion',
+ ];
+
+ /**
+ * Definición de tipos de datos que se deben castear.
+ *
+ * @var array
+ */
+ protected $casts = [
+ 'status' => 'boolean',
+ ];
+
+ /**
+ * Define el modelo Eloquent asociado con el formulario.
+ *
+ * @return string
+ */
+ protected function model(): string
+ {
+ return User::class;
+ }
+
+ /**
+ * Define los campos del formulario.
+ *
+ * @return array
+ */
+ protected function fields(): array
+ {
+ return (new User())->getFillable();
+ }
+
+ /**
+ * Valores por defecto para el formulario.
+ *
+ * @return array
+ */
+ protected function defaults(): array
+ {
+ return [
+ //
+ ];
+ }
+
+ /**
+ * Campo que se debe enfocar cuando se abra el formulario.
+ *
+ * @return string
+ */
+ protected function focusOnOpen(): string
+ {
+ return 'name';
+ }
+
+ /**
+ * Define reglas de validación dinámicas basadas en el modo actual.
+ *
+ * @param string $mode El modo actual del formulario ('create', 'edit', 'delete').
+ * @return array
+ */
+ protected function dynamicRules(string $mode): array
+ {
+ switch ($mode) {
+ case 'create':
+ case 'edit':
+ return [
+ 'code' => ['required', 'string', 'max:16', Rule::unique('contact', 'code')->ignore($this->id)],
+ 'name' => ['required', 'string', 'max:96'],
+ 'notes' => ['nullable', 'string', 'max:1024'],
+ 'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'],
+ ];
+
+ case 'delete':
+ return [
+ 'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación
+ ];
+
+ default:
+ return [];
+ }
+ }
+
+ // ===================== VALIDACIONES =====================
+
+ /**
+ * Get custom attributes for validator errors.
+ *
+ * @return array
+ */
+ protected function attributes(): array
+ {
+ return [
+ 'code' => 'código de usuario',
+ 'name' => 'nombre del usuario',
+ ];
+ }
+
+ /**
+ * Get the error messages for the defined validation rules.
+ *
+ * @return array
+ */
+ protected function messages(): array
+ {
+ return [
+ 'code.unique' => 'Este código ya está en uso por otro usuario.',
+ 'name.required' => 'El nombre del usuario es obligatorio.',
+ ];
+ }
+
+ /**
+ * Carga el formulario con datos del usuario y actualiza las opciones dinámicas.
+ *
+ * @param int $id
+ */
+ public function loadFormModel($id): void
+ {
+ parent::loadFormModel($id);
+
+ $this->work_center_options = $this->store_id
+ ? DB::table('store_work_centers')
+ ->where('store_id', $this->store_id)
+ ->pluck('name', 'id')
+ ->toArray()
+ : [];
+ }
+
+ /**
+ * Carga el formulario para eliminar un usuario, actualizando las opciones necesarias.
+ *
+ * @param int $id
+ */
+ public function loadFormModelForDeletion($id): void
+ {
+ parent::loadFormModelForDeletion($id);
+
+ $this->work_center_options = DB::table('store_work_centers')
+ ->where('store_id', $this->store_id)
+ ->pluck('name', 'id')
+ ->toArray();
+ }
+
+ /**
+ * Define las opciones de los selectores desplegables.
+ *
+ * @return array
+ */
+ protected function options(): array
+ {
+ $storeCatalogService = app(StoreCatalogService::class);
+ $contactCatalogService = app(ContactCatalogService::class);
+
+ return [
+ 'store_options' => $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]),
+ 'manager_options' => $contactCatalogService->searchCatalog('users', '', ['limit' => -1]),
+ ];
+ }
+
+ /**
+ * Procesa el documento recibido (CFDI XML o Constancia PDF).
+ */
+ public function processDocument()
+ {
+ // Verificamos si el archivo es válido
+ if (!$this->doc_file instanceof UploadedFile) {
+ return $this->addError('doc_file', 'No se pudo recibir el archivo.');
+ }
+
+ try {
+ // Validar tipo de archivo
+ $this->validate([
+ 'doc_file' => 'required|mimes:pdf,xml|max:2048'
+ ]);
+
+
+ // **Detectar el tipo de documento**
+ $extension = strtolower($this->doc_file->getClientOriginalExtension());
+
+ // **Procesar según el tipo de archivo**
+ switch ($extension) {
+ case 'xml':
+ $service = new FacturaXmlService();
+ $data = $service->processUploadedFile($this->doc_file);
+ break;
+
+ case 'pdf':
+ $service = new ConstanciaFiscalService();
+ $data = $service->extractData($this->doc_file);
+ break;
+
+ default:
+ throw new Exception("Formato de archivo no soportado.");
+ }
+
+ dd($data);
+
+ // **Asignar los valores extraídos al formulario**
+ $this->rfc = $data['rfc'] ?? null;
+ $this->name = $data['name'] ?? null;
+ $this->email = $data['email'] ?? null;
+ $this->tel = $data['telefono'] ?? null;
+ //$this->direccion = $data['domicilio_fiscal'] ?? null;
+
+ // Ocultar el Dropzone después de procesar
+ $this->dropzoneVisible = false;
+
+ } catch (ValidationException $e) {
+ $this->handleValidationException($e);
+
+ } catch (QueryException $e) {
+ $this->handleDatabaseException($e);
+
+ } catch (ModelNotFoundException $e) {
+ $this->handleException('danger', 'Registro no encontrado.');
+
+ } catch (Exception $e) {
+ $this->handleException('danger', 'Error al procesar el archivo: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * Ruta de la vista asociada con este formulario.
+ *
+ * @return string
+ */
+ protected function viewPath(): string
+ {
+ return 'vuexy-admin::livewire.users.offcanvas-form';
+ }
+}
diff --git a/Livewire/Users/UserShow.php b/Livewire/Users/UserShow.php
new file mode 100644
index 0000000..de9cd95
--- /dev/null
+++ b/Livewire/Users/UserShow.php
@@ -0,0 +1,283 @@
+ 'nullable|integer',
+ 'name' => 'required|string|min:3|max:255',
+ 'cargo' => 'nullable|string|min:3|max:255',
+ 'is_prospect' => 'nullable|boolean',
+ 'is_customer' => 'nullable|boolean',
+ 'is_provider' => 'nullable|boolean',
+ 'is_user' => 'nullable|boolean',
+ 'pricelist_id' => 'nullable|integer',
+ 'enable_credit' => 'nullable|boolean',
+ 'credit_days' => 'nullable|integer',
+ 'credit_limit' => 'nullable|numeric|min:0|max:9999999.99|regex:/^\d{1,7}(\.\d{1,2})?$/',
+ 'image' => 'nullable|mimes:jpg,png|image|max:20480', // 20MB Max
+ ];
+
+ // Reglas de validación para los campos fiscales
+ protected $rulesFacturacion = [
+ 'rfc' => 'nullable|string|max:13',
+ 'domicilio_fiscal' => [
+ 'nullable',
+ 'regex:/^[0-9]{5}$/',
+ 'exists:sat_codigo_postal,c_codigo_postal'
+ ],
+ 'nombre_fiscal' => 'nullable|string|max:255',
+ 'c_regimen_fiscal' => 'nullable|integer',
+ 'c_uso_cfdi' => 'nullable|string',
+ ];
+
+ public function mount($userId)
+ {
+ $this->user = User::findOrFail($userId);
+
+ $this->reloadUserData();
+
+ $this->pricelists_options = DropdownList::selectList(DropdownList::POS_PRICELIST);
+
+ $this->status_options = [
+ User::STATUS_ENABLED => User::$statusList[User::STATUS_ENABLED],
+ User::STATUS_DISABLED => User::$statusList[User::STATUS_DISABLED],
+ ];
+
+ $this->regimen_fiscal_options = RegimenFiscal::selectList();
+ $this->uso_cfdi_options = UsoCfdi::selectList();
+ }
+
+
+ public function reloadUserData()
+ {
+ $this->tipo_persona = $this->user->tipo_persona;
+ $this->name = $this->user->name;
+ $this->cargo = $this->user->cargo;
+ $this->is_prospect = $this->user->is_prospect? true : false;
+ $this->is_customer = $this->user->is_customer? true : false;
+ $this->is_provider = $this->user->is_provider? true : false;
+ $this->is_user = $this->user->is_user? true : false;
+ $this->pricelist_id = $this->user->pricelist_id;
+ $this->enable_credit = $this->user->enable_credit? true : false;
+ $this->credit_days = $this->user->credit_days;
+ $this->credit_limit = $this->user->credit_limit;
+ $this->profile_photo = $this->user->profile_photo_url;
+ $this->profile_photo_path = $this->user->profile_photo_path;
+ $this->image = null;
+ $this->deleteUserImage = false;
+
+ $this->status = $this->user->status;
+ $this->email = $this->user->email;
+ $this->password = null;
+ $this->password_confirmation = null;
+
+ $this->rfc = $this->user->rfc;
+ $this->domicilio_fiscal = $this->user->domicilio_fiscal;
+ $this->nombre_fiscal = $this->user->nombre_fiscal;
+ $this->c_regimen_fiscal = $this->user->c_regimen_fiscal;
+ $this->c_uso_cfdi = $this->user->c_uso_cfdi;
+
+ $this->cuentaUsuarioAlert = null;
+ $this->accesosAlert = null;
+ $this->facturacionElectronicaAlert = null;
+ }
+
+
+ public function saveCuentaUsuario()
+ {
+ try {
+ // Validar Información de usuario
+ $validatedData = $this->validate($this->rulesUser);
+
+ $validatedData['name'] = trim($validatedData['name']);
+ $validatedData['cargo'] = $validatedData['cargo']? trim($validatedData['cargo']): null;
+ $validatedData['is_prospect'] = $validatedData['is_prospect'] ? 1 : 0;
+ $validatedData['is_customer'] = $validatedData['is_customer'] ? 1 : 0;
+ $validatedData['is_provider'] = $validatedData['is_provider'] ? 1 : 0;
+ $validatedData['is_user'] = $validatedData['is_user'] ? 1 : 0;
+ $validatedData['pricelist_id'] = $validatedData['pricelist_id'] ?: null;
+ $validatedData['enable_credit'] = $validatedData['enable_credit'] ? 1 : 0;
+ $validatedData['credit_days'] = $validatedData['credit_days'] ?: null;
+ $validatedData['credit_limit'] = $validatedData['credit_limit'] ?: null;
+
+ if($this->tipo_persona == User::TIPO_RFC_PUBLICO){
+ $validatedData['cargo'] = null;
+ $validatedData['is_prospect'] = null;
+ $validatedData['is_provider'] = null;
+ $validatedData['is_user'] = null;
+ $validatedData['enable_credit'] = null;
+ $validatedData['credit_days'] = null;
+ $validatedData['credit_limit'] = null;
+ }
+
+ if(!$this->user->is_prospect && !$this->user->is_customer){
+ $validatedData['pricelist_id'] = null;
+ }
+
+ if(!$this->user->is_customer){
+ $validatedData['enable_credit'] = null;
+ $validatedData['credit_days'] = null;
+ $validatedData['credit_limit'] = null;
+ }
+
+ $this->user->update($validatedData);
+
+
+ if($this->deleteUserImage && $this->user->profile_photo_path){
+ $this->user->deleteProfilePhoto();
+
+ // Reiniciar variables después de la eliminación
+ $this->deleteUserImage = false;
+ $this->profile_photo_path = null;
+ $this->profile_photo = $this->user->profile_photo_url;
+
+ }else if ($this->image) {
+ $image = ImageManager::imagick()->read($this->image->getRealPath());
+ $image = $image->scale(520, 520);
+
+ $imageName = $this->image->hashName(); // Genera un nombre único
+
+ $image->save(storage_path('app/public/profile-photos/' . $imageName));
+
+ $this->user->deleteProfilePhoto();
+
+ $this->profile_photo_path = $this->user->profile_photo_path = 'profile-photos/' . $imageName;
+ $this->profile_photo = $this->user->profile_photo_url;
+ $this->user->save();
+
+ unlink($this->image->getRealPath());
+
+ $this->reset('image');
+ }
+
+ // Puedes también devolver un mensaje de éxito si lo deseas
+ $this->setAlert('Se guardó los cambios exitosamente.', 'cuentaUsuarioAlert');
+
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ // Si hay errores de validación, los puedes capturar y manejar aquí
+ $this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'cuentaUsuarioAlert', 'danger');
+ }
+ }
+
+ public function saveAccesos()
+ {
+ try {
+ $validatedData = $this->validate([
+ 'status' => 'integer',
+ 'email' => ['required', 'email', 'unique:users,email,' . $this->user->id],
+ 'password' => ['nullable', 'string', 'min:6', 'max:32', 'confirmed'], // La regla 'confirmed' valida que ambas contraseñas coincidan
+ ], [
+ 'email.required' => 'El correo electrónico es obligatorio.',
+ 'email.email' => 'Debes ingresar un correo electrónico válido.',
+ 'email.unique' => 'Este correo ya está en uso.',
+ 'password.min' => 'La contraseña debe tener al menos 5 caracteres.',
+ 'password.max' => 'La contraseña no puede tener más de 32 caracteres.',
+ 'password.confirmed' => 'Las contraseñas no coinciden.',
+ ]);
+
+ // Si la validación es exitosa, continuar con el procesamiento
+ $validatedData['email'] = trim($this->email);
+
+ if ($this->password)
+ $validatedData['password'] = bcrypt($this->password);
+
+ else
+ unset($validatedData['password']);
+
+ $this->user->update($validatedData);
+
+ $this->password = null;
+ $this->password_confirmation = null;
+
+ // Puedes también devolver un mensaje de éxito si lo deseas
+ $this->setAlert('Se guardó los cambios exitosamente.', 'accesosAlert');
+
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ // Si hay errores de validación, los puedes capturar y manejar aquí
+ $this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'accesosAlert', 'danger');
+ }
+ }
+
+ public function saveFacturacionElectronica()
+ {
+ try {
+ // Validar Información fiscal
+ $validatedData = $this->validate($this->rulesFacturacion);
+
+ $validatedData['rfc'] = strtoupper(trim($validatedData['rfc'])) ?: null;
+ $validatedData['domicilio_fiscal'] = $validatedData['domicilio_fiscal'] ?: null;
+ $validatedData['nombre_fiscal'] = strtoupper(trim($validatedData['nombre_fiscal'])) ?: null;
+ $validatedData['c_regimen_fiscal'] = $validatedData['c_regimen_fiscal'] ?: null;
+ $validatedData['c_uso_cfdi'] = $validatedData['c_uso_cfdi'] ?: null;
+
+ $this->user->update($validatedData);
+
+ // Puedes también devolver un mensaje de éxito si lo deseas
+ $this->setAlert('Se guardó los cambios exitosamente.', 'facturacionElectronicaAlert');
+
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ // Si hay errores de validación, los puedes capturar y manejar aquí
+ $this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'facturacionElectronicaAlert', 'danger');
+ }
+ }
+
+
+ private function setAlert($message, $alertName, $type = 'success')
+ {
+ $this->$alertName = [
+ 'message' => $message,
+ 'type' => $type
+ ];
+ }
+
+ public function render()
+ {
+ return view('livewire.admin.crm.contact-view');
+ }
+}
diff --git a/Models/MediaItem.php b/Models/MediaItem.php
new file mode 100644
index 0000000..ad44908
--- /dev/null
+++ b/Models/MediaItem.php
@@ -0,0 +1,62 @@
+ 'Card',
+ self::TYPE_BANNER => 'Banner',
+ self::TYPE_COVER => 'Cover',
+ self::TYPE_GALLERY => 'Gallery',
+ self::TYPE_BANNER_HOME => 'Banner Home',
+ self::TYPE_CARD2 => 'Card 2',
+ self::TYPE_BANNER2 => 'Banner 2',
+ self::TYPE_COVER2 => 'Cover 2',
+ ];
+
+ /**
+ * Get the parent imageable model (user or post).
+ */
+ public function imageable()
+ {
+ return $this->morphTo();
+ }
+}
diff --git a/Models/Setting.php b/Models/Setting.php
new file mode 100644
index 0000000..7300adf
--- /dev/null
+++ b/Models/Setting.php
@@ -0,0 +1,39 @@
+
+ */
+ protected $fillable = [
+ 'key',
+ 'value',
+ 'user_id',
+ ];
+
+ public $timestamps = false;
+
+ // Relación con el usuario
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+
+ // Scope para obtener configuraciones de un usuario específico
+ public function scopeForUser($query, $userId)
+ {
+ return $query->where('user_id', $userId);
+ }
+
+ // Configuraciones globales (sin usuario)
+ public function scopeGlobal($query)
+ {
+ return $query->whereNull('user_id');
+ }
+}
diff --git a/Models/User copy.php b/Models/User copy.php
new file mode 100644
index 0000000..cb20a95
--- /dev/null
+++ b/Models/User copy.php
@@ -0,0 +1,377 @@
+ 'Habilitado',
+ self::STATUS_DISABLED => 'Deshabilitado',
+ self::STATUS_REMOVED => 'Eliminado',
+ ];
+
+ /**
+ * List of names for each status.
+ * @var array
+ */
+ public static $statusListClass = [
+ self::STATUS_ENABLED => 'success',
+ self::STATUS_DISABLED => 'warning',
+ self::STATUS_REMOVED => 'danger',
+ ];
+
+ /**
+ * The attributes that are mass assignable.
+ *
+ * @var array
+ */
+ protected $fillable = [
+ 'name',
+ 'last_name',
+ 'email',
+ 'password',
+ 'profile_photo_path',
+ 'status',
+ 'created_by',
+ ];
+
+ /**
+ * The attributes that should be hidden for serialization.
+ *
+ * @var array
+ */
+ protected $hidden = [
+ 'password',
+ 'remember_token',
+ 'two_factor_recovery_codes',
+ 'two_factor_secret',
+ ];
+
+ /**
+ * The accessors to append to the model's array form.
+ *
+ * @var array
+ */
+ protected $appends = [
+ 'profile_photo_url',
+ ];
+
+ /**
+ * Get the attributes that should be cast.
+ *
+ * @return array
+ */
+ protected function casts(): array
+ {
+ return [
+ 'email_verified_at' => 'datetime',
+ 'password' => 'hashed',
+ ];
+ }
+
+ /**
+ * Attributes to include in the Audit.
+ *
+ * @var array
+ */
+ protected $auditInclude = [
+ 'name',
+ 'email',
+ ];
+
+ public function updateProfilePhoto(UploadedFile $image_avatar)
+ {
+ try {
+ // Verificar si el archivo existe
+ if (!file_exists($image_avatar->getRealPath()))
+ throw new \Exception('El archivo no existe en la ruta especificada.');
+
+ if (!in_array($image_avatar->getClientOriginalExtension(), ['jpg', 'jpeg', 'png']))
+ throw new \Exception('El formato del archivo debe ser JPG o PNG.');
+
+ // Directorio donde se guardarán los avatares
+ $avatarDisk = self::AVATAR_DISK;
+ $avatarPath = self::PROFILE_PHOTO_DIR;
+ $avatarName = uniqid('avatar_') . '.png'; // Nombre único para el avatar
+
+ // Crear la instancia de ImageManager
+ $driver = config('image.driver', 'gd');
+ $manager = new ImageManager($driver);
+
+ // Crear el directorio si no existe
+ if (!Storage::disk($avatarDisk)->exists($avatarPath))
+ Storage::disk($avatarDisk)->makeDirectory($avatarPath);
+
+ // Leer la imagen
+ $image = $manager->read($image_avatar->getRealPath());
+
+ // crop the best fitting 5:3 (600x360) ratio and resize to 600x360 pixel
+ $image->cover(self::AVATAR_WIDTH, self::AVATAR_HEIGHT);
+
+ // Guardar la imagen en el disco de almacenamiento gestionado por Laravel
+ Storage::disk($avatarDisk)->put($avatarPath . '/' . $avatarName, $image->toPng(indexed: true));
+
+ // Elimina el avatar existente si hay uno
+ $this->deleteProfilePhoto();
+
+ // Update the user's profile photo path
+ $this->forceFill([
+ 'profile_photo_path' => $avatarName,
+ ])->save();
+ } catch (\Exception $e) {
+ throw new \Exception('Ocurrió un error al actualizar el avatar. ' . $e->getMessage());
+ }
+ }
+
+ public function deleteProfilePhoto()
+ {
+ if (!empty($this->profile_photo_path)) {
+ $avatarDisk = self::AVATAR_DISK;
+
+ Storage::disk($avatarDisk)->delete($this->profile_photo_path);
+
+ $this->forceFill([
+ 'profile_photo_path' => null,
+ ])->save();
+ }
+ }
+
+ public function getAvatarColor()
+ {
+ // Selecciona un color basado en el id del usuario
+ return self::AVATAR_COLORS[$this->id % count(self::AVATAR_COLORS)];
+ }
+
+ public static function getAvatarImage($name, $color, $background, $size)
+ {
+ $avatarDisk = self::AVATAR_DISK;
+ $directory = self::INITIAL_AVATAR_DIR;
+ $initials = self::getInitials($name);
+
+ $cacheKey = "avatar-{$initials}-{$color}-{$background}-{$size}";
+ $path = "{$directory}/{$cacheKey}.png";
+ $storagePath = storage_path("app/public/{$path}");
+
+ // Verificar si el avatar ya está en caché
+ if (Storage::disk($avatarDisk)->exists($path))
+ return response()->file($storagePath);
+
+ // Crear el avatar
+ $image = self::createAvatarImage($name, $color, $background, $size);
+
+ // Guardar en el directorio de iniciales
+ Storage::disk($avatarDisk)->put($path, $image->toPng(indexed: true));
+
+ // Retornar la imagen directamente
+ return response()->file($storagePath);
+ }
+
+ private static function createAvatarImage($name, $color, $background, $size)
+ {
+ // Usar la configuración del driver de imagen
+ $driver = config('image.driver', 'gd');
+ $manager = new ImageManager($driver);
+
+ $initials = self::getInitials($name);
+
+ // Obtener la ruta correcta de la fuente dentro del paquete
+ $fontPath = __DIR__ . '/../storage/fonts/OpenSans-Bold.ttf';
+
+ // Crear la imagen con fondo
+ $image = $manager->create($size, $size)
+ ->fill($background);
+
+ // Escribir texto en la imagen
+ $image->text(
+ $initials,
+ $size / 2, // Centrar horizontalmente
+ $size / 2, // Centrar verticalmente
+ function (FontFactory $font) use ($color, $size, $fontPath) {
+ $font->file($fontPath);
+ $font->size($size * 0.4);
+ $font->color($color);
+ $font->align('center');
+ $font->valign('middle');
+ }
+ );
+
+ return $image;
+ }
+
+ public static function getInitials($name)
+ {
+ // Manejar casos de nombres vacíos o nulos
+ if (empty($name))
+ return 'NA';
+
+ // Usar array_map para mayor eficiencia
+ $initials = implode('', array_map(function ($word) {
+ return mb_substr($word, 0, 1);
+ }, explode(' ', $name)));
+
+ $initials = substr($initials, 0, self::INITIAL_MAX_LENGTH);
+
+ return strtoupper($initials);
+ }
+
+ public function getProfilePhotoUrlAttribute()
+ {
+ if ($this->profile_photo_path)
+ return Storage::url(self::PROFILE_PHOTO_DIR . '/' . $this->profile_photo_path);
+
+ // Generar URL del avatar por iniciales
+ $name = urlencode($this->fullname);
+ $color = ltrim($this->getAvatarColor(), '#');
+ $background = ltrim(self::AVATAR_BACKGROUND, '#');
+ $size = (self::AVATAR_WIDTH + self::AVATAR_HEIGHT) / 2;
+
+ return url("/admin/usuario/avatar?name={$name}&color={$color}&background={$background}&size={$size}");
+ }
+
+ public function getFullnameAttribute()
+ {
+ return trim($this->name . ' ' . $this->last_name);
+ }
+
+ public function getInitialsAttribute()
+ {
+ return self::getInitials($this->fullname);
+ }
+
+ /**
+ * Envía la notificación de restablecimiento de contraseña.
+ *
+ * @param string $token
+ */
+ public function sendPasswordResetNotification($token)
+ {
+ // Usar la notificación personalizada
+ $this->notify(new CustomResetPasswordNotification($token));
+ }
+
+
+ /**
+ * Obtener usuarios activos con una excepción para incluir un usuario específico desactivado.
+ *
+ * @param array $filters Filtros opcionales como ['type' => 'user', 'status' => 1]
+ * @param int|null $includeUserId ID de usuario específico a incluir aunque esté inactivo
+ * @return array
+ */
+ public static function getUsersListWithInactive(int $includeUserId = null, array $filters = []): array
+ {
+ $query = self::query();
+
+ // Filtro por tipo de usuario
+ if (isset($filters['type'])) {
+ switch ($filters['type']) {
+ case 'partner':
+ $query->where('is_partner', 1);
+ break;
+ case 'employee':
+ $query->where('is_employee', 1);
+ break;
+ case 'prospect':
+ $query->where('is_prospect', 1);
+ break;
+ case 'customer':
+ $query->where('is_customer', 1);
+ break;
+ case 'provider':
+ $query->where('is_provider', 1);
+ break;
+ case 'user':
+ $query->where('is_user', 1);
+ break;
+ }
+ }
+
+ // Incluir usuarios activos o el usuario desactivado seleccionado
+ $query->where(function ($q) use ($filters, $includeUserId) {
+ if (isset($filters['status'])) {
+ $q->where('status', $filters['status']);
+ }
+
+ if ($includeUserId) {
+ $q->orWhere('id', $includeUserId);
+ }
+ });
+
+ // Formatear los datos como id => "Nombre Apellido"
+ return $query->pluck(\DB::raw("CONCAT(name, ' ', IFNULL(last_name, ''))"), 'id')->toArray();
+ }
+
+
+ /**
+ * Relations
+ */
+
+ // User who created this user
+ public function creator()
+ {
+ return $this->belongsTo(self::class, 'created_by');
+ }
+
+ public function isActive()
+ {
+ return $this->status === self::STATUS_ENABLED;
+ }
+
+}
diff --git a/Models/User.php b/Models/User.php
new file mode 100644
index 0000000..b1644d8
--- /dev/null
+++ b/Models/User.php
@@ -0,0 +1,237 @@
+ 'Habilitado',
+ self::STATUS_DISABLED => 'Deshabilitado',
+ self::STATUS_REMOVED => 'Eliminado',
+ ];
+
+ /**
+ * List of names for each status.
+ * @var array
+ */
+ public static $statusListClass = [
+ self::STATUS_ENABLED => 'success',
+ self::STATUS_DISABLED => 'warning',
+ self::STATUS_REMOVED => 'danger',
+ ];
+
+ /**
+ * The attributes that are mass assignable.
+ *
+ * @var array
+ */
+ protected $fillable = [
+ 'name',
+ 'last_name',
+ 'email',
+ 'password',
+ 'profile_photo_path',
+ 'status',
+ 'created_by',
+ ];
+
+ /**
+ * The attributes that should be hidden for serialization.
+ *
+ * @var array
+ */
+ protected $hidden = [
+ 'password',
+ 'remember_token',
+ 'two_factor_recovery_codes',
+ 'two_factor_secret',
+ ];
+
+ /**
+ * The accessors to append to the model's array form.
+ *
+ * @var array
+ */
+ protected $appends = [
+ 'profile_photo_url',
+ ];
+
+ /**
+ * Nombre de la etiqueta para generar Componentes
+ *
+ * @var string
+ */
+ public $tagName = 'User';
+
+ /**
+ * Nombre de la columna que contiee el nombre del registro
+ *
+ * @var string
+ */
+ public $columnNameLabel = 'full_name';
+
+ /**
+ * Nombre singular del registro.
+ *
+ * @var string
+ */
+ public $singularName = 'usuario';
+
+ /**
+ * Nombre plural del registro.
+ *
+ * @var string
+ */
+ public $pluralName = 'usuarios';
+
+ /**
+ * Get the attributes that should be cast.
+ *
+ * @return array
+ */
+ protected function casts(): array
+ {
+ return [
+ 'email_verified_at' => 'datetime',
+ 'password' => 'hashed',
+ ];
+ }
+
+ /**
+ * Attributes to include in the Audit.
+ *
+ * @var array
+ */
+ protected $auditInclude = [
+ 'name',
+ 'email',
+ ];
+
+ /**
+ * Get the full name of the user.
+ *
+ * @return string
+ */
+ public function getFullnameAttribute()
+ {
+ return trim($this->name . ' ' . $this->last_name);
+ }
+
+ /**
+ * Get the initials of the user's full name.
+ *
+ * @return string
+ */
+ public function getInitialsAttribute()
+ {
+ return self::getInitials($this->fullname);
+ }
+
+ /**
+ * Envía la notificación de restablecimiento de contraseña.
+ *
+ * @param string $token
+ */
+ public function sendPasswordResetNotification($token)
+ {
+ // Usar la notificación personalizada
+ $this->notify(new CustomResetPasswordNotification($token));
+ }
+
+ /**
+ * Obtener usuarios activos con una excepción para incluir un usuario específico desactivado.
+ *
+ * @param array $filters Filtros opcionales como ['type' => 'user', 'status' => 1]
+ * @param int|null $includeUserId ID de usuario específico a incluir aunque esté inactivo
+ * @return array
+ */
+ public static function getUsersListWithInactive($includeUserId = null, array $filters = []): array
+ {
+ $query = self::query();
+
+ // Filtro por tipo de usuario dinámico
+ $tipoUsuarios = [
+ 'partner' => 'is_partner',
+ 'employee' => 'is_employee',
+ 'prospect' => 'is_prospect',
+ 'customer' => 'is_customer',
+ 'provider' => 'is_provider',
+ 'user' => 'is_user',
+ ];
+
+ if (isset($filters['type']) && isset($tipoUsuarios[$filters['type']])) {
+ $query->where($tipoUsuarios[$filters['type']], 1);
+ }
+
+ // Filtrar por estado o incluir usuario inactivo
+ $query->where(function ($q) use ($filters, $includeUserId) {
+ if (isset($filters['status'])) {
+ $q->where('status', $filters['status']);
+ }
+
+ if ($includeUserId) {
+ $q->orWhere('id', $includeUserId);
+ }
+ });
+
+ return $query->pluck(\DB::raw("CONCAT(name, ' ', IFNULL(last_name, ''))"), 'id')->toArray();
+ }
+
+ /**
+ * User who created this user
+ */
+ public function creator()
+ {
+ return $this->belongsTo(self::class, 'created_by');
+ }
+
+ /**
+ * Check if the user is active
+ */
+ public function isActive()
+ {
+ return $this->status === self::STATUS_ENABLED;
+ }
+
+}
diff --git a/Models/UserLogin.php b/Models/UserLogin.php
new file mode 100644
index 0000000..3f86d0c
--- /dev/null
+++ b/Models/UserLogin.php
@@ -0,0 +1,14 @@
+token = $token;
+ }
+
+ /**
+ * Configura el canal de la notificación.
+ */
+ public function via($notifiable)
+ {
+ return ['mail'];
+ }
+
+ /**
+ * Configura el mensaje de correo.
+ */
+ public function toMail($notifiable)
+ {
+ try {
+ // Cargar configuración SMTP desde la base de datos
+ $this->loadDynamicMailConfig();
+
+ $resetUrl = url(route('password.reset', [
+ 'token' => $this->token,
+ 'email' => $notifiable->getEmailForPasswordReset()
+ ], false));
+
+ $appTitle = Setting::global()->where('key', 'website_title')->first()->value ?? Config::get('koneko.appTitle');
+ $imageBase64 = 'data:image/png;base64,' . base64_encode(file_get_contents(public_path('/assets/img/logo/koneko-04.png')));
+ $expireMinutes = Config::get('auth.passwords.' . Config::get('auth.defaults.passwords') . '.expire', 60);
+
+ Config::set('app.name', $appTitle);
+
+ return (new MailMessage)
+ ->subject("Restablece tu contraseña - {$appTitle}")
+ ->markdown('vuexy-admin::notifications.email', [ // Usar tu plantilla del módulo
+ 'greeting' => "Hola {$notifiable->name}",
+ 'introLines' => [
+ 'Estás recibiendo este correo porque solicitaste restablecer tu contraseña.',
+ ],
+ 'actionText' => 'Restablecer contraseña',
+ 'actionUrl' => $resetUrl,
+ 'outroLines' => [
+ "Este enlace expirará en {$expireMinutes} minutos.",
+ 'Si no solicitaste este cambio, no se requiere realizar ninguna acción.',
+ ],
+ 'displayableActionUrl' => $resetUrl, // Para el subcopy
+ 'image' => $imageBase64, // Imagen del logo
+ ]);
+
+ /*
+ */
+ } catch (\Exception $e) {
+ // Registrar el error
+ Log::error('Error al enviar el correo de restablecimiento: ' . $e->getMessage());
+
+ // Retornar un mensaje alternativo
+ return (new MailMessage)
+ ->subject('Restablece tu contraseña')
+ ->line('Ocurrió un error al enviar el correo. Por favor, intenta de nuevo más tarde.');
+ }
+ }
+
+ /**
+ * Cargar configuración SMTP desde la base de datos.
+ */
+ protected function loadDynamicMailConfig()
+ {
+ try {
+ $smtpConfig = Setting::where('key', 'LIKE', 'mail_%')
+ ->pluck('value', 'key');
+
+ if ($smtpConfig->isEmpty()) {
+ throw new Exception('No SMTP configuration found in the database.');
+ }
+
+ Config::set('mail.mailers.smtp.host', $smtpConfig['mail_mailers_smtp_host'] ?? null);
+ Config::set('mail.mailers.smtp.port', $smtpConfig['mail_mailers_smtp_port'] ?? null);
+ Config::set('mail.mailers.smtp.username', $smtpConfig['mail_mailers_smtp_username'] ?? null);
+ Config::set(
+ 'mail.mailers.smtp.password',
+ isset($smtpConfig['mail_mailers_smtp_password'])
+ ? Crypt::decryptString($smtpConfig['mail_mailers_smtp_password'])
+ : null
+ );
+ Config::set('mail.mailers.smtp.encryption', $smtpConfig['mail_mailers_smtp_encryption'] ?? null);
+ Config::set('mail.from.address', $smtpConfig['mail_from_address'] ?? null);
+ Config::set('mail.from.name', $smtpConfig['mail_from_name'] ?? null);
+ } catch (Exception $e) {
+ Log::error('SMTP Configuration Error: ' . $e->getMessage());
+ // Opcional: Puedes lanzar la excepción o manejarla de otra manera.
+ throw new Exception('Error al cargar la configuración SMTP.');
+ }
+ }
+}
diff --git a/Providers/ConfigServiceProvider.php b/Providers/ConfigServiceProvider.php
new file mode 100644
index 0000000..d165392
--- /dev/null
+++ b/Providers/ConfigServiceProvider.php
@@ -0,0 +1,31 @@
+mergeConfigFrom(__DIR__.'/../config/vuexy.php', 'vuexy');
+ }
+
+ /**
+ * Bootstrap services.
+ */
+ public function boot(): void
+ {
+ // Cargar configuración del sistema
+ $globalSettingsService = app(GlobalSettingsService::class);
+ $globalSettingsService->loadSystemConfig();
+
+ // Cargar configuración del sistema a través del servicio
+ app(GlobalSettingsService::class)->loadSystemConfig();
+ }
+}
diff --git a/Providers/FortifyServiceProvider.php b/Providers/FortifyServiceProvider.php
new file mode 100644
index 0000000..4b631d9
--- /dev/null
+++ b/Providers/FortifyServiceProvider.php
@@ -0,0 +1,124 @@
+input(Fortify::username())) . '|' . $request->ip());
+
+ return Limit::perMinute(5)->by($throttleKey);
+ });
+
+ RateLimiter::for('two-factor', function (Request $request) {
+ return Limit::perMinute(5)->by($request->session()->get('login.id'));
+ });
+
+ Fortify::authenticateUsing(function (Request $request) {
+ $user = User::where('email', $request->email)
+ ->where('status', User::STATUS_ENABLED)
+ ->first();
+
+ if ($user && Hash::check($request->password, $user->password)) {
+ return $user;
+ }
+ });
+
+ // Simula lo que hace tu middleware y comparte `_admin`
+ $viewMode = Config::get('vuexy.custom.authViewMode');
+ $adminVars = app(AdminTemplateService::class)->getAdminVars();
+
+ // Configurar la vista del login
+ Fortify::loginView(function () use ($viewMode, $adminVars) {
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ view()->share('_admin', $adminVars);
+
+ return view("vuexy-admin::auth.login-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ });
+
+ // Configurar la vista del registro (si lo necesitas)
+ Fortify::registerView(function () use ($viewMode, $adminVars) {
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ view()->share('_admin', $adminVars);
+
+ return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ });
+
+ // Configurar la vista de restablecimiento de contraseñas
+ Fortify::requestPasswordResetLinkView(function () use ($viewMode, $adminVars) {
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ view()->share('_admin', $adminVars);
+
+ return view("vuexy-admin::auth.forgot-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ });
+
+ Fortify::resetPasswordView(function ($request) use ($viewMode, $adminVars) {
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ view()->share('_admin', $adminVars);
+
+ return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs, 'request' => $request]);
+ });
+
+ // Vista de verificación de correo electrónico
+ Fortify::verifyEmailView(function () use ($viewMode, $adminVars) {
+ view()->share('_admin', $adminVars);
+
+ return view("vuexy-admin::auth.verify-email-{$viewMode}");
+ });
+
+ // Vista de confirmación de contraseña
+ Fortify::confirmPasswordView(function () use ($viewMode, $adminVars) {
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ view()->share('_admin', $adminVars);
+
+ return view("vuexy-admin::auth.confirm-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ });
+
+ // Configurar la vista para la verificación de dos factores
+ Fortify::twoFactorChallengeView(function () use ($viewMode, $adminVars) {
+ $pageConfigs = ['myLayout' => 'blank'];
+
+ view()->share('_admin', $adminVars);
+
+ return view("vuexy-admin::auth.two-factor-challenge-{$viewMode}", ['pageConfigs' => $pageConfigs]);
+ });
+ }
+}
diff --git a/Providers/VuexyAdminServiceProvider.php b/Providers/VuexyAdminServiceProvider.php
new file mode 100644
index 0000000..0040af0
--- /dev/null
+++ b/Providers/VuexyAdminServiceProvider.php
@@ -0,0 +1,132 @@
+mergeConfigFrom(__DIR__.'/../config/koneko.php', 'koneko');
+
+ // Register the module's services and providers
+ $this->app->register(ConfigServiceProvider::class);
+ $this->app->register(FortifyServiceProvider::class);
+ $this->app->register(PermissionServiceProvider::class);
+
+ // Register the module's aliases
+ AliasLoader::getInstance()->alias('Helper', VuexyHelper::class);
+ }
+
+ /**
+ * Bootstrap any application services.
+ */
+ public function boot(): void
+ {
+ if(env('FORCE_HTTPS', false)){
+ URL::forceScheme('https');
+ }
+
+ // Registrar alias del middleware
+ $this->app['router']->aliasMiddleware('admin', AdminTemplateMiddleware::class);
+
+ // Sobrescribir ruta de traducciones para asegurar que se usen las del paquete
+ $this->app->bind('path.lang', function () {
+ return __DIR__ . '/../resources/lang';
+ });
+
+ // Register the module's routes
+ $this->loadRoutesFrom(__DIR__.'/../routes/admin.php');
+
+
+ // Cargar vistas del paquete
+ $this->loadViewsFrom(__DIR__.'/../resources/views', 'vuexy-admin');
+
+ // Registrar Componentes Blade
+ Blade::componentNamespace('VuexyAdmin\\View\\Components', 'vuexy-admin');
+
+
+ // Publicar los archivos necesarios
+ $this->publishes([
+ __DIR__.'/../config/fortify.php' => config_path('fortify.php'),
+ __DIR__.'/../config/image.php' => config_path('image.php'),
+ __DIR__.'/../config/vuexy_menu.php' => config_path('vuexy_menu.php'),
+ ], 'vuexy-admin-config');
+
+ $this->publishes([
+ __DIR__.'/../database/seeders/' => database_path('seeders'),
+ __DIR__.'/../database/data' => database_path('data'),
+ ], 'vuexy-admin-seeders');
+
+ $this->publishes([
+ __DIR__.'/../resources/img' => public_path('vendor/vuexy-admin/img'),
+ ], 'vuexy-admin-images');
+
+
+ // Register the migrations
+ $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
+
+
+ // Registrar eventos
+ Event::listen(Login::class, HandleUserLogin::class);
+ Event::listen(Logout::class, ClearUserCache::class);
+
+
+ // Registrar comandos de consola
+ if ($this->app->runningInConsole()) {
+ $this->commands([
+ CleanInitialAvatars::class,
+ ]);
+ }
+
+ // Registrar Livewire Components
+ $components = [
+ 'user-index' => UserIndex::class,
+ 'user-show' => UserShow::class,
+ 'user-form' => UserForm::class,
+ 'user-offcanvas-form' => UserOffCanvasForm::class,
+ 'role-index' => RoleIndex::class,
+ 'permission-index' => PermissionIndex::class,
+
+
+ 'general-settings' => GeneralSettings::class,
+ 'application-settings' => ApplicationSettings::class,
+ 'interface-settings' => InterfaceSettings::class,
+ 'mail-smtp-settings' => MailSmtpSettings::class,
+ 'mail-sender-response-settings' => MailSenderResponseSettings::class,
+ 'cache-stats' => CacheStats::class,
+ 'session-stats' => SessionStats::class,
+ 'redis-stats' => RedisStats::class,
+ 'memcached-stats' => MemcachedStats::class,
+ 'cache-functions' => CacheFunctions::class,
+ ];
+
+ foreach ($components as $alias => $component) {
+ Livewire::component($alias, $component);
+ }
+
+ // Registrar auditoría en usuarios
+ User::observe(AuditableObserver::class);
+ }
+}
diff --git a/Queries/BootstrapTableQueryBuilder.php b/Queries/BootstrapTableQueryBuilder.php
new file mode 100644
index 0000000..00984cb
--- /dev/null
+++ b/Queries/BootstrapTableQueryBuilder.php
@@ -0,0 +1,104 @@
+request = $request;
+ $this->config = $config;
+ $this->query = DB::table($config['table']);
+
+ $this->applyJoins();
+ $this->applyFilters();
+ }
+
+ protected function applyJoins()
+ {
+ if (!empty($this->config['joins'])) {
+ foreach ($this->config['joins'] as $join) {
+ $type = $join['type'] ?? 'join';
+
+ $this->query->{$type}($join['table'], function($joinObj) use ($join) {
+ $joinObj->on($join['first'], '=', $join['second']);
+
+ // Soporte para AND en ON, si está definidio
+ if (!empty($join['and'])) {
+ foreach ((array) $join['and'] as $andCondition) {
+ // 'sat_codigo_postal.c_estado = sat_localidad.c_estado'
+ $parts = explode('=', $andCondition);
+
+ if (count($parts) === 2) {
+ $left = trim($parts[0]);
+ $right = trim($parts[1]);
+
+ $joinObj->whereRaw("$left = $right");
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ protected function applyFilters()
+ {
+ if (!empty($this->config['filters'])) {
+ foreach ($this->config['filters'] as $filter => $column) {
+ if ($this->request->filled($filter)) {
+ $this->query->where($column, 'LIKE', '%' . $this->request->input($filter) . '%');
+ }
+ }
+ }
+ }
+
+ protected function applyGrouping()
+ {
+ if (!empty($this->config['group_by'])) {
+ $this->query->groupBy($this->config['group_by']);
+ }
+ }
+
+ public function getJson()
+ {
+ $this->applyGrouping();
+
+ // Calcular total de filas antes de aplicar paginación
+ $total = DB::select("SELECT COUNT(*) as num_rows FROM (" . $this->query->selectRaw('0')->toSql() . ") as items", $this->query->getBindings())[0]->num_rows;
+
+ // Para ver la sentencia SQL (con placeholders ?)
+ //dump($this->query->toSql()); dd($this->query->getBindings());
+
+ // Aplicar orden, paginación y selección de columnas
+ $this->query
+ ->select($this->config['columns'])
+ ->when($this->request->input('sort'), function ($query) {
+ $query->orderBy($this->request->input('sort'), $this->request->input('order', 'asc'));
+ })
+ ->when($this->request->input('offset'), function ($query) {
+ $query->offset($this->request->input('offset'));
+ })
+ ->limit($this->request->input('limit', 10));
+
+ // Obtener resultados y limpiar los datos antes de enviarlos
+ $rows = $this->query->get()->map(function ($item) {
+ return collect($item)
+ ->reject(fn($val) => is_null($val) || $val === '') // Eliminar valores nulos o vacíos
+ ->map(fn($val) => is_numeric($val) ? (float) $val : $val) // Convertir números correctamente
+ ->toArray();
+ });
+
+ return response()->json([
+ "total" => $total,
+ "rows" => $rows,
+ ]);
+ }
+}
diff --git a/Queries/GenericQueryBuilder.php b/Queries/GenericQueryBuilder.php
new file mode 100644
index 0000000..23a38d3
--- /dev/null
+++ b/Queries/GenericQueryBuilder.php
@@ -0,0 +1,8 @@
+
-
-
-
+
-
+
-
+
+
+
---
-# Laravel Vuexy Admin para México
+## 📌 Descripción
-**Laravel Vuexy Admin para México** es un proyecto basado en Laravel optimizado para necesidades específicas del mercado mexicano. Incluye integración con los catálogos del SAT (CFDI 4.0), herramientas avanzadas y una interfaz moderna inspirada en el template premium Vuexy.
+**Laravel Vuexy Admin** es un módulo de administración optimizado para México, basado en Laravel 11 y diseñado para integrarse con **Vuexy Admin Template**. Incluye gestión avanzada de usuarios, roles, permisos y auditoría de acciones.
-## Características destacadas
-
-- **Optimización para México**:
- - Uso de los catálogos oficiales del SAT (versión CFDI 4.0):
- - Banco (`sat_banco`)
- - Clave de Producto o Servicio (`sat_clave_prod_serv`)
- - Clave de Unidad (`sat_clave_unidad`)
- - Forma de Pago (`sat_forma_pago`)
- - Moneda (`sat_moneda`)
- - Código Postal (`sat_codigo_postal`)
- - Régimen Fiscal (`sat_regimen_fiscal`)
- - País (`sat_pais`)
- - Uso CFDI (`sat_uso_cfdi`)
- - Colonia (`sat_colonia`)
- - Estado (`sat_estado`)
- - Localidad (`sat_localidad`)
- - Municipio (`sat_municipio`)
- - Deducción (`sat_deduccion`)
- - Percepción (`sat_percepcion`)
- - Compatible con los lineamientos y formatos del Anexo 20 del SAT.
- - Útil para generar comprobantes fiscales digitales (CFDI) y otros procesos administrativos locales.
-
-- **Otras características avanzadas**:
- - Autenticación y gestión de usuarios con Laravel Fortify.
- - Gestión de roles y permisos usando Spatie Permission.
- - Tablas dinámicas con Laravel Datatables y Yajra.
- - Integración con Redis para caching eficiente.
- - Exportación y manejo de Excel mediante Maatwebsite.
-
-## Requisitos del Sistema
-
-- **PHP**: >= 8.2
-- **Composer**: >= 2.0
-- **Node.js**: >= 16.x
-- **MySQL** o cualquier base de datos compatible con Laravel.
+### ✨ Características
+- 🔹 Sistema de autenticación con Laravel Fortify.
+- 🔹 Gestión avanzada de usuarios con Livewire.
+- 🔹 Control de roles y permisos con Spatie Permissions.
+- 🔹 Auditoría de acciones con Laravel Auditing.
+- 🔹 Publicación de configuraciones y vistas.
+- 🔹 Soporte para cache y optimización de rendimiento.
---
-## Instalación
+## 📦 Instalación
-Este proyecto ofrece dos métodos de instalación: mediante Composer o manualmente. A continuación, te explicamos ambos procesos.
-
-### Opción 1: Usar Composer (Recomendado)
-
-Para instalar el proyecto rápidamente usando Composer, ejecuta el siguiente comando:
+Instalar vía **Composer**:
```bash
-composer create-project koneko/laravel-vuexy-admin
+composer require koneko/laravel-vuexy-admin
```
-Este comando realizará automáticamente los siguientes pasos:
-1. Configurará el archivo `.env` basado en `.env.example`.
-2. Generará la clave de la aplicación.
-
-Una vez completado, debes configurar una base de datos válida en el archivo `.env` y luego ejecutar:
+Publicar archivos de configuración y migraciones:
```bash
+php artisan vendor:publish --tag=vuexy-admin-config
+php artisan migrate
+```
+
+---
+
+## 🚀 Uso básico
+
+```php
+use Koneko\VuexyAdmin\Models\User;
+
+$user = User::create([
+ 'name' => 'Juan Pérez',
+ 'email' => 'juan@example.com',
+ 'password' => bcrypt('secret'),
+]);
+```
+
+---
+
+## 📚 Configuración adicional
+
+Si necesitas personalizar la configuración del módulo, publica el archivo de configuración:
+
+```bash
+php artisan vendor:publish --tag=vuexy-admin-config
+```
+
+Esto generará `config/vuexy_menu.php`, donde puedes modificar valores predeterminados.
+
+---
+
+## 🛠 Dependencias
+
+Este paquete requiere las siguientes dependencias:
+- Laravel 11
+- `laravel/fortify` (autenticación)
+- `spatie/laravel-permission` (gestión de roles y permisos)
+- `owen-it/laravel-auditing` (auditoría de usuarios)
+- `livewire/livewire` (interfaz dinámica)
+
+---
+
+## 📦 Publicación de Assets y Configuraciones
+
+Para publicar configuraciones y seeders:
+
+```bash
+php artisan vendor:publish --tag=vuexy-admin-config
+php artisan vendor:publish --tag=vuexy-admin-seeders
php artisan migrate --seed
```
-Finalmente, compila los activos iniciales:
+Para publicar imágenes del tema:
```bash
-npm install
-npm run dev
-```
-
-Inicia el servidor local con:
-
-```bash
-php artisan serve
+php artisan vendor:publish --tag=vuexy-admin-images
```
---
-### Opción 2: Instalación manual
+## 🌍 Repositorio Principal y Sincronización
-Si prefieres instalar el proyecto de forma manual, sigue estos pasos:
+Este repositorio es una **copia sincronizada** del repositorio principal alojado en **[Tea - Koneko Git](https://git.koneko.mx/koneko/laravel-vuexy-admin)**.
-1. Clona el repositorio:
- ```bash
- git clone https://git.koneko.mx/Koneko-ST/laravel-vuexy-admin.git
- cd laravel-vuexy-admin
- ```
+### 🔄 Sincronización con GitHub
+- **Repositorio Principal:** [git.koneko.mx](https://git.koneko.mx/koneko/laravel-vuexy-admin)
+- **Repositorio en GitHub:** [github.com/koneko-mx/laravel-vuexy-admin](https://github.com/koneko-mx/laravel-vuexy-admin)
+- **Los cambios pueden reflejarse primero en Tea antes de GitHub.**
-2. Instala las dependencias de Composer:
- ```bash
- composer install
- ```
+### 🤝 Contribuciones
+Si deseas contribuir:
+1. Puedes abrir un **Issue** en [GitHub Issues](https://github.com/koneko-mx/laravel-vuexy-admin/issues).
+2. Para Pull Requests, **preferimos contribuciones en Tea**. Contacta a `admin@koneko.mx` para solicitar acceso.
-3. Instala las dependencias de npm:
- ```bash
- npm install
- ```
-
-4. Configura las variables de entorno:
- ```bash
- cp .env.example .env
- ```
-
-5. Configura una base de datos válida en el archivo `.env`.
-
-6. Genera la clave de la aplicación:
- ```bash
- php artisan key:generate
- ```
-
-7. Migra y llena la base de datos:
- ```bash
- php artisan migrate --seed
- ```
-
-8. Compila los activos frontend:
- ```bash
- npm run dev
- ```
-
-9. Inicia el servidor de desarrollo:
- ```bash
- php artisan serve
- ```
+⚠️ **Nota:** Algunos cambios pueden tardar en reflejarse en GitHub, ya que este repositorio se actualiza automáticamente desde Tea.
---
-## Notas importantes
+## 🏅 Licencia
-- Asegúrate de tener instalado:
- - **PHP**: >= 8.2
- - **Composer**: >= 2.0
- - **Node.js**: >= 16.x
-- Este proyecto utiliza los catálogos SAT de la versión CFDI 4.0. Si deseas más información, visita la documentación oficial del SAT en [Anexo 20](http://omawww.sat.gob.mx/tramitesyservicios/Paginas/anexo_20.htm).
+Este paquete es de código abierto bajo la licencia [MIT](LICENSE).
---
-## Uso del Template Vuexy
-
-Este proyecto está diseñado para funcionar con el template premium [Vuexy](https://themeforest.net/item/vuexy-vuejs-html-laravel-admin-dashboard-template/23328599). Para utilizarlo:
-
-1. Adquiere una licencia válida de Vuexy en [ThemeForest](https://themeforest.net/item/vuexy-vuejs-html-laravel-admin-dashboard-template/23328599).
-2. Incluye los archivos necesarios en las carpetas correspondientes (`resources`, `public`, etc.) de este proyecto.
-
----
-
-## Créditos
-
-Este proyecto utiliza herramientas y recursos de código abierto, así como un template premium. Queremos agradecer a los desarrolladores y diseñadores que hacen posible esta implementación:
-
-- [Laravel](https://laravel.com)
-- [Vuexy Template](https://themeforest.net/item/vuexy-vuejs-html-laravel-admin-dashboard-template/23328599)
-- [Spatie Permission](https://spatie.be/docs/laravel-permission)
-- [Yajra Datatables](https://yajrabox.com/docs/laravel-datatables)
-
----
-
-## Licencia
-
-Este proyecto está licenciado bajo la licencia MIT. Consulta el archivo [LICENSE](LICENSE) para más detalles.
-
-El template "Vuexy" debe adquirirse por separado y está sujeto a su propia licencia comercial.
-
----
Hecho con ❤️ por Koneko Soluciones Tecnológicas
-
diff --git a/Rules/NotEmptyHtml.php b/Rules/NotEmptyHtml.php
new file mode 100644
index 0000000..885ec53
--- /dev/null
+++ b/Rules/NotEmptyHtml.php
@@ -0,0 +1,20 @@
+ [180, 180],
+ '192x192' => [192, 192],
+ '152x152' => [152, 152],
+ '120x120' => [120, 120],
+ '76x76' => [76, 76],
+ '16x16' => [16, 16],
+ ];
+
+ private $imageLogoMaxPixels1 = 22500; // Primera versión (px^2)
+ private $imageLogoMaxPixels2 = 75625; // Segunda versión (px^2)
+ private $imageLogoMaxPixels3 = 262144; // Tercera versión (px^2)
+ private $imageLogoMaxPixels4 = 230400; // Tercera versión (px^2) en Base64
+
+ protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos
+
+ public function __construct()
+ {
+ $this->driver = config('image.driver', 'gd');
+ }
+
+ public function updateSetting(string $key, string $value): bool
+ {
+ $setting = Setting::updateOrCreate(
+ ['key' => $key],
+ ['value' => trim($value)]
+ );
+
+ return $setting->save();
+ }
+
+ public function processAndSaveFavicon($image): void
+ {
+ Storage::makeDirectory($this->imageDisk . '/' . $this->favicon_basePath);
+
+ // Eliminar favicons antiguos
+ $this->deleteOldFavicons();
+
+ // Guardar imagen original
+ $imageManager = new ImageManager($this->driver);
+
+ $imageName = uniqid('admin_favicon_');
+
+ $image = $imageManager->read($image->getRealPath());
+
+ foreach ($this->faviconsSizes as $size => [$width, $height]) {
+ $resizedPath = $this->favicon_basePath . $imageName . "_{$size}.png";
+
+ $image->cover($width, $height);
+
+ Storage::disk($this->imageDisk)->put($resizedPath, $image->toPng(indexed: true));
+ }
+
+ $this->updateSetting('admin_favicon_ns', $this->favicon_basePath . $imageName);
+ }
+
+ protected function deleteOldFavicons(): void
+ {
+ // Obtener el favicon actual desde la base de datos
+ $currentFavicon = Setting::where('key', 'admin_favicon_ns')->value('value');
+
+ if ($currentFavicon) {
+ $filePaths = [
+ $this->imageDisk . '/' . $currentFavicon,
+ $this->imageDisk . '/' . $currentFavicon . '_16x16.png',
+ $this->imageDisk . '/' . $currentFavicon . '_76x76.png',
+ $this->imageDisk . '/' . $currentFavicon . '_120x120.png',
+ $this->imageDisk . '/' . $currentFavicon . '_152x152.png',
+ $this->imageDisk . '/' . $currentFavicon . '_180x180.png',
+ $this->imageDisk . '/' . $currentFavicon . '_192x192.png',
+ ];
+
+ foreach ($filePaths as $filePath) {
+ if (Storage::exists($filePath)) {
+ Storage::delete($filePath);
+ }
+ }
+ }
+ }
+
+ public function processAndSaveImageLogo($image, string $type = ''): void
+ {
+ // Crear directorio si no existe
+ Storage::makeDirectory($this->imageDisk . '/' . $this->image_logo_basePath);
+
+ // Eliminar imágenes antiguas
+ $this->deleteOldImageWebapp($type);
+
+ // Leer imagen original
+ $imageManager = new ImageManager($this->driver);
+ $image = $imageManager->read($image->getRealPath());
+
+ // Generar tres versiones con diferentes áreas máximas
+ $this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels1, 'small'); // Versión 1
+ $this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels2, 'medium'); // Versión 2
+ $this->generateAndSaveImage($image, $type, $this->imageLogoMaxPixels3); // Versión 3
+ $this->generateAndSaveImageAsBase64($image, $type, $this->imageLogoMaxPixels4); // Versión 3
+ }
+
+ private function generateAndSaveImage($image, string $type, int $maxPixels, string $suffix = ''): void
+ {
+ $imageClone = clone $image;
+
+ // Escalar imagen conservando aspecto
+ $this->resizeImageToMaxPixels($imageClone, $maxPixels);
+
+ $imageName = 'admin_image_logo' . ($suffix ? '_' . $suffix : '') . ($type == 'dark' ? '_dark' : '');
+
+ // Generar nombre y ruta
+ $imageNameUid = uniqid($imageName . '_', ".png");
+ $resizedPath = $this->image_logo_basePath . $imageNameUid;
+
+ // Guardar imagen en PNG
+ Storage::disk($this->imageDisk)->put($resizedPath, $imageClone->toPng(indexed: true));
+
+ // Actualizar configuración
+ $this->updateSetting($imageName, $resizedPath);
+ }
+
+ private function resizeImageToMaxPixels($image, int $maxPixels)
+ {
+ // Obtener dimensiones originales de la imagen
+ $originalWidth = $image->width(); // Método para obtener el ancho
+ $originalHeight = $image->height(); // Método para obtener el alto
+
+ // Calcular el aspecto
+ $aspectRatio = $originalWidth / $originalHeight;
+
+ // Calcular dimensiones redimensionadas conservando aspecto
+ if ($aspectRatio > 1) { // Ancho es dominante
+ $newWidth = sqrt($maxPixels * $aspectRatio);
+ $newHeight = $newWidth / $aspectRatio;
+ } else { // Alto es dominante
+ $newHeight = sqrt($maxPixels / $aspectRatio);
+ $newWidth = $newHeight * $aspectRatio;
+ }
+
+ // Redimensionar la imagen
+ $image->resize(
+ round($newWidth), // Redondear para evitar problemas con números decimales
+ round($newHeight),
+ function ($constraint) {
+ $constraint->aspectRatio();
+ $constraint->upsize();
+ }
+ );
+
+ return $image;
+ }
+
+
+ private function generateAndSaveImageAsBase64($image, string $type, int $maxPixels): void
+ {
+ $imageClone = clone $image;
+
+ // Redimensionar imagen conservando el aspecto
+ $this->resizeImageToMaxPixels($imageClone, $maxPixels);
+
+ // Convertir a Base64
+ $base64Image = (string) $imageClone->toJpg(40)->toDataUri();
+
+ // Guardar como configuración
+ $this->updateSetting(
+ "admin_image_logo_base64" . ($type === 'dark' ? '_dark' : ''),
+ $base64Image // Ya incluye "data:image/png;base64,"
+ );
+ }
+
+ protected function deleteOldImageWebapp(string $type = ''): void
+ {
+ // Determinar prefijo según el tipo (normal o dark)
+ $suffix = $type === 'dark' ? '_dark' : '';
+
+ // Claves relacionadas con las imágenes que queremos limpiar
+ $imageKeys = [
+ "admin_image_logo{$suffix}",
+ "admin_image_logo_small{$suffix}",
+ "admin_image_logo_medium{$suffix}",
+ ];
+
+ // Recuperar las imágenes actuales en una sola consulta
+ $settings = Setting::whereIn('key', $imageKeys)->pluck('value', 'key');
+
+ foreach ($imageKeys as $key) {
+ // Obtener la imagen correspondiente
+ $currentImage = $settings[$key] ?? null;
+
+ if ($currentImage) {
+ // Construir la ruta del archivo y eliminarlo si existe
+ $filePath = $this->imageDisk . '/' . $currentImage;
+ if (Storage::exists($filePath)) {
+ Storage::delete($filePath);
+ }
+
+ // Eliminar la configuración de la base de datos
+ Setting::where('key', $key)->delete();
+ }
+ }
+ }
+}
diff --git a/Services/AdminTemplateService.php b/Services/AdminTemplateService.php
new file mode 100644
index 0000000..b1d177f
--- /dev/null
+++ b/Services/AdminTemplateService.php
@@ -0,0 +1,156 @@
+ $key],
+ ['value' => trim($value)]
+ );
+
+ return $setting->save();
+ }
+
+ public function getAdminVars($adminSetting = false): array
+ {
+ try {
+ // Verificar si el sistema está inicializado (la tabla `migrations` existe)
+ if (!Schema::hasTable('migrations')) {
+ return $this->getDefaultAdminVars($adminSetting);
+ }
+
+ // Cargar desde el caché o la base de datos si está disponible
+ return Cache::remember('admin_settings', $this->cacheTTL, function () use ($adminSetting) {
+ $settings = Setting::global()
+ ->where('key', 'LIKE', 'admin_%')
+ ->pluck('value', 'key')
+ ->toArray();
+
+ $adminSettings = $this->buildAdminVarsArray($settings);
+
+ return $adminSetting
+ ? $adminSettings[$adminSetting]
+ : $adminSettings;
+ });
+ } catch (\Exception $e) {
+ // En caso de error, devolver valores predeterminados
+ return $this->getDefaultAdminVars($adminSetting);
+ }
+ }
+
+ private function getDefaultAdminVars($adminSetting = false): array
+ {
+ $defaultSettings = [
+ 'title' => config('koneko.appTitle', 'Default Title'),
+ 'author' => config('koneko.author', 'Default Author'),
+ 'description' => config('koneko.description', 'Default Description'),
+ 'favicon' => $this->getFaviconPaths([]),
+ 'app_name' => config('koneko.appName', 'Default App Name'),
+ 'image_logo' => $this->getImageLogoPaths([]),
+ ];
+
+ return $adminSetting
+ ? $defaultSettings[$adminSetting] ?? null
+ : $defaultSettings;
+ }
+
+ private function buildAdminVarsArray(array $settings): array
+ {
+ return [
+ 'title' => $settings['admin_title'] ?? config('koneko.appTitle'),
+ 'author' => config('koneko.author'),
+ 'description' => config('koneko.description'),
+ 'favicon' => $this->getFaviconPaths($settings),
+ 'app_name' => $settings['admin_app_name'] ?? config('koneko.appName'),
+ 'image_logo' => $this->getImageLogoPaths($settings),
+ ];
+ }
+
+ public function getVuexyCustomizerVars()
+ {
+ // Obtener valores de la base de datos
+ $settings = Setting::global()
+ ->where('key', 'LIKE', 'vuexy_%')
+ ->pluck('value', 'key')
+ ->toArray();
+
+ // Obtener configuraciones predeterminadas
+ $defaultConfig = Config::get('vuexy.custom', []);
+
+ // Mezclar las configuraciones predeterminadas con las de la base de datos
+ return collect($defaultConfig)
+ ->mapWithKeys(function ($defaultValue, $key) use ($settings) {
+ $vuexyKey = 'vuexy_' . $key; // Convertir clave al formato de la base de datos
+
+ // Obtener valor desde la base de datos o usar el predeterminado
+ $value = $settings[$vuexyKey] ?? $defaultValue;
+
+ // Forzar booleanos para claves específicas
+ if (in_array($key, ['displayCustomizer', 'footerFixed', 'menuFixed', 'menuCollapsed', 'showDropdownOnHover'])) {
+ $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
+ }
+
+ return [$key => $value];
+ })
+ ->toArray();
+ }
+
+ /**
+ * Obtiene los paths de favicon en distintos tamaños.
+ */
+ private function getFaviconPaths(array $settings): array
+ {
+ $defaultFavicon = config('koneko.appFavicon');
+ $namespace = $settings['admin_favicon_ns'] ?? null;
+
+ return [
+ 'namespace' => $namespace,
+ '16x16' => $namespace ? "{$namespace}_16x16.png" : $defaultFavicon,
+ '76x76' => $namespace ? "{$namespace}_76x76.png" : $defaultFavicon,
+ '120x120' => $namespace ? "{$namespace}_120x120.png" : $defaultFavicon,
+ '152x152' => $namespace ? "{$namespace}_152x152.png" : $defaultFavicon,
+ '180x180' => $namespace ? "{$namespace}_180x180.png" : $defaultFavicon,
+ '192x192' => $namespace ? "{$namespace}_192x192.png" : $defaultFavicon,
+ ];
+ }
+
+ /**
+ * Obtiene los paths de los logos en distintos tamaños.
+ */
+ private function getImageLogoPaths(array $settings): array
+ {
+ $defaultLogo = config('koneko.appLogo');
+
+ return [
+ 'small' => $this->getImagePath($settings, 'admin_image_logo_small', $defaultLogo),
+ 'medium' => $this->getImagePath($settings, 'admin_image_logo_medium', $defaultLogo),
+ 'large' => $this->getImagePath($settings, 'admin_image_logo', $defaultLogo),
+ 'small_dark' => $this->getImagePath($settings, 'admin_image_logo_small_dark', $defaultLogo),
+ 'medium_dark' => $this->getImagePath($settings, 'admin_image_logo_medium_dark', $defaultLogo),
+ 'large_dark' => $this->getImagePath($settings, 'admin_image_logo_dark', $defaultLogo),
+ ];
+ }
+
+ /**
+ * Obtiene un path de imagen o retorna un valor predeterminado.
+ */
+ private function getImagePath(array $settings, string $key, string $default): string
+ {
+ return $settings[$key] ?? $default;
+ }
+
+ public static function clearAdminVarsCache()
+ {
+ Cache::forget("admin_settings");
+ }
+}
diff --git a/Services/AvatarImageService.php b/Services/AvatarImageService.php
new file mode 100644
index 0000000..5efc78f
--- /dev/null
+++ b/Services/AvatarImageService.php
@@ -0,0 +1,76 @@
+getRealPath())) {
+ throw new \Exception('El archivo no existe en la ruta especificada.');
+ }
+
+ if (!in_array($image_avatar->getClientOriginalExtension(), ['jpg', 'jpeg', 'png'])) {
+ throw new \Exception('El formato del archivo debe ser JPG o PNG.');
+ }
+
+ $avatarName = uniqid('avatar_') . '.png';
+ $driver = config('image.driver', 'gd');
+
+ $manager = new ImageManager($driver);
+
+ if (!Storage::disk($this->avatarDisk)->exists($this->profilePhotoDir)) {
+ Storage::disk($this->avatarDisk)->makeDirectory($this->profilePhotoDir);
+ }
+
+ $image = $manager->read($image_avatar->getRealPath());
+ $image->cover($this->avatarWidth, $this->avatarHeight);
+ Storage::disk($this->avatarDisk)->put($this->profilePhotoDir . '/' . $avatarName, $image->toPng(indexed: true));
+
+ // Eliminar avatar existente
+ $this->deleteProfilePhoto($user);
+
+ $user->forceFill([
+ 'profile_photo_path' => $avatarName,
+ ])->save();
+ }
+
+
+ /**
+ * Elimina la foto de perfil actual del usuario.
+ *
+ * @param mixed $user Objeto usuario.
+ *
+ * @return void
+ */
+ public function deleteProfilePhoto($user)
+ {
+ if (!empty($user->profile_photo_path)) {
+ Storage::disk($this->avatarDisk)->delete($user->profile_photo_path);
+
+ $user->forceFill([
+ 'profile_photo_path' => null,
+ ])->save();
+ }
+ }
+}
diff --git a/Services/AvatarInitialsService.php b/Services/AvatarInitialsService.php
new file mode 100644
index 0000000..df045eb
--- /dev/null
+++ b/Services/AvatarInitialsService.php
@@ -0,0 +1,124 @@
+getAvatarColor($name);
+ $background = ltrim(self::AVATAR_BACKGROUND, '#');
+ $size = ($this->avatarWidth + $this->avatarHeight) / 2;
+ $initials = self::getInitials($name);
+ $cacheKey = "avatar-{$initials}-{$color}-{$background}-{$size}";
+ $path = "{$this->initialAvatarDir}/{$cacheKey}.png";
+ $storagePath = storage_path("app/public/{$path}");
+
+ if (Storage::disk($this->avatarDisk)->exists($path)) {
+ return response()->file($storagePath);
+ }
+
+ $image = $this->createAvatarImage($name, $color, self::AVATAR_BACKGROUND, $size);
+ Storage::disk($this->avatarDisk)->put($path, $image->toPng(indexed: true));
+
+ return response()->file($storagePath);
+ }
+
+ /**
+ * Crea la imagen del avatar con las iniciales.
+ *
+ * @param string $name Nombre completo.
+ * @param string $color Color del texto.
+ * @param string $background Color de fondo.
+ * @param int $size Tamaño de la imagen.
+ *
+ * @return \Intervention\Image\Image La imagen generada.
+ */
+ protected function createAvatarImage($name, $color, $background, $size)
+ {
+ $driver = config('image.driver', 'gd');
+ $manager = new ImageManager($driver);
+ $initials = self::getInitials($name);
+ $fontPath = __DIR__ . '/../storage/fonts/OpenSans-Bold.ttf';
+
+ $image = $manager->create($size, $size)
+ ->fill($background);
+
+ $image->text(
+ $initials,
+ $size / 2,
+ $size / 2,
+ function (FontFactory $font) use ($color, $size, $fontPath) {
+ $font->file($fontPath);
+ $font->size($size * 0.4);
+ $font->color($color);
+ $font->align('center');
+ $font->valign('middle');
+ }
+ );
+
+ return $image;
+ }
+
+ /**
+ * Calcula las iniciales a partir del nombre.
+ *
+ * @param string $name Nombre completo.
+ *
+ * @return string Iniciales en mayúsculas.
+ */
+ public static function getInitials($name)
+ {
+ if (empty($name)) {
+ return 'NA';
+ }
+
+ $initials = implode('', array_map(function ($word) {
+ return mb_substr($word, 0, 1);
+ }, explode(' ', $name)));
+
+ return strtoupper(substr($initials, 0, self::INITIAL_MAX_LENGTH));
+ }
+
+ /**
+ * Selecciona un color basado en el nombre.
+ *
+ * @param string $name Nombre del usuario.
+ *
+ * @return string Color seleccionado.
+ */
+ public function getAvatarColor($name)
+ {
+ // Por ejemplo, se puede basar en la suma de los códigos ASCII de las letras del nombre
+ $hash = array_sum(array_map('ord', str_split($name)));
+
+ return self::AVATAR_COLORS[$hash % count(self::AVATAR_COLORS)];
+ }
+}
diff --git a/Services/CacheConfigService.php b/Services/CacheConfigService.php
new file mode 100644
index 0000000..0d16a91
--- /dev/null
+++ b/Services/CacheConfigService.php
@@ -0,0 +1,235 @@
+ $this->getCacheConfig(),
+ 'session' => $this->getSessionConfig(),
+ 'database' => $this->getDatabaseConfig(),
+ 'driver' => $this->getDriverVersion(),
+ 'memcachedInUse' => $this->isDriverInUse('memcached'),
+ 'redisInUse' => $this->isDriverInUse('redis'),
+ ];
+ }
+
+
+ private function getCacheConfig(): array
+ {
+ $cacheConfig = Config::get('cache');
+ $driver = $cacheConfig['default'];
+
+ switch ($driver) {
+ case 'redis':
+ $connection = config('database.redis.cache');
+ $cacheConfig['host'] = $connection['host'] ?? 'localhost';
+ $cacheConfig['database'] = $connection['database'] ?? 'N/A';
+ break;
+
+ case 'database':
+ $connection = config('database.connections.' . config('cache.stores.database.connection'));
+ $cacheConfig['host'] = $connection['host'] ?? 'localhost';
+ $cacheConfig['database'] = $connection['database'] ?? 'N/A';
+ break;
+
+ case 'memcached':
+ $servers = config('cache.stores.memcached.servers');
+ $cacheConfig['host'] = $servers[0]['host'] ?? 'localhost';
+ $cacheConfig['database'] = 'N/A';
+ break;
+
+ case 'file':
+ $cacheConfig['host'] = storage_path('framework/cache/data');
+ $cacheConfig['database'] = 'N/A';
+ break;
+
+ default:
+ $cacheConfig['host'] = 'N/A';
+ $cacheConfig['database'] = 'N/A';
+ break;
+ }
+
+ return $cacheConfig;
+ }
+
+ private function getSessionConfig(): array
+ {
+ $sessionConfig = Config::get('session');
+ $driver = $sessionConfig['driver'];
+
+ switch ($driver) {
+ case 'redis':
+ $connection = config('database.redis.sessions');
+ $sessionConfig['host'] = $connection['host'] ?? 'localhost';
+ $sessionConfig['database'] = $connection['database'] ?? 'N/A';
+ break;
+
+ case 'database':
+ $connection = config('database.connections.' . $sessionConfig['connection']);
+ $sessionConfig['host'] = $connection['host'] ?? 'localhost';
+ $sessionConfig['database'] = $connection['database'] ?? 'N/A';
+ break;
+
+ case 'memcached':
+ $servers = config('cache.stores.memcached.servers');
+ $sessionConfig['host'] = $servers[0]['host'] ?? 'localhost';
+ $sessionConfig['database'] = 'N/A';
+ break;
+
+ case 'file':
+ $sessionConfig['host'] = storage_path('framework/sessions');
+ $sessionConfig['database'] = 'N/A';
+ break;
+
+ default:
+ $sessionConfig['host'] = 'N/A';
+ $sessionConfig['database'] = 'N/A';
+ break;
+ }
+
+ return $sessionConfig;
+ }
+
+ private function getDatabaseConfig(): array
+ {
+ $databaseConfig = Config::get('database');
+ $connection = $databaseConfig['default'];
+
+ $connectionConfig = config('database.connections.' . $connection);
+ $databaseConfig['host'] = $connectionConfig['host'] ?? 'localhost';
+ $databaseConfig['database'] = $connectionConfig['database'] ?? 'N/A';
+
+ return $databaseConfig;
+ }
+
+
+ private function getDriverVersion(): array
+ {
+ $drivers = [];
+ $defaultDatabaseDriver = config('database.default'); // Obtén el driver predeterminado
+
+ switch ($defaultDatabaseDriver) {
+ case 'mysql':
+ case 'mariadb':
+ $drivers['mysql'] = [
+ 'version' => $this->getMySqlVersion(),
+ 'details' => config("database.connections.$defaultDatabaseDriver"),
+ ];
+
+ $drivers['mariadb'] = $drivers['mysql'];
+
+ case 'pgsql':
+ $drivers['pgsql'] = [
+ 'version' => $this->getPgSqlVersion(),
+ 'details' => config("database.connections.pgsql"),
+ ];
+ break;
+
+ case 'sqlsrv':
+ $drivers['sqlsrv'] = [
+ 'version' => $this->getSqlSrvVersion(),
+ 'details' => config("database.connections.sqlsrv"),
+ ];
+ break;
+
+ default:
+ $drivers['unknown'] = [
+ 'version' => 'No disponible',
+ 'details' => 'Driver no identificado',
+ ];
+ break;
+ }
+
+ // Opcional: Agrega detalles de Redis y Memcached si están en uso
+ if ($this->isDriverInUse('redis')) {
+ $drivers['redis'] = [
+ 'version' => $this->getRedisVersion(),
+ ];
+ }
+
+ if ($this->isDriverInUse('memcached')) {
+ $drivers['memcached'] = [
+ 'version' => $this->getMemcachedVersion(),
+ ];
+ }
+
+ return $drivers;
+ }
+
+ private function getMySqlVersion(): string
+ {
+ try {
+ $version = DB::selectOne('SELECT VERSION() as version');
+ return $version->version ?? 'No disponible';
+ } catch (\Exception $e) {
+ return 'Error: ' . $e->getMessage();
+ }
+ }
+
+ private function getPgSqlVersion(): string
+ {
+ try {
+ $version = DB::selectOne("SHOW server_version");
+ return $version->server_version ?? 'No disponible';
+ } catch (\Exception $e) {
+ return 'Error: ' . $e->getMessage();
+ }
+ }
+
+ private function getSqlSrvVersion(): string
+ {
+ try {
+ $version = DB::selectOne("SELECT @@VERSION as version");
+ return $version->version ?? 'No disponible';
+ } catch (\Exception $e) {
+ return 'Error: ' . $e->getMessage();
+ }
+ }
+
+ private function getMemcachedVersion(): string
+ {
+ try {
+ $memcached = new \Memcached();
+ $memcached->addServer(
+ Config::get('cache.stores.memcached.servers.0.host'),
+ Config::get('cache.stores.memcached.servers.0.port')
+ );
+
+ $stats = $memcached->getStats();
+ foreach ($stats as $serverStats) {
+ return $serverStats['version'] ?? 'No disponible';
+ }
+
+ return 'No disponible';
+ } catch (\Exception $e) {
+ return 'Error: ' . $e->getMessage();
+ }
+ }
+
+ private function getRedisVersion(): string
+ {
+ try {
+ $info = Redis::info();
+ return $info['redis_version'] ?? 'No disponible';
+ } catch (\Exception $e) {
+ return 'Error: ' . $e->getMessage();
+ }
+ }
+
+
+ protected function isDriverInUse(string $driver): bool
+ {
+ return in_array($driver, [
+ Config::get('cache.default'),
+ Config::get('session.driver'),
+ Config::get('queue.default'),
+ ]);
+ }
+}
diff --git a/Services/CacheManagerService.php b/Services/CacheManagerService.php
new file mode 100644
index 0000000..6db4419
--- /dev/null
+++ b/Services/CacheManagerService.php
@@ -0,0 +1,389 @@
+driver = $driver ?? config('cache.default');
+ }
+
+ /**
+ * Obtiene estadísticas de caché para el driver especificado.
+ */
+ public function getCacheStats(string $driver = null): array
+ {
+ $driver = $driver ?? $this->driver;
+
+ if (!$this->isSupportedDriver($driver)) {
+ return $this->response('warning', 'Driver no soportado o no configurado.');
+ }
+
+ try {
+ return match ($driver) {
+ 'database' => $this->_getDatabaseStats(),
+ 'file' => $this->_getFilecacheStats(),
+ 'redis' => $this->_getRedisStats(),
+ 'memcached' => $this->_getMemcachedStats(),
+ default => $this->response('info', 'No hay estadísticas disponibles para este driver.'),
+ };
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al obtener estadísticas: ' . $e->getMessage());
+ }
+ }
+
+ public function clearCache(string $driver = null): array
+ {
+ $driver = $driver ?? $this->driver;
+
+ if (!$this->isSupportedDriver($driver)) {
+ return $this->response('warning', 'Driver no soportado o no configurado.');
+ }
+
+ try {
+ switch ($driver) {
+ case 'redis':
+ $keysCleared = $this->clearRedisCache();
+
+ return $keysCleared
+ ? $this->response('warning', 'Se ha purgado toda la caché de Redis.')
+ : $this->response('info', 'No se encontraron claves en Redis para eliminar.');
+
+ case 'memcached':
+ $keysCleared = $this->clearMemcachedCache();
+
+ return $keysCleared
+ ? $this->response('warning', 'Se ha purgado toda la caché de Memcached.')
+ : $this->response('info', 'No se encontraron claves en Memcached para eliminar.');
+
+ case 'database':
+ $rowsDeleted = $this->clearDatabaseCache();
+
+ return $rowsDeleted
+ ? $this->response('warning', 'Se ha purgado toda la caché almacenada en la base de datos.')
+ : $this->response('info', 'No se encontraron registros en la caché de la base de datos.');
+
+ case 'file':
+ $filesDeleted = $this->clearFilecache();
+
+ return $filesDeleted
+ ? $this->response('warning', 'Se ha purgado toda la caché de archivos.')
+ : $this->response('info', 'No se encontraron archivos en la caché para eliminar.');
+
+ default:
+ Cache::flush();
+
+ return $this->response('warning', 'Caché purgada.');
+ }
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al limpiar la caché: ' . $e->getMessage());
+ }
+ }
+
+ public function getRedisStats()
+ {
+ try {
+ if (!Redis::ping()) {
+ return $this->response('warning', 'No se puede conectar con el servidor Redis.');
+ }
+
+ $info = Redis::info();
+
+ $databases = $this->getRedisDatabases();
+
+ $redisInfo = [
+ 'server' => config('database.redis.default.host'),
+ 'redis_version' => $info['redis_version'] ?? 'N/A',
+ 'os' => $info['os'] ?? 'N/A',
+ 'tcp_port' => $info['tcp_port'] ?? 'N/A',
+ 'connected_clients' => $info['connected_clients'] ?? 'N/A',
+ 'blocked_clients' => $info['blocked_clients'] ?? 'N/A',
+ 'maxmemory' => $info['maxmemory'] ?? 0,
+ 'used_memory_human' => $info['used_memory_human'] ?? 'N/A',
+ 'used_memory_peak' => $info['used_memory_peak'] ?? 'N/A',
+ 'used_memory_peak_human' => $info['used_memory_peak_human'] ?? 'N/A',
+ 'total_system_memory' => $info['total_system_memory'] ?? 0,
+ 'total_system_memory_human' => $info['total_system_memory_human'] ?? 'N/A',
+ 'maxmemory_human' => $info['maxmemory_human'] !== '0B' ? $info['maxmemory_human'] : 'Sin Límite',
+ 'total_connections_received' => number_format($info['total_connections_received']) ?? 'N/A',
+ 'total_commands_processed' => number_format($info['total_commands_processed']) ?? 'N/A',
+ 'maxmemory_policy' => $info['maxmemory_policy'] ?? 'N/A',
+ 'role' => $info['role'] ?? 'N/A',
+ 'cache_database' => '',
+ 'sessions_database' => '',
+ 'general_database' => ',',
+ 'keys' => $databases['total_keys'],
+ 'used_memory' => $info['used_memory'] ?? 0,
+ 'uptime' => gmdate('H\h i\m s\s', $info['uptime_in_seconds'] ?? 0),
+ 'databases' => $databases,
+ ];
+
+ return $this->response('success', 'Se a recargado las estadísticas de Redis.', ['info' => $redisInfo]);
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al conectar con el servidor Redis: ' . Redis::getLastError());
+ }
+ }
+
+ public function getMemcachedStats()
+ {
+ try {
+ $memcachedStats = [];
+
+ // Crear instancia del cliente Memcached
+ $memcached = new \Memcached();
+ $memcached->addServer(config('memcached.host'), config('memcached.port'));
+
+ // Obtener estadísticas del servidor
+ $stats = $memcached->getStats();
+
+ foreach ($stats as $server => $data) {
+ $server = explode(':', $server);
+
+ $memcachedStats[] = [
+ 'server' => $server[0],
+ 'tcp_port' => $server[1],
+ 'uptime' => $data['uptime'] ?? 'N/A',
+ 'version' => $data['version'] ?? 'N/A',
+ 'libevent' => $data['libevent'] ?? 'N/A',
+ 'max_connections' => $data['max_connections'] ?? 0,
+ 'total_connections' => $data['total_connections'] ?? 0,
+ 'rejected_connections' => $data['rejected_connections'] ?? 0,
+ 'curr_items' => $data['curr_items'] ?? 0, // Claves almacenadas
+ 'bytes' => $data['bytes'] ?? 0, // Memoria usada
+ 'limit_maxbytes' => $data['limit_maxbytes'] ?? 0, // Memoria máxima
+ 'cmd_get' => $data['cmd_get'] ?? 0, // Comandos GET ejecutados
+ 'cmd_set' => $data['cmd_set'] ?? 0, // Comandos SET ejecutados
+ 'get_hits' => $data['get_hits'] ?? 0, // GET exitosos
+ 'get_misses' => $data['get_misses'] ?? 0, // GET fallidos
+ 'evictions' => $data['evictions'] ?? 0, // Claves expulsadas
+ 'bytes_read' => $data['bytes_read'] ?? 0, // Bytes leídos
+ 'bytes_written' => $data['bytes_written'] ?? 0, // Bytes escritos
+ 'total_items' => $data['total_items'] ?? 0,
+ ];
+ }
+
+ return $this->response('success', 'Se a recargado las estadísticas de Memcached.', ['info' => $memcachedStats]);
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al conectar con el servidor Memcached: ' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * Obtiene estadísticas para caché en base de datos.
+ */
+ private function _getDatabaseStats(): array
+ {
+ try {
+ $recordCount = DB::table('cache')->count();
+ $tableInfo = DB::select("SHOW TABLE STATUS WHERE Name = 'cache'");
+
+ $memory_usage = isset($tableInfo[0]) ? $this->formatBytes($tableInfo[0]->Data_length + $tableInfo[0]->Index_length) : 'N/A';
+
+ return $this->response('success', 'Se ha recargado la información de la caché de base de datos.', ['item_count' => $recordCount, 'memory_usage' => $memory_usage]);
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al obtener estadísticas de la base de datos: ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * Obtiene estadísticas para caché en archivos.
+ */
+ private function _getFilecacheStats(): array
+ {
+ try {
+ $cachePath = config('cache.stores.file.path');
+ $files = glob($cachePath . '/*');
+
+ $memory_usage = $this->formatBytes(array_sum(array_map('filesize', $files)));
+
+ return $this->response('success', 'Se ha recargado la información de la caché de archivos.', ['item_count' => count($files), 'memory_usage' => $memory_usage]);
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al obtener estadísticas de archivos: ' . $e->getMessage());
+ }
+ }
+
+ private function _getRedisStats()
+ {
+ try {
+ $prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
+
+ $info = Redis::info();
+ $keys = Redis::connection('cache')->keys($prefix . '*');
+
+ $memory_usage = $this->formatBytes($info['used_memory'] ?? 0);
+
+ return $this->response('success', 'Se ha recargado la información de la caché de Redis.', ['item_count' => count($keys), 'memory_usage' => $memory_usage]);
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al obtener estadísticas de Redis: ' . $e->getMessage());
+ }
+ }
+
+ public function _getMemcachedStats(): array
+ {
+ try {
+ // Obtener estadísticas generales del servidor
+ $stats = Cache::getStore()->getMemcached()->getStats();
+
+ if (empty($stats)) {
+ return $this->response('error', 'No se pudieron obtener las estadísticas del servidor Memcached.', ['item_count' => 0, 'memory_usage' => 0]);
+ }
+
+ // Usar el primer servidor configurado (en la mayoría de los casos hay uno)
+ $serverStats = array_shift($stats);
+
+ return $this->response(
+ 'success',
+ 'Estadísticas del servidor Memcached obtenidas correctamente.',
+ [
+ 'item_count' => $serverStats['curr_items'] ?? 0, // Número total de claves
+ 'memory_usage' => $this->formatBytes($serverStats['bytes'] ?? 0), // Memoria usada
+ 'max_memory' => $this->formatBytes($serverStats['limit_maxbytes'] ?? 0), // Memoria máxima asignada
+ ]
+ );
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al obtener estadísticas de Memcached: ' . $e->getMessage());
+ }
+ }
+
+ private function getRedisDatabases(): array
+ {
+ // Verificar si Redis está en uso
+ $isRedisUsed = collect([
+ config('cache.default'),
+ config('session.driver'),
+ config('queue.default'),
+ ])->contains('redis');
+
+ if (!$isRedisUsed) {
+ return []; // Si Redis no está en uso, devolver un arreglo vacío
+ }
+
+ // Configuraciones de bases de datos de Redis según su uso
+ $databases = [
+ 'default' => config('database.redis.default.database', 0), // REDIS_DB
+ 'cache' => config('database.redis.cache.database', 0), // REDIS_CACHE_DB
+ 'sessions' => config('database.redis.sessions.database', 0), // REDIS_SESSION_DB
+ ];
+
+ $result = [];
+ $totalKeys = 0;
+
+ // Recorrer solo las bases configuradas y activas
+ foreach ($databases as $type => $db) {
+ Redis::select($db); // Seleccionar la base de datos
+
+ $keys = Redis::dbsize(); // Contar las claves en la base
+
+ if ($keys > 0) {
+ $result[$type] = [
+ 'database' => $db,
+ 'keys' => $keys,
+ ];
+
+ $totalKeys += $keys;
+ }
+ }
+
+ if (!empty($result)) {
+ $result['total_keys'] = $totalKeys;
+ }
+
+ return $result;
+ }
+
+
+ private function clearDatabaseCache(): bool
+ {
+ $count = DB::table(config('cache.stores.database.table'))->count();
+
+ if ($count > 0) {
+ DB::table(config('cache.stores.database.table'))->truncate();
+ return true;
+ }
+
+ return false;
+ }
+
+ private function clearFilecache(): bool
+ {
+ $cachePath = config('cache.stores.file.path');
+ $files = glob($cachePath . '/*');
+
+ if (!empty($files)) {
+ File::deleteDirectory($cachePath);
+ return true;
+ }
+
+ return false;
+ }
+
+ private function clearRedisCache(): bool
+ {
+ $prefix = config('cache.prefix', '');
+ $keys = Redis::connection('cache')->keys($prefix . '*');
+
+ if (!empty($keys)) {
+ Redis::connection('cache')->flushdb();
+
+ // Simulate cache clearing delay
+ sleep(1);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private function clearMemcachedCache(): bool
+ {
+ // Obtener el cliente Memcached directamente
+ $memcached = Cache::store('memcached')->getStore()->getMemcached();
+
+ // Ejecutar flush para eliminar todo
+ if ($memcached->flush()) {
+ // Simulate cache clearing delay
+ sleep(1);
+
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Verifica si un driver es soportado.
+ */
+ private function isSupportedDriver(string $driver): bool
+ {
+ return in_array($driver, ['redis', 'memcached', 'database', 'file']);
+ }
+
+ /**
+ * Convierte bytes en un formato legible.
+ */
+ private function formatBytes($bytes)
+ {
+ $sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ $factor = floor((strlen($bytes) - 1) / 3);
+
+ return sprintf('%.2f', $bytes / pow(1024, $factor)) . ' ' . $sizes[$factor];
+ }
+
+ /**
+ * Genera una respuesta estandarizada.
+ */
+ private function response(string $status, string $message, array $data = []): array
+ {
+ return array_merge(compact('status', 'message'), $data);
+ }
+}
diff --git a/Services/GlobalSettingsService.php b/Services/GlobalSettingsService.php
new file mode 100644
index 0000000..404fd66
--- /dev/null
+++ b/Services/GlobalSettingsService.php
@@ -0,0 +1,225 @@
+ $key],
+ ['value' => trim($value)]
+ );
+
+ return $setting->save();
+ }
+
+ /**
+ * Carga y sobrescribe las configuraciones del sistema.
+ */
+ public function loadSystemConfig(): void
+ {
+ try {
+ if (!Schema::hasTable('migrations')) {
+ // Base de datos no inicializada: usar valores predeterminados
+ $config = $this->getDefaultSystemConfig();
+ } else {
+ // Cargar configuración desde la caché o base de datos
+ $config = Cache::remember('global_system_config', $this->cacheTTL, function () {
+ $settings = Setting::global()
+ ->where('key', 'LIKE', 'config.%')
+ ->pluck('value', 'key')
+ ->toArray();
+
+ return [
+ 'servicesFacebook' => $this->buildServiceConfig($settings, 'config.services.facebook.', 'services.facebook'),
+ 'servicesGoogle' => $this->buildServiceConfig($settings, 'config.services.google.', 'services.google'),
+ 'vuexy' => $this->buildVuexyConfig($settings),
+ ];
+ });
+ }
+
+ // Aplicar configuración al sistema
+ Config::set('services.facebook', $config['servicesFacebook']);
+ Config::set('services.google', $config['servicesGoogle']);
+ Config::set('vuexy', $config['vuexy']);
+ } catch (\Exception $e) {
+ // Manejo silencioso de errores para evitar interrupciones
+ Config::set('services.facebook', config('services.facebook', []));
+ Config::set('services.google', config('services.google', []));
+ Config::set('vuexy', config('vuexy', []));
+ }
+ }
+
+ /**
+ * Devuelve una configuración predeterminada si la base de datos no está inicializada.
+ */
+ private function getDefaultSystemConfig(): array
+ {
+ return [
+ 'servicesFacebook' => config('services.facebook', [
+ 'client_id' => '',
+ 'client_secret' => '',
+ 'redirect' => '',
+ ]),
+ 'servicesGoogle' => config('services.google', [
+ 'client_id' => '',
+ 'client_secret' => '',
+ 'redirect' => '',
+ ]),
+ 'vuexy' => config('vuexy', []),
+ ];
+ }
+
+ /**
+ * Verifica si un bloque de configuraciones está presente.
+ */
+ protected function hasBlockConfig(array $settings, string $blockPrefix): bool
+ {
+ return array_key_exists($blockPrefix, array_filter($settings, fn($key) => str_starts_with($key, $blockPrefix), ARRAY_FILTER_USE_KEY));
+ }
+
+ /**
+ * Construye la configuración de un servicio (Facebook, Google, etc.).
+ */
+ protected function buildServiceConfig(array $settings, string $blockPrefix, string $defaultConfigKey): array
+ {
+ if (!$this->hasBlockConfig($settings, $blockPrefix)) {
+ return [];
+ return config($defaultConfigKey);
+ }
+
+ return [
+ 'client_id' => $settings["{$blockPrefix}client_id"] ?? '',
+ 'client_secret' => $settings["{$blockPrefix}client_secret"] ?? '',
+ 'redirect' => $settings["{$blockPrefix}redirect"] ?? '',
+ ];
+ }
+
+ /**
+ * Construye la configuración personalizada de Vuexy.
+ */
+ protected function buildVuexyConfig(array $settings): array
+ {
+ // Configuración predeterminada del sistema
+ $defaultVuexyConfig = config('vuexy', []);
+
+ // Convertimos las claves planas a un array multidimensional
+ $settingsNested = Arr::undot($settings);
+
+ // Navegamos hasta la parte relevante del array desanidado
+ $vuexySettings = $settingsNested['config']['vuexy'] ?? [];
+
+ // Fusionamos la configuración predeterminada con los valores del sistema
+ $mergedConfig = array_replace_recursive($defaultVuexyConfig, $vuexySettings);
+
+ // Normalizamos los valores booleanos
+ return $this->normalizeBooleanFields($mergedConfig);
+ }
+
+ /**
+ * Normaliza los campos booleanos.
+ */
+ protected function normalizeBooleanFields(array $config): array
+ {
+ $booleanFields = [
+ 'myRTLSupport',
+ 'myRTLMode',
+ 'hasCustomizer',
+ 'displayCustomizer',
+ 'footerFixed',
+ 'menuFixed',
+ 'menuCollapsed',
+ 'showDropdownOnHover',
+ ];
+
+ foreach ($booleanFields as $field) {
+ if (isset($config['vuexy'][$field])) {
+ $config['vuexy'][$field] = (bool) $config['vuexy'][$field];
+ }
+ }
+
+ return $config;
+ }
+
+ /**
+ * Limpia el caché de la configuración del sistema.
+ */
+ public static function clearSystemConfigCache(): void
+ {
+ Cache::forget('global_system_config');
+ }
+
+ /**
+ * Elimina las claves config.vuexy.* y limpia global_system_config
+ */
+ public static function clearVuexyConfig(): void
+ {
+ Setting::where('key', 'LIKE', 'config.vuexy.%')->delete();
+ Cache::forget('global_system_config');
+ }
+
+ /**
+ * Obtiene y sobrescribe la configuración de correo electrónico.
+ */
+ public function getMailSystemConfig(): array
+ {
+ return Cache::remember('mail_system_config', $this->cacheTTL, function () {
+ $settings = Setting::global()
+ ->where('key', 'LIKE', 'mail.%')
+ ->pluck('value', 'key')
+ ->toArray();
+
+ $defaultMailersSmtpVars = config('mail.mailers.smtp');
+
+ return [
+ 'mailers' => [
+ 'smtp' => array_merge($defaultMailersSmtpVars, [
+ 'url' => $settings['mail.mailers.smtp.url'] ?? $defaultMailersSmtpVars['url'],
+ 'host' => $settings['mail.mailers.smtp.host'] ?? $defaultMailersSmtpVars['host'],
+ 'port' => $settings['mail.mailers.smtp.port'] ?? $defaultMailersSmtpVars['port'],
+ 'encryption' => $settings['mail.mailers.smtp.encryption'] ?? 'TLS',
+ 'username' => $settings['mail.mailers.smtp.username'] ?? $defaultMailersSmtpVars['username'],
+ 'password' => isset($settings['mail.mailers.smtp.password']) && !empty($settings['mail.mailers.smtp.password'])
+ ? Crypt::decryptString($settings['mail.mailers.smtp.password'])
+ : $defaultMailersSmtpVars['password'],
+ 'timeout' => $settings['mail.mailers.smtp.timeout'] ?? $defaultMailersSmtpVars['timeout'],
+ ]),
+ ],
+ 'from' => [
+ 'address' => $settings['mail.from.address'] ?? config('mail.from.address'),
+ 'name' => $settings['mail.from.name'] ?? config('mail.from.name'),
+ ],
+ 'reply_to' => [
+ 'method' => $settings['mail.reply_to.method'] ?? config('mail.reply_to.method'),
+ 'email' => $settings['mail.reply_to.email'] ?? config('mail.reply_to.email'),
+ 'name' => $settings['mail.reply_to.name'] ?? config('mail.reply_to.name'),
+ ],
+ ];
+ });
+ }
+
+ /**
+ * Limpia el caché de la configuración de correo electrónico.
+ */
+ public static function clearMailSystemConfigCache(): void
+ {
+ Cache::forget('mail_system_config');
+ }
+
+}
diff --git a/Services/RBACService.php b/Services/RBACService.php
new file mode 100644
index 0000000..f38db6b
--- /dev/null
+++ b/Services/RBACService.php
@@ -0,0 +1,28 @@
+ $perm]);
+ }
+
+ foreach ($rbacData['roles'] as $name => $role) {
+ $roleInstance = Role::updateOrCreate(['name' => $name, 'style' => $role['style']]);
+ $roleInstance->syncPermissions($role['permissions']);
+ }
+ }
+}
diff --git a/Services/SessionManagerService.php b/Services/SessionManagerService.php
new file mode 100644
index 0000000..d57d05f
--- /dev/null
+++ b/Services/SessionManagerService.php
@@ -0,0 +1,153 @@
+driver = $driver ?? config('session.driver');
+ }
+
+ public function getSessionStats(string $driver = null): array
+ {
+ $driver = $driver ?? $this->driver;
+
+ if (!$this->isSupportedDriver($driver))
+ return $this->response('warning', 'Driver no soportado o no configurado.', ['session_count' => 0]);
+
+ try {
+ switch ($driver) {
+ case 'redis':
+ return $this->getRedisStats();
+
+ case 'database':
+ return $this->getDatabaseStats();
+
+ case 'file':
+ return $this->getFileStats();
+
+ default:
+ return $this->response('warning', 'Driver no reconocido.', ['session_count' => 0]);
+ }
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al obtener estadísticas: ' . $e->getMessage(), ['session_count' => 0]);
+ }
+ }
+
+ public function clearSessions(string $driver = null): array
+ {
+ $driver = $driver ?? $this->driver;
+
+ if (!$this->isSupportedDriver($driver)) {
+ return $this->response('warning', 'Driver no soportado o no configurado.');
+ }
+
+ try {
+ switch ($driver) {
+ case 'redis':
+ return $this->clearRedisSessions();
+
+ case 'memcached':
+ Cache::getStore()->flush();
+ return $this->response('success', 'Se eliminó la memoria caché de sesiones en Memcached.');
+
+ case 'database':
+ DB::table('sessions')->truncate();
+ return $this->response('success', 'Se eliminó la memoria caché de sesiones en la base de datos.');
+
+ case 'file':
+ return $this->clearFileSessions();
+
+ default:
+ return $this->response('warning', 'Driver no reconocido.');
+ }
+ } catch (\Exception $e) {
+ return $this->response('danger', 'Error al limpiar las sesiones: ' . $e->getMessage());
+ }
+ }
+
+
+ private function getRedisStats()
+ {
+ $prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
+ $keys = Redis::connection('sessions')->keys($prefix . '*');
+
+ return $this->response('success', 'Se ha recargado la información de la caché de Redis.', ['session_count' => count($keys)]);
+ }
+
+ private function getDatabaseStats(): array
+ {
+ $sessionCount = DB::table('sessions')->count();
+
+ return $this->response('success', 'Se ha recargado la información de la base de datos.', ['session_count' => $sessionCount]);
+ }
+
+ private function getFileStats(): array
+ {
+ $cachePath = config('session.files');
+ $files = glob($cachePath . '/*');
+
+ return $this->response('success', 'Se ha recargado la información de sesiones de archivos.', ['session_count' => count($files)]);
+ }
+
+
+ /**
+ * Limpia sesiones en Redis.
+ */
+ private function clearRedisSessions(): array
+ {
+ $prefix = config('cache.prefix', '');
+ $keys = Redis::connection('sessions')->keys($prefix . '*');
+
+ if (!empty($keys)) {
+ Redis::connection('sessions')->flushdb();
+
+ // Simulate cache clearing delay
+ sleep(1);
+
+ return $this->response('success', 'Se eliminó la memoria caché de sesiones en Redis.');
+ }
+
+ return $this->response('info', 'No se encontraron claves para eliminar en Redis.');
+ }
+
+ /**
+ * Limpia sesiones en archivos.
+ */
+ private function clearFileSessions(): array
+ {
+ $cachePath = config('session.files');
+ $files = glob($cachePath . '/*');
+
+ if (!empty($files)) {
+ foreach ($files as $file) {
+ unlink($file);
+ }
+
+ return $this->response('success', 'Se eliminó la memoria caché de sesiones en archivos.');
+ }
+
+ return $this->response('info', 'No se encontraron sesiones en archivos para eliminar.');
+ }
+
+
+ private function isSupportedDriver(string $driver): bool
+ {
+ return in_array($driver, ['redis', 'memcached', 'database', 'file']);
+ }
+
+ /**
+ * Genera una respuesta estandarizada.
+ */
+ private function response(string $status, string $message, array $data = []): array
+ {
+ return array_merge(compact('status', 'message'), $data);
+ }
+}
diff --git a/Services/VuexyAdminService.php b/Services/VuexyAdminService.php
new file mode 100644
index 0000000..64d3982
--- /dev/null
+++ b/Services/VuexyAdminService.php
@@ -0,0 +1,623 @@
+ 'Inicio',
+ 'route' => 'admin.core.home.index',
+ ];
+
+ private $user;
+
+ public function __construct()
+ {
+ $this->user = Auth::user();
+ $this->vuexySearch = Auth::user() !== null;
+ $this->orientation = config('vuexy.custom.myLayout');
+ }
+
+ /**
+ * Obtiene el menú según el estado del usuario (autenticado o no).
+ */
+ public function getMenu()
+ {
+ // Obtener el menú desde la caché
+ $menu = $this->user === null
+ ? $this->getGuestMenu()
+ : $this->getUserMenu();
+
+ // Marcar la ruta actual como activa
+ $currentRoute = Route::currentRouteName();
+
+ return $this->markActive($menu, $currentRoute);
+ }
+
+ /**
+ * Menú para usuarios no autenticados.dump
+ */
+ private function getGuestMenu()
+ {
+ return Cache::remember('vuexy_menu_guest', now()->addDays(7), function () {
+ return $this->getMenuArray();
+ });
+ }
+
+ /**
+ * Menú para usuarios autenticados.
+ */
+ private function getUserMenu()
+ {
+ Cache::forget("vuexy_menu_user_{$this->user->id}"); // Borrar la caché anterior para actualizarla
+
+ return Cache::remember("vuexy_menu_user_{$this->user->id}", now()->addHours(24), function () {
+ return $this->getMenuArray();
+ });
+ }
+
+ private function markActive($menu, $currentRoute)
+ {
+ foreach ($menu as &$item) {
+ $item['active'] = false;
+
+ // Check if the route matches
+ if (isset($item['route']) && $item['route'] === $currentRoute)
+ $item['active'] = true;
+
+ // Process submenus recursively
+ if (isset($item['submenu']) && !empty($item['submenu'])) {
+ $item['submenu'] = $this->markActive($item['submenu'], $currentRoute);
+
+ // If any submenu is active, mark the parent as active
+ if (collect($item['submenu'])->contains('active', true))
+ $item['active'] = true;
+ }
+ }
+
+ return $menu;
+ }
+
+ /**
+ * Invalida el cache del menú de un usuario.
+ */
+ public static function clearUserMenuCache()
+ {
+ $user = Auth::user();
+
+ if ($user !== null)
+ Cache::forget("vuexy_menu_user_{$user->id}");
+ }
+
+ /**
+ * Invalida el cache del menú de invitados.
+ */
+ public static function clearGuestMenuCache()
+ {
+ Cache::forget('vuexy_menu_guest');
+ }
+
+
+
+
+ public function getSearch()
+ {
+ return $this->vuexySearch;
+ }
+
+ public function getVuexySearchData()
+ {
+ if ($this->user === null)
+ return null;
+
+ $pages = Cache::remember("vuexy_search_user_{$this->user->id}", now()->addDays(7), function () {
+ return $this->cacheVuexySearchData();
+ });
+
+ // Formatear como JSON esperado
+ return [
+ 'pages' => $pages,
+ ];
+ }
+
+ private function cacheVuexySearchData()
+ {
+ $originalMenu = $this->getUserMenu();
+
+ return $this->getPagesSearchMenu($originalMenu);
+ }
+
+ private function getPagesSearchMenu(array $menu, string $parentPath = '')
+ {
+ $formattedMenu = [];
+
+ foreach ($menu as $name => $item) {
+ // Construir la ruta jerárquica (menu / submenu / submenu)
+ $currentPath = $parentPath ? $parentPath . ' / ' . $name : $name;
+
+ // Verificar si el elemento tiene una URL o una ruta
+ $url = $item['url'] ?? (isset($item['route']) && route::has($item['route']) ? route($item['route']) : null);
+
+ // Agregar el elemento al menú formateado
+ if ($url) {
+ $formattedMenu[] = [
+ 'name' => $currentPath, // Usar la ruta completa
+ 'icon' => $item['icon'] ?? 'ti ti-point',
+ 'url' => $url,
+ ];
+ }
+
+ // Si hay un submenú, procesarlo recursivamente
+ if (isset($item['submenu']) && is_array($item['submenu'])) {
+ $formattedMenu = array_merge(
+ $formattedMenu,
+ $this->getPagesSearchMenu($item['submenu'], $currentPath) // Pasar el path acumulado
+ );
+ }
+ }
+
+ return $formattedMenu;
+ }
+
+ public static function clearSearchMenuCache()
+ {
+ $user = Auth::user();
+
+ if ($user !== null)
+ Cache::forget("vuexy_search_user_{$user->id}");
+ }
+
+
+
+
+ public function getQuickLinks()
+ {
+ if ($this->user === null)
+ return null;
+
+ // Recuperar enlaces desde la caché
+ $quickLinks = Cache::remember("vuexy_quick_links_user_{$this->user->id}", now()->addDays(7), function () {
+ return $this->cacheQuickLinks();
+ });
+
+ // Verificar si la ruta actual está en la lista
+ $currentRoute = Route::currentRouteName();
+ $currentPageInList = $this->isCurrentPageInList($quickLinks, $currentRoute);
+
+ // Agregar la verificación al resultado
+ $quickLinks['current_page_in_list'] = $currentPageInList;
+
+ return $quickLinks;
+ }
+
+ private function cacheQuickLinks()
+ {
+ $originalMenu = $this->getUserMenu();
+
+ $quickLinks = [];
+
+ $quicklinks = Setting::where('user_id', Auth::user()->id)
+ ->where('key', 'quicklinks')
+ ->first();
+
+ $this->quicklinksRouteNames = $quicklinks ? json_decode($quicklinks->value, true) : [];
+
+ // Ordenar y generar los quickLinks según el orden del menú
+ $this->collectQuickLinksFromMenu($originalMenu, $quickLinks);
+
+ $quickLinksData = [
+ 'totalLinks' => count($quickLinks),
+ 'rows' => array_chunk($quickLinks, 2), // Agrupar los atajos en filas de dos
+ ];
+
+ return $quickLinksData;
+ }
+
+ private function collectQuickLinksFromMenu(array $menu, array &$quickLinks, string $parentTitle = null)
+ {
+ foreach ($menu as $title => $item) {
+ // Verificar si el elemento está en la lista de quicklinksRouteNames
+ if (isset($item['route']) && in_array($item['route'], $this->quicklinksRouteNames)) {
+ $quickLinks[] = [
+ 'title' => $title,
+ 'subtitle' => $parentTitle ?? env('APP_NAME'),
+ 'icon' => $item['icon'] ?? 'ti ti-point',
+ 'url' => isset($item['route']) ? route($item['route']) : ($item['url'] ?? '#'),
+ 'route' => $item['route'],
+ ];
+ }
+
+ // Si tiene submenú, procesarlo recursivamente
+ if (isset($item['submenu']) && is_array($item['submenu'])) {
+ $this->collectQuickLinksFromMenu(
+ $item['submenu'],
+ $quickLinks,
+ $title // Pasar el título actual como subtítulo
+ );
+ }
+ }
+ }
+
+ /**
+ * Verifica si la ruta actual existe en la lista de enlaces.
+ */
+ private function isCurrentPageInList(array $quickLinks, string $currentRoute): bool
+ {
+ foreach ($quickLinks['rows'] as $row) {
+ foreach ($row as $link) {
+ if (isset($link['route']) && $link['route'] === $currentRoute) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static function clearQuickLinksCache()
+ {
+ $user = Auth::user();
+
+ if ($user !== null)
+ Cache::forget("vuexy_quick_links_user_{$user->id}");
+ }
+
+
+
+
+ public function getNotifications()
+ {
+ if ($this->user === null)
+ return null;
+
+ return Cache::remember("vuexy_notifications_user_{$this->user->id}", now()->addHours(4), function () {
+ return $this->cacheNotifications();
+ });
+ }
+
+ private function cacheNotifications()
+ {
+ return "
+
+
+
+
+
+
+
+ ";
+ }
+
+ public static function clearNotificationsCache()
+ {
+ $user = Auth::user();
+
+ if ($user !== null)
+ Cache::forget("vuexy_notifications_user_{$user->id}");
+ }
+
+
+
+
+ public function getBreadcrumbs()
+ {
+ $originalMenu = $this->user === null
+ ? $this->getGuestMenu()
+ : $this->getUserMenu();
+
+ // Lógica para construir los breadcrumbs
+ $breadcrumbs = $this->findBreadcrumbTrail($originalMenu);
+
+ // Asegurar que el primer elemento siempre sea "Inicio"
+ array_unshift($breadcrumbs, $this->homeRoute);
+
+ return $breadcrumbs;
+ }
+
+ private function findBreadcrumbTrail(array $menu, array $breadcrumbs = []): array
+ {
+ foreach ($menu as $title => $item) {
+ $skipBreadcrumb = isset($item['breadcrumbs']) && $item['breadcrumbs'] === false;
+
+ $itemRoute = isset($item['route']) ? implode('.', array_slice(explode('.', $item['route']), 0, -1)): '';
+ $currentRoute = implode('.', array_slice(explode('.', Route::currentRouteName()), 0, -1));
+
+ if ($itemRoute === $currentRoute) {
+ if (!$skipBreadcrumb) {
+ $breadcrumbs[] = [
+ 'name' => $title,
+ 'active' => true,
+ ];
+ }
+
+ return $breadcrumbs;
+ }
+
+ if (isset($item['submenu']) && is_array($item['submenu'])) {
+ $newBreadcrumbs = $breadcrumbs;
+
+ if (!$skipBreadcrumb)
+ $newBreadcrumbs[] = [
+ 'name' => $title,
+ 'route' => $item['route'] ?? null,
+ ];
+
+ $found = $this->findBreadcrumbTrail($item['submenu'], $newBreadcrumbs);
+
+ if ($found)
+ return $found;
+ }
+ }
+
+ return [];
+ }
+
+
+
+
+ private function getMenuArray()
+ {
+ $configMenu = config('vuexy_menu');
+
+ return $this->filterMenu($configMenu);
+ }
+
+ private function filterMenu(array $menu)
+ {
+ $filteredMenu = [];
+
+ foreach ($menu as $key => $item) {
+ // Evaluar permisos con Spatie y eliminar elementos no autorizados
+ if (isset($item['can']) && !$this->userCan($item['can'])) {
+ continue;
+ }
+
+ if (isset($item['canNot']) && $this->userCannot($item['canNot'])) {
+ continue;
+ }
+
+ // Si tiene un submenú, filtrarlo recursivamente
+ if (isset($item['submenu'])) {
+ $item['submenu'] = $this->filterMenu($item['submenu']);
+
+ // Si el submenú queda vacío, eliminar el menú
+ if (empty($item['submenu'])) {
+ continue;
+ }
+ }
+
+ // Removemos los atributos 'can' y 'canNot' del resultado final
+ unset($item['can'], $item['canNot']);
+
+ if(isset($item['route']) && route::has($item['route'])){
+ $item['url'] = route($item['route'])?? '';
+ }
+
+ // Agregar elemento filtrado al menú resultante
+ $filteredMenu[$key] = $item;
+ }
+
+ return $filteredMenu;
+ }
+
+ private function userCan($permissions)
+ {
+ if (is_array($permissions)) {
+ foreach ($permissions as $permission) {
+ if (Gate::allows($permission)) {
+ return true; // Si tiene al menos un permiso, lo mostramos
+ }
+ }
+ return true;
+ }
+
+ return Gate::allows($permissions);
+ }
+
+ private function userCannot($permissions)
+ {
+ if (is_array($permissions)) {
+ foreach ($permissions as $permission) {
+ if (Gate::denies($permission)) {
+ return true; // Si se le ha denegado al menos un permiso, lo ocultamos
+ }
+ }
+ return false;
+ }
+
+ return Gate::denies($permissions);
+ }
+}
diff --git a/composer.json b/composer.json
index 3f3d1f3..7fe74eb 100644
--- a/composer.json
+++ b/composer.json
@@ -1,40 +1,28 @@
{
- "name": "koneko/laravel-vuexy-admin-module",
- "description": "Base modular para proyectos Laravel altamente personalizados.",
+ "name": "koneko/laravel-vuexy-admin",
+ "description": "Laravel Vuexy Admin, un modulo de administracion optimizado para México.",
+ "keywords": ["laravel", "koneko", "framework", "vuexy", "admin", "mexico"],
"type": "library",
"license": "MIT",
"require": {
"php": "^8.2",
- "intervention/image-laravel": "^1.3",
+ "intervention/image-laravel": "^1.4",
+ "laravel/framework": "^11.31",
"laravel/fortify": "^1.25",
- "laravel/framework": "^11.0",
"laravel/sanctum": "^4.0",
- "laravel/tinker": "^2.9",
"livewire/livewire": "^3.5",
- "maatwebsite/excel": "^3.1",
"owen-it/laravel-auditing": "^13.6",
- "spatie/laravel-permission": "^6.10",
- "yajra/laravel-datatables-oracle": "^11.0"
- },
- "require-dev": {
- "barryvdh/laravel-debugbar": "^3.14",
- "fakerphp/faker": "^1.23",
- "laravel/pint": "^1.13",
- "laravel/sail": "^1.26",
- "mockery/mockery": "^1.6",
- "nunomaduro/collision": "^8.0",
- "phpunit/phpunit": "^11.0",
- "spatie/laravel-ignition": "^2.4"
+ "spatie/laravel-permission": "^6.10"
},
"autoload": {
"psr-4": {
- "Koneko\\VuexyAdminModule\\": "src/"
+ "Koneko\\VuexyAdmin\\": ""
}
},
"extra": {
"laravel": {
"providers": [
- "Koneko\\VuexyAdminModule\\BaseServiceProvider"
+ "Koneko\\VuexyAdmin\\Providers\\VuexyAdminServiceProvider"
]
}
},
@@ -43,5 +31,11 @@
"name": "Arturo Corro Pacheco",
"email": "arturo@koneko.mx"
}
- ]
+ ],
+ "support": {
+ "source": "https://github.com/koneko-mx/laravel-vuexy-admin",
+ "issues": "https://github.com/koneko-mx/laravel-vuexy-admin/issues"
+ },
+ "minimum-stability": "stable",
+ "prefer-stable": true
}
diff --git a/config/fortify.php b/config/fortify.php
new file mode 100644
index 0000000..9aec61d
--- /dev/null
+++ b/config/fortify.php
@@ -0,0 +1,159 @@
+ 'web',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Fortify Password Broker
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify which password broker Fortify can use when a user
+ | is resetting their password. This configured value should match one
+ | of your password brokers setup in your "auth" configuration file.
+ |
+ */
+
+ 'passwords' => 'users',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Username / Email
+ |--------------------------------------------------------------------------
+ |
+ | This value defines which model attribute should be considered as your
+ | application's "username" field. Typically, this might be the email
+ | address of the users but you are free to change this value here.
+ |
+ | Out of the box, Fortify expects forgot password and reset password
+ | requests to have a field named 'email'. If the application uses
+ | another name for the field you may define it below as needed.
+ |
+ */
+
+ 'username' => 'email',
+
+ 'email' => 'email',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Lowercase Usernames
+ |--------------------------------------------------------------------------
+ |
+ | This value defines whether usernames should be lowercased before saving
+ | them in the database, as some database system string fields are case
+ | sensitive. You may disable this for your application if necessary.
+ |
+ */
+
+ 'lowercase_usernames' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Home Path
+ |--------------------------------------------------------------------------
+ |
+ | Here you may configure the path where users will get redirected during
+ | authentication or password reset when the operations are successful
+ | and the user is authenticated. You are free to change this value.
+ |
+ */
+
+ 'home' => '/admin',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Fortify Routes Prefix / Subdomain
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify which prefix Fortify will assign to all the routes
+ | that it registers with the application. If necessary, you may change
+ | subdomain under which all of the Fortify routes will be available.
+ |
+ */
+
+ 'prefix' => '',
+
+ 'domain' => null,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Fortify Routes Middleware
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify which middleware Fortify will assign to the routes
+ | that it registers with the application. If necessary, you may change
+ | these middleware but typically this provided default is preferred.
+ |
+ */
+
+ 'middleware' => ['web'],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Rate Limiting
+ |--------------------------------------------------------------------------
+ |
+ | By default, Fortify will throttle logins to five requests per minute for
+ | every email and IP address combination. However, if you would like to
+ | specify a custom rate limiter to call then you may specify it here.
+ |
+ */
+
+ 'limiters' => [
+ 'login' => 'login',
+ 'two-factor' => 'two-factor',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Register View Routes
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify if the routes returning views should be disabled as
+ | you may not need them when building your own application. This may be
+ | especially true if you're writing a custom single-page application.
+ |
+ */
+
+ 'views' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Features
+ |--------------------------------------------------------------------------
+ |
+ | Some of the Fortify features are optional. You may disable the features
+ | by removing them from this array. You're free to only remove some of
+ | these features or you can even remove all of these if you need to.
+ |
+ */
+
+ 'features' => [
+ Features::registration(),
+ Features::resetPasswords(),
+ Features::emailVerification(),
+ Features::updateProfileInformation(),
+ Features::updatePasswords(),
+ Features::twoFactorAuthentication([
+ 'confirm' => true,
+ 'confirmPassword' => true,
+ 'window' => 1,
+ ]),
+ ],
+
+];
diff --git a/config/image.php b/config/image.php
new file mode 100644
index 0000000..c6c0ebb
--- /dev/null
+++ b/config/image.php
@@ -0,0 +1,42 @@
+ \Intervention\Image\Drivers\Imagick\Driver::class,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Configuration Options
+ |--------------------------------------------------------------------------
+ |
+ | These options control the behavior of Intervention Image.
+ |
+ | - "autoOrientation" controls whether an imported image should be
+ | automatically rotated according to any existing Exif data.
+ |
+ | - "decodeAnimation" decides whether a possibly animated image is
+ | decoded as such or whether the animation is discarded.
+ |
+ | - "blendingColor" Defines the default blending color.
+ */
+
+ 'options' => [
+ 'autoOrientation' => true,
+ 'decodeAnimation' => true,
+ 'blendingColor' => 'ffffff',
+ ]
+];
diff --git a/config/koneko.php b/config/koneko.php
new file mode 100644
index 0000000..666842c
--- /dev/null
+++ b/config/koneko.php
@@ -0,0 +1,14 @@
+ "koneko.mx",
+ "appTitle" => "Koneko Soluciones Tecnológicas",
+ "appDescription" => "Koneko Soluciones Tecnológicas",
+ "appLogo" => "../vendor/vuexy-admin/img/logo/koneko-04.png",
+ "appFavicon" => "../vendor/vuexy-admin/img/logo/koneko-04.png",
+ "author" => "arturo@koneko.mx",
+ "creatorName" => "Koneko Soluciones Tecnológicas",
+ "creatorUrl" => "https://koneko.mx",
+ "licenseUrl" => "https://koneko.mx/koneko-admin/licencia",
+ "supportUrl" => "https://koneko.mx/soporte",
+];
diff --git a/config/vuexy.php b/config/vuexy.php
new file mode 100644
index 0000000..ab4980f
--- /dev/null
+++ b/config/vuexy.php
@@ -0,0 +1,36 @@
+ [
+ 'myLayout' => 'horizontal', // Options[String]: vertical(default), horizontal
+ 'myTheme' => 'theme-semi-dark', // Options[String]: theme-default(default), theme-bordered, theme-semi-dark
+ 'myStyle' => 'light', // Options[String]: light(default), dark & system mode
+ 'myRTLSupport' => false, // options[Boolean]: true(default), false // To provide RTLSupport or not
+ 'myRTLMode' => false, // options[Boolean]: false(default), true // To set layout to RTL layout (myRTLSupport must be true for rtl mode)
+ 'hasCustomizer' => true, // options[Boolean]: true(default), false // Display customizer or not THIS WILL REMOVE INCLUDED JS FILE. SO LOCAL STORAGE WON'T WORK
+ 'displayCustomizer' => true, // options[Boolean]: true(default), false // Display customizer UI or not, THIS WON'T REMOVE INCLUDED JS FILE. SO LOCAL STORAGE WILL WORK
+ 'contentLayout' => 'compact', // options[String]: 'compact', 'wide' (compact=container-xxl, wide=container-fluid)
+ 'navbarType' => 'static', // options[String]: 'sticky', 'static', 'hidden' (Only for vertical Layout)
+ 'footerFixed' => false, // options[Boolean]: false(default), true // Footer Fixed
+ 'menuFixed' => false, // options[Boolean]: true(default), false // Layout(menu) Fixed (Only for vertical Layout)
+ 'menuCollapsed' => true, // options[Boolean]: false(default), true // Show menu collapsed, (Only for vertical Layout)
+ 'headerType' => 'static', // options[String]: 'static', 'fixed' (for horizontal layout only)
+ 'showDropdownOnHover' => false, // true, false (for horizontal layout only)
+ 'authViewMode' => 'cover', // Options[String]: cover(default), basic
+ 'maxQuickLinks' => 8, // options[Integer]: 6(default), 8, 10
+ 'customizerControls' => [
+ //'rtl',
+ 'style',
+ 'headerType',
+ 'contentLayout',
+ 'layoutCollapsed',
+ 'layoutNavbarOptions',
+ 'themes',
+ ], // To show/hide customizer options
+ ],
+];
\ No newline at end of file
diff --git a/config/vuexy_menu.php b/config/vuexy_menu.php
new file mode 100644
index 0000000..4fdc3bc
--- /dev/null
+++ b/config/vuexy_menu.php
@@ -0,0 +1,848 @@
+ [
+ 'breadcrumbs' => false,
+ 'icon' => 'menu-icon tf-icons ti ti-home',
+ 'submenu' => [
+ 'Inicio' => [
+ 'route' => 'admin.core.home.index',
+ 'icon' => 'menu-icon tf-icons ti ti-home',
+ ],
+ 'Sitio Web' => [
+ 'url' => env('APP_URL'),
+ 'icon' => 'menu-icon tf-icons ti ti-world-www',
+ ],
+ 'Ajustes' => [
+ 'icon' => 'menu-icon tf-icons ti ti-settings-cog',
+ 'submenu' => [
+ 'Aplicación' => [
+ 'submenu' => [
+ 'Ajustes generales' => [
+ 'route' => 'admin.core.general-settings.index',
+ 'can' => 'admin.core.general-settings.allow',
+ ],
+ 'Ajustes de caché' => [
+ 'route' => 'admin.core.cache-manager.index',
+ 'can' => 'admin.core.cache-manager.view',
+ ],
+ 'Servidor de correo SMTP' => [
+ 'route' => 'admin.core.smtp-settings.index',
+ 'can' => 'admin.core.smtp-settings.allow',
+ ],
+ ],
+ ],
+ 'Empresa' => [
+ 'submenu' => [
+ 'Información general' => [
+ 'route' => 'admin.store-manager.company.index',
+ 'can' => 'admin.store-manager.company.view',
+ ],
+ 'Sucursales' => [
+ 'route' => 'admin.store-manager.stores.index',
+ 'can' => 'admin.store-manager.stores.view',
+ ],
+ 'Centros de trabajo' => [
+ 'route' => 'admin.store-manager.work-centers.index',
+ 'can' => 'admin.store-manager.stores.view',
+ ],
+ ]
+ ],
+ 'BANXICO' => [
+ 'route' => 'admin.finance.banxico.index',
+ 'can' => 'admin.finance.banxico.allow',
+ ],
+ 'Conectividad bancaria' => [
+ 'route' => 'admin.finance.banking.index',
+ 'can' => 'admin.finance.banking.allow',
+ ],
+ 'Punto de venta' => [
+ 'submenu' => [
+ 'Ticket' => [
+ 'route' => 'admin.sales.ticket-config.index',
+ 'can' => 'admin.sales.ticket-config.allow',
+ ],
+ ]
+ ],
+ 'Facturación' => [
+ 'submenu' => [
+ 'Certificados de Sello Digital' => [
+ 'route' => 'admin.billing.csds-settings.index',
+ 'can' => 'admin.billing.csds-settings.allow',
+ ],
+ 'Paquete de timbrado' => [
+ 'route' => 'admin.billing.stamping-package.index',
+ 'can' => 'admin.billing.stamping-package.allow',
+ ],
+ 'Servidor de correo SMTP' => [
+ 'route' => 'admin.billing.smtp-settings.index',
+ 'can' => 'admin.billing.smtp-settings.allow',
+ ],
+ 'Descarga masiva de CFDI' => [
+ 'route' => 'admin.billing.mass-cfdi-download.index',
+ 'can' => 'admin.billing.mass-cfdi-download.allow',
+ ],
+ ]
+ ],
+ ]
+ ],
+ 'Sistema' => [
+ 'icon' => 'menu-icon tf-icons ti ti-user-cog',
+ 'submenu' => [
+ 'Usuarios' => [
+ 'route' => 'admin.core.users.index',
+ 'can' => 'admin.core.users.view',
+ ],
+ 'Roles' => [
+ 'route' => 'admin.core.roles.index',
+ 'can' => 'admin.core.roles.view',
+ ],
+ 'Permisos' => [
+ 'route' => 'admin.core.permissions.index',
+ 'can' => 'admin.core.permissions.view',
+ ]
+ ]
+ ],
+ 'Catálogos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-library',
+ 'submenu' => [
+ 'Importar catálogos SAT' => [
+ 'route' => 'admin.core.sat-catalogs.index',
+ 'can' => 'admin.core.sat-catalogs.allow',
+ ],
+ ]
+ ],
+ 'Configuración de cuenta' => [
+ 'route' => 'admin.core.user-profile.index',
+ 'icon' => 'menu-icon tf-icons ti ti-user-cog',
+ ],
+ 'Acerca de' => [
+ 'route' => 'admin.core.about.index',
+ 'icon' => 'menu-icon tf-icons ti ti-cat',
+ ],
+ ],
+ ],
+ 'Herramientas Avanzadas' => [
+ 'icon' => 'menu-icon tf-icons ti ti-device-ipad-cog',
+ 'submenu' => [
+ 'Asistente AI' => [
+ 'icon' => 'menu-icon tf-icons ti ti-brain',
+ 'submenu' => [
+ 'Panel de IA' => [
+ 'route' => 'admin.ai.dashboard.index',
+ 'can' => 'ai.dashboard.view',
+ ],
+ 'Generación de Contenidos' => [
+ 'route' => 'admin.ai.content.index',
+ 'can' => 'ai.content.create',
+ ],
+ 'Análisis de Datos' => [
+ 'route' => 'admin.ai.analytics.index',
+ 'can' => 'ai.analytics.view',
+ ],
+ ],
+ ],
+ 'Chatbot' => [
+ 'icon' => 'menu-icon tf-icons ti ti-message-chatbot',
+ 'submenu' => [
+ 'Configuración' => [
+ 'route' => 'admin.chatbot.config.index',
+ 'can' => 'chatbot.config.view',
+ ],
+ 'Flujos de Conversación' => [
+ 'route' => 'admin.chatbot.flows.index',
+ 'can' => 'chatbot.flows.manage',
+ ],
+ 'Historial de Interacciones' => [
+ 'route' => 'admin.chatbot.history.index',
+ 'can' => 'chatbot.history.view',
+ ],
+ ],
+ ],
+ 'IoT Box' => [
+ 'icon' => 'menu-icon tf-icons ti ti-cpu',
+ 'submenu' => [
+ 'Dispositivos Conectados' => [
+ 'route' => 'admin.iot.devices.index',
+ 'can' => 'iot.devices.view',
+ ],
+ 'Sensores y Configuración' => [
+ 'route' => 'admin.iot.sensors.index',
+ 'can' => 'iot.sensors.manage',
+ ],
+ 'Monitoreo en Tiempo Real' => [
+ 'route' => 'admin.iot.monitoring.index',
+ 'can' => 'iot.monitoring.view',
+ ],
+ ],
+ ],
+ 'Reconocimiento Facial' => [
+ 'icon' => 'menu-icon tf-icons ti ti-face-id',
+ 'submenu' => [
+ 'Gestión de Perfiles' => [
+ 'route' => 'admin.facial-recognition.profiles.index',
+ 'can' => 'facial-recognition.profiles.manage',
+ ],
+ 'Verificación en Vivo' => [
+ 'route' => 'admin.facial-recognition.live.index',
+ 'can' => 'facial-recognition.live.verify',
+ ],
+ 'Historial de Accesos' => [
+ 'route' => 'admin.facial-recognition.history.index',
+ 'can' => 'facial-recognition.history.view',
+ ],
+ ],
+ ],
+ 'Servidor de Impresión' => [
+ 'icon' => 'menu-icon tf-icons ti ti-printer',
+ 'submenu' => [
+ 'Cola de Impresión' => [
+ 'route' => 'admin.print.queue.index',
+ 'can' => 'print.queue.view',
+ ],
+ 'Historial de Impresiones' => [
+ 'route' => 'admin.print.history.index',
+ 'can' => 'print.history.view',
+ ],
+ 'Configuración de Impresoras' => [
+ 'route' => 'admin.print.settings.index',
+ 'can' => 'print.settings.manage',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'Sitio Web' => [
+ 'icon' => 'menu-icon tf-icons ti ti-tools',
+ 'submenu' => [
+ 'Ajustes generales' => [
+ 'icon' => 'menu-icon tf-icons ti ti-tools',
+ 'route' => 'admin.website.general-settings.index',
+ 'can' => 'website.general-settings.allow',
+ ],
+ 'Avisos legales' => [
+ 'route' => 'admin.website.legal.index',
+ 'icon' => 'menu-icon tf-icons ti ti-writing-sign',
+ 'can' => 'website.legal.view',
+ ],
+ 'Preguntas frecuentes' => [
+ 'route' => 'admin.website.faq.index',
+ 'icon' => 'menu-icon tf-icons ti ti-bubble-text',
+ 'can' => 'website.faq.view',
+ ],
+ ]
+ ],
+ 'Blog' => [
+ 'icon' => 'menu-icon tf-icons ti ti-news',
+ 'submenu' => [
+ 'Categorias' => [
+ 'route' => 'admin.blog.categories.index',
+ 'icon' => 'menu-icon tf-icons ti ti-category',
+ 'can' => 'blog.categories.view',
+ ],
+ 'Etiquetas' => [
+ 'route' => 'admin.blog.tags.index',
+ 'icon' => 'menu-icon tf-icons ti ti-tags',
+ 'can' => 'blog.tags.view',
+ ],
+ 'Articulos' => [
+ 'route' => 'admin.blog.articles.index',
+ 'icon' => 'menu-icon tf-icons ti ti-news',
+ 'can' => 'blog.articles.view',
+ ],
+ 'Comentarios' => [
+ 'route' => 'admin.blog.comments.index',
+ 'icon' => 'menu-icon tf-icons ti ti-message',
+ 'can' => 'blog.comments.view',
+ ],
+ ]
+ ],
+ 'Contactos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-users',
+ 'submenu' => [
+ 'Contactos' => [
+ 'route' => 'admin.crm.contacts.index',
+ 'icon' => 'menu-icon tf-icons ti ti-users',
+ 'can' => 'crm.contacts.view',
+ ],
+ 'Campañas de marketing' => [
+ 'route' => 'admin.crm.marketing-campaigns.index',
+ 'icon' => 'menu-icon tf-icons ti ti-ad-2',
+ 'can' => 'crm.marketing-campaigns.view',
+ ],
+ 'Oportunidades ' => [
+ 'route' => 'admin.crm.leads.index',
+ 'icon' => 'menu-icon tf-icons ti ti-target-arrow',
+ 'can' => 'crm.leads.view',
+ ],
+ 'Newsletter' => [
+ 'route' => 'admin.crm.newsletter.index',
+ 'icon' => 'menu-icon tf-icons ti ti-notebook',
+ 'can' => 'crm.newsletter.view',
+ ],
+ ]
+ ],
+ 'RRHH' => [
+ 'icon' => 'menu-icon tf-icons ti ti-users-group',
+ 'submenu' => [
+ 'Gestión de Empleados' => [
+ 'icon' => 'menu-icon tf-icons ti ti-id-badge-2',
+ 'submenu' => [
+ 'Lista de Empleados' => [
+ 'route' => 'admin.rrhh.employees.index',
+ 'can' => 'rrhh.employees.view',
+ ],
+ 'Agregar Nuevo Empleado' => [
+ 'route' => 'admin.rrhh.employees.create',
+ 'can' => 'rrhh.employees.create',
+ ],
+ 'Puestos de trabajo' => [
+ 'route' => 'admin.rrhh.jobs.index',
+ 'can' => 'rrhh.jobs.view',
+ ],
+ 'Estructura Organizacional' => [
+ 'route' => 'admin.rrhh.organization.index',
+ 'can' => 'rrhh.organization.view',
+ ],
+ ],
+ ],
+ 'Reclutamiento' => [
+ 'icon' => 'menu-icon tf-icons ti ti-user-search',
+ 'submenu' => [
+ 'Vacantes Disponibles' => [
+ 'route' => 'admin.recruitment.jobs.index',
+ 'can' => 'recruitment.jobs.view',
+ ],
+ 'Seguimiento de Candidatos' => [
+ 'route' => 'admin.recruitment.candidates.index',
+ 'can' => 'recruitment.candidates.view',
+ ],
+ 'Entrevistas y Evaluaciones' => [
+ 'route' => 'admin.recruitment.interviews.index',
+ 'can' => 'recruitment.interviews.view',
+ ],
+ ],
+ ],
+ 'Nómina' => [
+ 'icon' => 'menu-icon tf-icons ti ti-cash',
+ 'submenu' => [
+ 'Contratos' => [
+ 'route' => 'admin.payroll.contracts.index',
+ 'can' => 'payroll.contracts.view',
+ ],
+ 'Procesar Nómina' => [
+ 'route' => 'admin.payroll.process.index',
+ 'can' => 'payroll.process.view',
+ ],
+ 'Recibos de Nómina' => [
+ 'route' => 'admin.payroll.receipts.index',
+ 'can' => 'payroll.receipts.view',
+ ],
+ 'Reportes Financieros' => [
+ 'route' => 'admin.payroll.reports.index',
+ 'can' => 'payroll.reports.view',
+ ],
+ ],
+ ],
+ 'Asistencia' => [
+ 'icon' => 'menu-icon tf-icons ti ti-calendar-exclamation',
+ 'submenu' => [
+ 'Registro de Horarios' => [
+ 'route' => 'admin.attendance.records.index',
+ 'can' => 'attendance.records.view',
+ ],
+ 'Asistencia con Biométricos' => [
+ 'route' => 'admin.attendance.biometric.index',
+ 'can' => 'attendance.biometric.view',
+ ],
+ 'Justificación de Ausencias' => [
+ 'route' => 'admin.attendance.absences.index',
+ 'can' => 'attendance.absences.view',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'Productos y servicios' => [
+ 'icon' => 'menu-icon tf-icons ti ti-package',
+ 'submenu' => [
+ 'Categorias' => [
+ 'route' => 'admin.inventory.product-categories.index',
+ 'icon' => 'menu-icon tf-icons ti ti-category',
+ 'can' => 'admin.inventory.product-categories.view',
+ ],
+ 'Catálogos' => [
+ 'route' => 'admin.inventory.product-catalogs.index',
+ 'icon' => 'menu-icon tf-icons ti ti-library',
+ 'can' => 'admin.inventory.product-catalogs.view',
+ ],
+ 'Productos y servicios' => [
+ 'route' => 'admin.inventory.products.index',
+ 'icon' => 'menu-icon tf-icons ti ti-packages',
+ 'can' => 'admin.inventory.products.view',
+ ],
+ 'Agregar producto o servicio' => [
+ 'route' => 'admin.inventory.products.create',
+ 'icon' => 'menu-icon tf-icons ti ti-package',
+ 'can' => 'admin.inventory.products.create',
+ ],
+ ]
+ ],
+ 'Ventas' => [
+ 'icon' => 'menu-icon tf-icons ti ti-cash-register',
+ 'submenu' => [
+ 'Tablero' => [
+ 'route' => 'admin.sales.dashboard.index',
+ 'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
+ 'can' => 'admin.sales.dashboard.allow',
+ ],
+ 'Clientes' => [
+ 'route' => 'admin.sales.customers.index',
+ 'icon' => 'menu-icon tf-icons ti ti-users-group',
+ 'can' => 'admin.sales.customers.view',
+ ],
+ 'Lista de precios' => [
+ 'route' => 'admin.sales.pricelist.index',
+ 'icon' => 'menu-icon tf-icons ti ti-report-search',
+ 'can' => 'admin.sales.sales.view',
+ ],
+ 'Cotizaciones' => [
+ 'route' => 'admin.sales.quotes.index',
+ 'icon' => 'menu-icon tf-icons ti ti-file-dollar',
+ 'can' => 'admin.sales.quotes.view',
+ ],
+ 'Ventas' => [
+ 'icon' => 'menu-icon tf-icons ti ti-cash-register',
+ 'submenu' => [
+ 'Crear venta' => [
+ 'route' => 'admin.sales.sales.create',
+ 'can' => 'admin.sales.sales.create',
+ ],
+ 'Ventas' => [
+ 'route' => 'admin.sales.sales.index',
+ 'can' => 'admin.sales.sales.view',
+ ],
+ 'Ventas por producto o servicio' => [
+ 'route' => 'admin.sales.sales-by-product.index',
+ 'can' => 'admin.sales.sales.view',
+ ],
+ ]
+ ],
+ 'Remisiones' => [
+ 'icon' => 'menu-icon tf-icons ti ti-receipt',
+ 'submenu' => [
+ 'Crear remisión' => [
+ 'route' => 'admin.sales.remissions.create',
+ 'can' => 'admin.sales.remissions.create',
+ ],
+ 'Remisiones' => [
+ 'route' => 'admin.sales.remissions.index',
+ 'can' => 'admin.sales.remissions.view',
+ ],
+ 'Remisiones por producto o servicio' => [
+ 'route' => 'admin.sales.remissions-by-product.index',
+ 'can' => 'admin.sales.remissions.view',
+ ],
+ ]
+ ],
+ 'Notas de crédito' => [
+ 'icon' => 'menu-icon tf-icons ti ti-receipt-refund',
+ 'submenu' => [
+ 'Crear nota de crédito' => [
+ 'route' => 'admin.sales.credit-notes.create',
+ 'can' => 'admin.sales.credit-notes.create',
+ ],
+ 'Notas de créditos' => [
+ 'route' => 'admin.sales.credit-notes.index',
+ 'can' => 'admin.sales.credit-notes.view',
+ ],
+ 'Notas de crédito por producto o servicio' => [
+ 'route' => 'admin.sales.credit-notes-by-product.index',
+ 'can' => 'admin.sales.credit-notes.view',
+ ],
+ ]
+ ],
+ ],
+ ],
+ 'Finanzas' => [
+ 'icon' => 'menu-icon tf-icons ti ti-coins',
+ 'submenu' => [
+ 'Tablero Financiero' => [
+ 'route' => 'admin.accounting.dashboard.index',
+ 'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
+ 'can' => 'accounting.dashboard.view',
+ ],
+ 'Contabilidad' => [
+ 'icon' => 'menu-icon tf-icons ti ti-chart-pie',
+ 'submenu' => [
+ 'Cuentas Contables' => [
+ 'route' => 'admin.accounting.charts.index',
+ 'can' => 'accounting.charts.view',
+ ],
+ 'Cuentas por pagar' => [
+ 'route' => 'admin.finance.accounts-payable.index',
+ 'can' => 'finance.accounts-payable.view',
+ ],
+ 'Cuentas por cobrar' => [
+ 'route' => 'admin.finance.accounts-receivable.index',
+ 'can' => 'finance.accounts-receivable.view',
+ ],
+ 'Balance General' => [
+ 'route' => 'admin.accounting.balance.index',
+ 'can' => 'accounting.balance.view',
+ ],
+ 'Estado de Resultados' => [
+ 'route' => 'admin.accounting.income-statement.index',
+ 'can' => 'accounting.income-statement.view',
+ ],
+ 'Libro Mayor' => [
+ 'route' => 'admin.accounting.ledger.index',
+ 'can' => 'accounting.ledger.view',
+ ],
+ 'Registros Contables' => [
+ 'route' => 'admin.accounting.entries.index',
+ 'can' => 'accounting.entries.view',
+ ],
+ ],
+ ],
+ 'Tablero de Gastos' => [
+ 'route' => 'admin.expenses.dashboard.index',
+ 'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
+ 'can' => 'expenses.dashboard.view',
+ ],
+ 'Gestión de Gastos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-receipt-2',
+ 'submenu' => [
+ 'Nuevo gasto' => [
+ 'route' => 'admin.expenses.expenses.create',
+ 'can' => 'expenses.expenses.create',
+ ],
+ 'Gastos' => [
+ 'route' => 'admin.expenses.expenses.index',
+ 'can' => 'expenses.expenses.view',
+ ],
+ 'Categorías de Gastos' => [
+ 'route' => 'admin.expenses.categories.index',
+ 'can' => 'expenses.categories.view',
+ ],
+ 'Historial de Gastos' => [
+ 'route' => 'admin.expenses.history.index',
+ 'can' => 'expenses.history.view',
+ ],
+ ],
+ ],
+ ],
+ ],
+
+
+
+
+ 'Facturación' => [
+ 'icon' => 'menu-icon tf-icons ti ti-rubber-stamp',
+ 'submenu' => [
+ 'Tablero' => [
+ 'route' => 'admin.billing.dashboard.index',
+ 'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
+ 'can' => 'admin.billing.dashboard.allow',
+ ],
+ 'Ingresos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-file-certificate',
+ 'submenu' => [
+ 'Facturar ventas' => [
+ 'route' => 'admin.billing.ingresos-stamp.index',
+ 'can' => 'admin.billing.ingresos.create',
+ ],
+ 'CFDI Ingresos' => [
+ 'route' => 'admin.billing.ingresos.index',
+ 'can' => 'admin.billing.ingresos.view',
+ ],
+ 'CFDI Ingresos por producto o servicio' => [
+ 'route' => 'admin.billing.ingresos-by-product.index',
+ 'can' => 'admin.billing.ingresos.view',
+ ],
+ ]
+ ],
+ 'Egresos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-file-certificate',
+ 'submenu' => [
+ 'Facturar notas de crédito' => [
+ 'route' => 'admin.billing.egresos-stamp.index',
+ 'can' => 'admin.billing.egresos.create',
+ ],
+ 'CFDI Engresos' => [
+ 'route' => 'admin.billing.egresos.index',
+ 'can' => 'admin.billing.egresos.view',
+ ],
+ 'CFDI Engresos por producto o servicio' => [
+ 'route' => 'admin.billing.egresos-by-product.index',
+ 'can' => 'admin.billing.egresos.view',
+ ],
+ ]
+ ],
+ 'Pagos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-file-certificate',
+ 'submenu' => [
+ 'Facturar pagos' => [
+ 'route' => 'admin.billing.pagos-stamp.index',
+ 'can' => 'admin.billing.pagos.created',
+ ],
+ 'CFDI Pagos' => [
+ 'route' => 'admin.billing.pagos.index',
+ 'can' => 'admin.billing.pagos.view',
+ ],
+ ]
+ ],
+ 'CFDI Nómina' => [
+ 'route' => 'admin.billing.nomina.index',
+ 'icon' => 'menu-icon tf-icons ti ti-file-certificate',
+ 'can' => 'admin.billing.nomina.view',
+ ],
+ 'Verificador de CFDI 4.0' => [
+ 'route' => 'admin.billing.verify-cfdi.index',
+ 'icon' => 'menu-icon tf-icons ti ti-rosette-discount-check',
+ 'can' => 'admin.billing.verify-cfdi.allow',
+ ],
+ ]
+ ],
+
+ 'Inventario y Logística' => [
+ 'icon' => 'menu-icon tf-icons ti ti-truck-delivery',
+ 'submenu' => [
+ 'Cadena de Suministro' => [
+ 'icon' => 'menu-icon tf-icons ti ti-chart-dots-3',
+ 'submenu' => [
+ 'Proveedores' => [
+ 'route' => 'admin.inventory.suppliers.index',
+ 'can' => 'admin.inventory.suppliers.view',
+ ],
+ 'Órdenes de Compra' => [
+ 'route' => 'admin.inventory.orders.index',
+ 'can' => 'admin.inventory.orders.view',
+ ],
+ 'Recepción de Productos' => [
+ 'route' => 'admin.inventory.reception.index',
+ 'can' => 'admin.inventory.reception.view',
+ ],
+ 'Gestión de Insumos' => [
+ 'route' => 'admin.inventory.materials.index',
+ 'can' => 'admin.inventory.materials.view',
+ ],
+ ],
+ ],
+ 'Gestión de Almacenes' => [
+ 'icon' => 'menu-icon tf-icons ti ti-building-warehouse',
+ 'submenu' => [
+ 'Almacenes' => [
+ 'route' => 'admin.inventory.warehouse.index',
+ 'can' => 'admin.inventory.warehouse.view',
+ ],
+ 'Stock de Inventario' => [
+ 'route' => 'admin.inventory.stock.index',
+ 'can' => 'admin.inventory.stock.view',
+ ],
+ 'Movimientos de almacenes' => [
+ 'route' => 'admin.inventory.movements.index',
+ 'can' => 'admin.inventory.movements.view',
+ ],
+ 'Transferencias entre Almacenes' => [
+ 'route' => 'admin.inventory.transfers.index',
+ 'can' => 'admin.inventory.transfers.view',
+ ],
+ ],
+ ],
+ 'Envíos y Logística' => [
+ 'icon' => 'menu-icon tf-icons ti ti-truck',
+ 'submenu' => [
+ 'Órdenes de Envío' => [
+ 'route' => 'admin.inventory.shipping-orders.index',
+ 'can' => 'admin.inventory.shipping-orders.view',
+ ],
+ 'Seguimiento de Envíos' => [
+ 'route' => 'admin.inventory.shipping-tracking.index',
+ 'can' => 'admin.inventory.shipping-tracking.view',
+ ],
+ 'Transportistas' => [
+ 'route' => 'admin.inventory.shipping-carriers.index',
+ 'can' => 'admin.inventory.shipping-carriers.view',
+ ],
+ 'Tarifas y Métodos de Envío' => [
+ 'route' => 'admin.inventory.shipping-rates.index',
+ 'can' => 'admin.inventory.shipping-rates.view',
+ ],
+ ],
+ ],
+ 'Gestión de Activos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-tools-kitchen',
+ 'submenu' => [
+ 'Activos Registrados' => [
+ 'route' => 'admin.inventory.asset.index',
+ 'can' => 'admin.inventory.asset.view',
+ ],
+ 'Mantenimiento Preventivo' => [
+ 'route' => 'admin.inventory.asset-maintenance.index',
+ 'can' => 'admin.inventory.asset-maintenance.view',
+ ],
+ 'Control de Vida Útil' => [
+ 'route' => 'admin.inventory.asset-lifecycle.index',
+ 'can' => 'admin.inventory.asset-lifecycle.view',
+ ],
+ 'Asignación de Activos' => [
+ 'route' => 'admin.inventory.asset-assignments.index',
+ 'can' => 'admin.inventory.asset-assignments.view',
+ ],
+ ],
+ ],
+ ],
+ ],
+
+ 'Gestión Empresarial' => [
+ 'icon' => 'menu-icon tf-icons ti ti-briefcase',
+ 'submenu' => [
+ 'Gestión de Proyectos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-layout-kanban',
+ 'submenu' => [
+ 'Tablero de Proyectos' => [
+ 'route' => 'admin.projects.dashboard.index',
+ 'can' => 'projects.dashboard.view',
+ ],
+ 'Proyectos Activos' => [
+ 'route' => 'admin.projects.index',
+ 'can' => 'projects.view',
+ ],
+ 'Crear Proyecto' => [
+ 'route' => 'admin.projects.create',
+ 'can' => 'projects.create',
+ ],
+ 'Gestión de Tareas' => [
+ 'route' => 'admin.projects.tasks.index',
+ 'can' => 'projects.tasks.view',
+ ],
+ 'Historial de Proyectos' => [
+ 'route' => 'admin.projects.history.index',
+ 'can' => 'projects.history.view',
+ ],
+ ],
+ ],
+ 'Producción y Manufactura' => [
+ 'icon' => 'menu-icon tf-icons ti ti-building-factory',
+ 'submenu' => [
+ 'Órdenes de Producción' => [
+ 'route' => 'admin.production.orders.index',
+ 'can' => 'production.orders.view',
+ ],
+ 'Nueva Orden de Producción' => [
+ 'route' => 'admin.production.orders.create',
+ 'can' => 'production.orders.create',
+ ],
+ 'Control de Procesos' => [
+ 'route' => 'admin.production.process.index',
+ 'can' => 'production.process.view',
+ ],
+ 'Historial de Producción' => [
+ 'route' => 'admin.production.history.index',
+ 'can' => 'production.history.view',
+ ],
+ ],
+ ],
+ 'Control de Calidad' => [
+ 'icon' => 'menu-icon tf-icons ti ti-award',
+ 'submenu' => [
+ 'Inspecciones de Calidad' => [
+ 'route' => 'admin.quality.inspections.index',
+ 'can' => 'quality.inspections.view',
+ ],
+ 'Crear Inspección' => [
+ 'route' => 'admin.quality.inspections.create',
+ 'can' => 'quality.inspections.create',
+ ],
+ 'Reportes de Calidad' => [
+ 'route' => 'admin.quality.reports.index',
+ 'can' => 'quality.reports.view',
+ ],
+ 'Historial de Inspecciones' => [
+ 'route' => 'admin.quality.history.index',
+ 'can' => 'quality.history.view',
+ ],
+ ],
+ ],
+ 'Flujos de Trabajo y Automatización' => [
+ 'icon' => 'menu-icon tf-icons ti ti-chart-dots-3',
+ 'submenu' => [
+ 'Gestión de Flujos de Trabajo' => [
+ 'route' => 'admin.workflows.index',
+ 'can' => 'workflows.view',
+ ],
+ 'Crear Flujo de Trabajo' => [
+ 'route' => 'admin.workflows.create',
+ 'can' => 'workflows.create',
+ ],
+ 'Automatizaciones' => [
+ 'route' => 'admin.workflows.automations.index',
+ 'can' => 'workflows.automations.view',
+ ],
+ 'Historial de Flujos' => [
+ 'route' => 'admin.workflows.history.index',
+ 'can' => 'workflows.history.view',
+ ],
+ ],
+ ],
+ ],
+ ],
+
+
+ 'Contratos' => [
+ 'icon' => 'menu-icon tf-icons ti ti-writing-sign',
+ 'submenu' => [
+ 'Mis Contratos' => [
+ 'route' => 'admin.contracts.index',
+ 'icon' => 'menu-icon tf-icons ti ti-file-description',
+ 'can' => 'contracts.view',
+ ],
+ 'Firmar Contrato' => [
+ 'route' => 'admin.contracts.sign',
+ 'icon' => 'menu-icon tf-icons ti ti-signature',
+ 'can' => 'contracts.sign',
+ ],
+ 'Contratos Automatizados' => [
+ 'route' => 'admin.contracts.automated',
+ 'icon' => 'menu-icon tf-icons ti ti-robot',
+ 'can' => 'contracts.automated.view',
+ ],
+ 'Historial de Contratos' => [
+ 'route' => 'admin.contracts.history',
+ 'icon' => 'menu-icon tf-icons ti ti-archive',
+ 'can' => 'contracts.history.view',
+ ],
+ ]
+ ],
+ 'Atención al Cliente' => [
+ 'icon' => 'menu-icon tf-icons ti ti-messages',
+ 'submenu' => [
+ 'Tablero' => [
+ 'route' => 'admin.sales.dashboard.index',
+ 'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
+ 'can' => 'ticketing.dashboard.view',
+ ],
+ 'Mis Tickets' => [
+ 'route' => 'admin.ticketing.tickets.index',
+ 'icon' => 'menu-icon tf-icons ti ti-ticket',
+ 'can' => 'ticketing.tickets.view',
+ ],
+ 'Crear Ticket' => [
+ 'route' => 'admin.ticketing.tickets.create',
+ 'icon' => 'menu-icon tf-icons ti ti-square-plus',
+ 'can' => 'ticketing.tickets.create',
+ ],
+ 'Categorías de Tickets' => [
+ 'route' => 'admin.ticketing.categories.index',
+ 'icon' => 'menu-icon tf-icons ti ti-category',
+ 'can' => 'ticketing.categories.view',
+ ],
+ 'Estadísticas de Atención' => [
+ 'route' => 'admin.ticketing.analytics.index',
+ 'icon' => 'menu-icon tf-icons ti ti-chart-bar',
+ 'can' => 'ticketing.analytics.view',
+ ],
+ ]
+ ],
+];
diff --git a/database/data/rbac-config.json b/database/data/rbac-config.json
new file mode 100644
index 0000000..249d102
--- /dev/null
+++ b/database/data/rbac-config.json
@@ -0,0 +1,510 @@
+{
+ "roles": {
+ "SuperAdmin" : {
+ "style": "dark",
+ "permissions" : [
+ "admin.core.general-settings.allow",
+ "admin.core.cache-manager.view",
+ "admin.core.smtp-settings.allow",
+ "admin.store-manager.company.view",
+ "admin.store-manager.stores.view",
+ "admin.store-manager.stores.view",
+ "admin.finance.banxico.allow",
+ "admin.finance.banking.allow",
+ "admin.sales.ticket-config.allow",
+ "admin.billing.csds-settings.allow",
+ "admin.billing.stamping-package.allow",
+ "admin.billing.smtp-settings.allow",
+ "admin.billing.mass-cfdi-download.allow",
+ "admin.core.users.view",
+ "admin.core.roles.view",
+ "admin.core.permissions.view",
+ "admin.core.import-sat-catalogs.allow",
+ "admin.ai.dashboard.view",
+ "admin.ai.content.create",
+ "admin.ai.analytics.view",
+ "admin.chatbot.config.view",
+ "admin.chatbot.flows.manage",
+ "admin.chatbot.history.view",
+ "admin.iot.devices.view",
+ "admin.iot.sensors.manage",
+ "admin.iot.monitoring.view",
+ "admin.facial-recognition.profiles.manage",
+ "admin.facial-recognition.live.verify",
+ "admin.facial-recognition.history.view",
+ "admin.print.queue.view",
+ "admin.print.history.view",
+ "admin.print.settings.manage",
+ "admin.website.general-settings.allow",
+ "admin.website.legal.view",
+ "admin.website.faq.view",
+ "admin.blog.categories.view",
+ "admin.blog.tags.view",
+ "admin.blog.articles.view",
+ "admin.blog.comments.view",
+ "admin.contacts.contacts.view",
+ "admin.contacts.employees.view",
+ "admin.contacts.employees.create",
+ "admin.rrhh.jobs.view",
+ "admin.rrhh.organization.view",
+ "admin.recruitment.jobs.view",
+ "admin.recruitment.candidates.view",
+ "admin.recruitment.interviews.view",
+ "admin.payroll.contracts.view",
+ "admin.payroll.process.view",
+ "admin.payroll.receipts.view",
+ "admin.payroll.reports.view",
+ "admin.attendance.records.view",
+ "admin.attendance.biometric.view",
+ "admin.attendance.absences.view",
+ "admin.inventory.product-categories.view",
+ "admin.inventory.product-catalogs.view",
+ "admin.inventory.products.view",
+ "admin.inventory.products.create",
+ "admin.sales.dashboard.allow",
+ "admin.contacts.customers.view",
+ "admin.sales.sales.view",
+ "admin.sales.quotes.view",
+ "admin.sales.sales.create",
+ "admin.sales.sales.view",
+ "admin.sales.sales.view",
+ "admin.sales.remissions.create",
+ "admin.sales.remissions.view",
+ "admin.sales.remissions.view",
+ "admin.sales.credit-notes.create",
+ "admin.sales.credit-notes.view",
+ "admin.sales.credit-notes.view",
+ "admin.accounting.dashboard.view",
+ "admin.accounting.charts.view",
+ "admin.finance.accounts-payable.view",
+ "admin.finance.accounts-receivable.view",
+ "admin.accounting.balance.view",
+ "admin.accounting.income-statement.view",
+ "admin.accounting.ledger.view",
+ "admin.accounting.entries.view",
+ "admin.expenses.dashboard.view",
+ "admin.expenses.expenses.create",
+ "admin.expenses.expenses.view",
+ "admin.expenses.categories.view",
+ "admin.expenses.history.view",
+ "admin.billing.dashboard.allow",
+ "admin.billing.ingresos.create",
+ "admin.billing.ingresos.view",
+ "admin.billing.ingresos.view",
+ "admin.billing.egresos.create",
+ "admin.billing.egresos.view",
+ "admin.billing.egresos.view",
+ "admin.billing.pagos.created",
+ "admin.billing.pagos.view",
+ "admin.billing.nomina.view",
+ "admin.billing.verify-cfdi.allow",
+ "admin.contacts.suppliers.view",
+ "admin.inventory.orders.view",
+ "admin.inventory.reception.view",
+ "admin.inventory.materials.view",
+ "admin.inventory.warehouse.view",
+ "admin.inventory.stock.view",
+ "admin.inventory.movements.view",
+ "admin.inventory.transfers.view",
+ "admin.inventory.shipping-orders.view",
+ "admin.inventory.shipping-tracking.view",
+ "admin.inventory.shipping-carriers.view",
+ "admin.inventory.shipping-rates.view",
+ "admin.inventory.assets.view",
+ "admin.inventory.asset-maintenance.view",
+ "admin.inventory.asset-lifecycle.view",
+ "admin.inventory.asset-assignments.view",
+ "admin.projects.dashboard.view",
+ "admin.projects.view",
+ "admin.projects.create",
+ "admin.projects.tasks.view",
+ "admin.projects.history.view",
+ "admin.production.orders.view",
+ "admin.production.orders.create",
+ "admin.production.process.view",
+ "admin.production.history.view",
+ "admin.quality.inspections.view",
+ "admin.quality.inspections.create",
+ "admin.quality.reports.view",
+ "admin.quality.history.view",
+ "admin.workflows.view",
+ "admin.workflows.create",
+ "admin.workflows.automations.view",
+ "admin.workflows.history.view",
+ "admin.contracts.view",
+ "admin.contracts.sign",
+ "admin.contracts.automated.view",
+ "admin.contracts.history.view",
+ "admin.ticketing.dashboard.view",
+ "admin.ticketing.tickets.view",
+ "admin.ticketing.tickets.create",
+ "admin.ticketing.categories.view",
+ "admin.ticketing.analytics.view"
+ ]
+ },
+ "Admin" : {
+ "style": "primary",
+ "permissions" : [
+ "admin.core.general-settings.allow",
+ "admin.core.cache-manager.view",
+ "admin.core.smtp-settings.allow",
+ "admin.website.general-settings.allow",
+ "admin.website.legal.view",
+ "admin.store-manager.company.view",
+ "admin.store-manager.stores.view",
+ "admin.store-manager.stores.view",
+ "admin.core.users.view",
+ "admin.core.roles.view",
+ "admin.core.permissions.view",
+ "admin.core.import-sat-catalogs.allow",
+ "admin.contacts.contacts.view",
+ "admin.contacts.contacts.create",
+ "admin.contacts.employees.view",
+ "admin.contacts.employees.create",
+ "admin.contacts.customers.view",
+ "admin.contacts.customers.create",
+ "admin.rrhh.jobs.view",
+ "admin.rrhh.organization.view",
+ "admin.inventory.product-categories.view",
+ "admin.inventory.product-catalogs.view",
+ "admin.inventory.products.view",
+ "admin.inventory.products.create",
+ "admin.contacts.suppliers.view",
+ "admin.contacts.suppliers.create",
+ "admin.inventory.warehouse.view",
+ "admin.inventory.orders.view",
+ "admin.inventory.reception.view",
+ "admin.inventory.materials.view",
+ "admin.inventory.stock.view",
+ "admin.inventory.movements.view",
+ "admin.inventory.transfers.view",
+ "admin.inventory.assets.view",
+ "admin.inventory.asset-maintenance.view",
+ "admin.inventory.asset-lifecycle.view",
+ "admin.inventory.asset-assignments.view"
+ ]
+ },
+ "Administrador Web" : {
+ "style": "primary",
+ "permissions" : []
+ },
+ "Editor" : {
+ "style": "primary",
+ "permissions" : []
+ },
+ "Almacenista" : {
+ "style": "success",
+ "permissions" : [
+ "admin.inventory.product-categories.view",
+ "admin.inventory.product-catalogs.view",
+ "admin.inventory.products.view",
+ "admin.inventory.products.create",
+ "admin.inventory.warehouse.view",
+ "admin.inventory.stock.view",
+ "admin.inventory.movements.view",
+ "admin.inventory.transfers.view"
+ ]
+ },
+ "Productos y servicios" : {
+ "style": "info",
+ "permissions" : []
+ },
+ "Recursos humanos" : {
+ "style": "success",
+ "permissions" : []
+ },
+ "Nómina" : {
+ "style": "success",
+ "permissions" : []
+ },
+ "Activos fijos" : {
+ "style": "secondary",
+ "permissions" : []
+ },
+ "Compras y gastos" : {
+ "style": "info",
+ "permissions" : []
+ },
+ "CRM" : {
+ "style": "warning",
+ "permissions" : []
+ },
+ "Vendedor" : {
+ "style": "info",
+ "permissions" : []
+ },
+ "Gerente" : {
+ "style": "danger",
+ "permissions" : []
+ },
+ "Facturación" : {
+ "style": "info",
+ "permissions" : []
+ },
+ "Facturación avanzado" : {
+ "style": "danger",
+ "permissions" : []
+ },
+ "Finanzas" : {
+ "style": "info",
+ "permissions" : []
+ },
+ "Auditor" : {
+ "style": "dark",
+ "permissions" : [
+ "admin.core.cache-manager.view",
+ "admin.store-manager.company.view",
+ "admin.store-manager.stores.view",
+ "admin.store-manager.stores.view",
+ "admin.core.users.view",
+ "admin.core.roles.view",
+ "admin.core.permissions.view",
+ "admin.ai.dashboard.view",
+ "admin.ai.analytics.view",
+ "admin.chatbot.config.view",
+ "admin.chatbot.history.view",
+ "admin.iot.devices.view",
+ "admin.iot.monitoring.view",
+ "admin.facial-recognition.history.view",
+ "admin.print.queue.view",
+ "admin.print.history.view",
+ "admin.website.legal.view",
+ "admin.website.faq.view",
+ "admin.blog.categories.view",
+ "admin.blog.tags.view",
+ "admin.blog.articles.view",
+ "admin.blog.comments.view",
+ "admin.contacts.contacts.view",
+ "admin.crm.marketing-campaigns.view",
+ "admin.crm.leads.view",
+ "admin.crm.newsletter.view",
+ "admin.contacts.employees.view",
+ "admin.rrhh.jobs.view",
+ "admin.rrhh.organization.view",
+ "admin.recruitment.jobs.view",
+ "admin.recruitment.candidates.view",
+ "admin.recruitment.interviews.view",
+ "admin.payroll.contracts.view",
+ "admin.payroll.process.view",
+ "admin.payroll.receipts.view",
+ "admin.payroll.reports.view",
+ "admin.attendance.records.view",
+ "admin.attendance.biometric.view",
+ "admin.attendance.absences.view",
+ "admin.inventory.product-categories.view",
+ "admin.inventory.product-catalogs.view",
+ "admin.inventory.products.view",
+ "admin.contacts.customers.view",
+ "admin.sales.sales.view",
+ "admin.sales.quotes.view",
+ "admin.sales.sales.view",
+ "admin.sales.sales.view",
+ "admin.sales.remissions.view",
+ "admin.sales.remissions.view",
+ "admin.sales.credit-notes.view",
+ "admin.sales.credit-notes.view",
+ "admin.accounting.dashboard.view",
+ "admin.accounting.charts.view",
+ "admin.finance.accounts-payable.view",
+ "admin.finance.accounts-receivable.view",
+ "admin.accounting.balance.view",
+ "admin.accounting.income-statement.view",
+ "admin.accounting.ledger.view",
+ "admin.accounting.entries.view",
+ "admin.expenses.dashboard.view",
+ "admin.expenses.expenses.view",
+ "admin.expenses.categories.view",
+ "admin.expenses.history.view",
+ "admin.billing.ingresos.view",
+ "admin.billing.ingresos.view",
+ "admin.billing.egresos.view",
+ "admin.billing.egresos.view",
+ "admin.billing.pagos.view",
+ "admin.billing.nomina.view",
+ "admin.contacts.suppliers.view",
+ "admin.inventory.orders.view",
+ "admin.inventory.reception.view",
+ "admin.inventory.materials.view",
+ "admin.inventory.warehouse.view",
+ "admin.inventory.stock.view",
+ "admin.inventory.movements.view",
+ "admin.inventory.transfers.view",
+ "admin.inventory.shipping-orders.view",
+ "admin.inventory.shipping-tracking.view",
+ "admin.inventory.shipping-carriers.view",
+ "admin.inventory.shipping-rates.view",
+ "admin.inventory.assets.view",
+ "admin.inventory.asset-maintenance.view",
+ "admin.inventory.asset-lifecycle.view",
+ "admin.inventory.asset-assignments.view",
+ "admin.projects.dashboard.view",
+ "admin.projects.view",
+ "admin.projects.tasks.view",
+ "admin.projects.history.view",
+ "admin.production.orders.view",
+ "admin.production.process.view",
+ "admin.production.history.view",
+ "admin.quality.inspections.view",
+ "admin.quality.reports.view",
+ "admin.quality.history.view",
+ "admin.workflows.view",
+ "admin.workflows.automations.view",
+ "admin.workflows.history.view",
+ "admin.contracts.view",
+ "admin.contracts.automated.view",
+ "admin.contracts.history.view",
+ "admin.ticketing.dashboard.view",
+ "admin.ticketing.tickets.view",
+ "admin.ticketing.categories.view",
+ "admin.ticketing.analytics.view"
+ ]
+ }
+ },
+ "permissions": [
+ "admin.core.general-settings.allow",
+ "admin.core.cache-manager.view",
+ "admin.core.smtp-settings.allow",
+ "admin.store-manager.company.view",
+ "admin.store-manager.stores.view",
+ "admin.store-manager.stores.view",
+ "admin.finance.banxico.allow",
+ "admin.finance.banking.allow",
+ "admin.sales.ticket-config.allow",
+ "admin.billing.csds-settings.allow",
+ "admin.billing.stamping-package.allow",
+ "admin.billing.smtp-settings.allow",
+ "admin.billing.mass-cfdi-download.allow",
+ "admin.core.users.view",
+ "admin.core.roles.view",
+ "admin.core.permissions.view",
+ "admin.core.import-sat-catalogs.allow",
+ "admin.ai.dashboard.view",
+ "admin.ai.content.create",
+ "admin.ai.analytics.view",
+ "admin.chatbot.config.view",
+ "admin.chatbot.flows.manage",
+ "admin.chatbot.history.view",
+ "admin.iot.devices.view",
+ "admin.iot.sensors.manage",
+ "admin.iot.monitoring.view",
+ "admin.facial-recognition.profiles.manage",
+ "admin.facial-recognition.live.verify",
+ "admin.facial-recognition.history.view",
+ "admin.print.queue.view",
+ "admin.print.history.view",
+ "admin.print.settings.manage",
+ "admin.website.general-settings.allow",
+ "admin.website.legal.view",
+ "admin.website.faq.view",
+ "admin.blog.categories.view",
+ "admin.blog.tags.view",
+ "admin.blog.articles.view",
+ "admin.blog.comments.view",
+ "admin.contacts.contacts.view",
+ "admin.contacts.contacts.create",
+ "admin.crm.marketing-campaigns.view",
+ "admin.crm.leads.view",
+ "admin.crm.newsletter.view",
+ "admin.contacts.employees.view",
+ "admin.contacts.employees.create",
+ "admin.rrhh.jobs.view",
+ "admin.rrhh.organization.view",
+ "admin.recruitment.jobs.view",
+ "admin.recruitment.candidates.view",
+ "admin.recruitment.interviews.view",
+ "admin.payroll.contracts.view",
+ "admin.payroll.process.view",
+ "admin.payroll.receipts.view",
+ "admin.payroll.reports.view",
+ "admin.attendance.records.view",
+ "admin.attendance.biometric.view",
+ "admin.attendance.absences.view",
+ "admin.inventory.product-categories.view",
+ "admin.inventory.product-catalogs.view",
+ "admin.inventory.products.view",
+ "admin.inventory.products.create",
+ "admin.sales.dashboard.allow",
+ "admin.contacts.customers.view",
+ "admin.contacts.customers.create",
+ "admin.sales.sales.view",
+ "admin.sales.quotes.view",
+ "admin.sales.sales.create",
+ "admin.sales.sales.view",
+ "admin.sales.sales.view",
+ "admin.sales.remissions.create",
+ "admin.sales.remissions.view",
+ "admin.sales.remissions.view",
+ "admin.sales.credit-notes.create",
+ "admin.sales.credit-notes.view",
+ "admin.sales.credit-notes.view",
+ "admin.accounting.dashboard.view",
+ "admin.accounting.charts.view",
+ "admin.finance.accounts-payable.view",
+ "admin.finance.accounts-receivable.view",
+ "admin.accounting.balance.view",
+ "admin.accounting.income-statement.view",
+ "admin.accounting.ledger.view",
+ "admin.accounting.entries.view",
+ "admin.expenses.dashboard.view",
+ "admin.expenses.expenses.create",
+ "admin.expenses.expenses.view",
+ "admin.expenses.categories.view",
+ "admin.expenses.history.view",
+ "admin.billing.dashboard.allow",
+ "admin.billing.ingresos.create",
+ "admin.billing.ingresos.view",
+ "admin.billing.ingresos.view",
+ "admin.billing.egresos.create",
+ "admin.billing.egresos.view",
+ "admin.billing.egresos.view",
+ "admin.billing.pagos.created",
+ "admin.billing.pagos.view",
+ "admin.billing.nomina.view",
+ "admin.billing.verify-cfdi.allow",
+ "admin.contacts.suppliers.view",
+ "admin.contacts.suppliers.create",
+ "admin.inventory.orders.view",
+ "admin.inventory.reception.view",
+ "admin.inventory.materials.view",
+ "admin.inventory.warehouse.view",
+ "admin.inventory.stock.view",
+ "admin.inventory.movements.view",
+ "admin.inventory.transfers.view",
+ "admin.inventory.shipping-orders.view",
+ "admin.inventory.shipping-tracking.view",
+ "admin.inventory.shipping-carriers.view",
+ "admin.inventory.shipping-rates.view",
+ "admin.inventory.assets.view",
+ "admin.inventory.asset-maintenance.view",
+ "admin.inventory.asset-lifecycle.view",
+ "admin.inventory.asset-assignments.view",
+ "admin.projects.dashboard.view",
+ "admin.projects.view",
+ "admin.projects.create",
+ "admin.projects.tasks.view",
+ "admin.projects.history.view",
+ "admin.production.orders.view",
+ "admin.production.orders.create",
+ "admin.production.process.view",
+ "admin.production.history.view",
+ "admin.quality.inspections.view",
+ "admin.quality.inspections.create",
+ "admin.quality.reports.view",
+ "admin.quality.history.view",
+ "admin.workflows.view",
+ "admin.workflows.create",
+ "admin.workflows.automations.view",
+ "admin.workflows.history.view",
+ "admin.contracts.view",
+ "admin.contracts.sign",
+ "admin.contracts.automated.view",
+ "admin.contracts.history.view",
+ "admin.ticketing.dashboard.view",
+ "admin.ticketing.tickets.view",
+ "admin.ticketing.tickets.create",
+ "admin.ticketing.categories.view",
+ "admin.ticketing.analytics.view"
+ ]
+}
+
+
diff --git a/database/data/users.csv b/database/data/users.csv
new file mode 100644
index 0000000..403ec91
--- /dev/null
+++ b/database/data/users.csv
@@ -0,0 +1,14 @@
+name,email,role,password
+Administrador Web,webadmin@concierge.test,Administrador Web,LAdmin123
+Productos y servicios,productos@concierge.test,Productos y servicios,LAdmin123
+Recursos humanos,rrhh@concierge.test,Recursos humanos,LAdmin123
+Nómina,nomina@concierge.test,Nómina,LAdmin123
+Activos fijos,activos@concierge.test,Activos fijos,LAdmin123
+Compras y gastos,compras@concierge.test,Compras y gastos,LAdmin123
+CRM,crm@concierge.test,CRM,LAdmin123
+Vendedor,vendedor@concierge.test,Vendedor,LAdmin123
+Gerente,gerente@concierge.test,Gerente,LAdmin123
+Facturación,facturacion@concierge.test,Facturación,LAdmin123
+Facturación avanzado,facturacion_avanzado@concierge.test,Facturación avanzado,LAdmin123
+Finanzas,finanzas@concierge.test,Finanzas,LAdmin123
+Auditor,auditor@concierge.test,Auditor,LAdmin123
diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php
new file mode 100644
index 0000000..ba86720
--- /dev/null
+++ b/database/factories/UserFactory.php
@@ -0,0 +1,49 @@
+
+ */
+class UserFactory extends Factory
+{
+ /**
+ * The current password being used by the factory.
+ */
+ protected static ?string $password;
+
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition(): array
+ {
+ return [
+ 'name' => fake()->name(),
+ 'email' => fake()->unique()->safeEmail(),
+ 'email_verified_at' => now(),
+ 'password' => static::$password ??= Hash::make('password'),
+ 'two_factor_secret' => null,
+ 'two_factor_recovery_codes' => null,
+ 'remember_token' => Str::random(10),
+ 'profile_photo_path' => null,
+ 'status' => fake()->randomElement([User::STATUS_ENABLED, User::STATUS_DISABLED])
+ ];
+ }
+
+ /**
+ * Indicate that the model's email address should be unverified.
+ */
+ public function unverified(): static
+ {
+ return $this->state(fn(array $attributes) => [
+ 'email_verified_at' => null,
+ ]);
+ }
+}
diff --git a/database/migrations/2024_12_14_030215_modify_users_table.php b/database/migrations/2024_12_14_030215_modify_users_table.php
new file mode 100644
index 0000000..f4567b4
--- /dev/null
+++ b/database/migrations/2024_12_14_030215_modify_users_table.php
@@ -0,0 +1,44 @@
+string('last_name', 100)->nullable()->comment('Apellidos')->index()->after('name');
+ $table->string('profile_photo_path', 2048)->nullable()->after('remember_token');
+ $table->unsignedTinyInteger('status')->default(User::STATUS_DISABLED)->after('profile_photo_path');
+ $table->unsignedMediumInteger('created_by')->nullable()->index()->after('status');
+
+ // Definir la relación con created_by
+ $table->foreign('created_by')->references('id')->on('users')->onUpdate('restrict')->onDelete('restrict');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ DB::statement('ALTER TABLE `users` MODIFY `id` MEDIUMINT UNSIGNED NOT NULL;');
+ DB::statement('ALTER TABLE `users` DROP PRIMARY KEY;');
+ DB::statement('ALTER TABLE `users` MODIFY `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (`id`);');
+
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn(['last_name', 'profile_photo_path', 'status', 'created_by']);
+
+ });
+ }
+};
diff --git a/database/migrations/2024_12_14_035487_create_user_logins_table.php b/database/migrations/2024_12_14_035487_create_user_logins_table.php
new file mode 100644
index 0000000..84f5ba3
--- /dev/null
+++ b/database/migrations/2024_12_14_035487_create_user_logins_table.php
@@ -0,0 +1,36 @@
+integerIncrements('id');
+
+ $table->unsignedMediumInteger('user_id')->nullable()->index();
+ $table->ipAddress('ip_address')->nullable();
+ $table->string('user_agent')->nullable();
+
+ $table->timestamps();
+
+ // Relaciones
+ $table->foreign('user_id')->references('id')->on('users');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ // Elimina tablas solo si existen
+ Schema::dropIfExists('user_logins');
+ }
+};
diff --git a/database/migrations/2024_12_14_073441_create_personal_access_tokens_table.php b/database/migrations/2024_12_14_073441_create_personal_access_tokens_table.php
new file mode 100644
index 0000000..e828ad8
--- /dev/null
+++ b/database/migrations/2024_12_14_073441_create_personal_access_tokens_table.php
@@ -0,0 +1,33 @@
+id();
+ $table->morphs('tokenable');
+ $table->string('name');
+ $table->string('token', 64)->unique();
+ $table->text('abilities')->nullable();
+ $table->timestamp('last_used_at')->nullable();
+ $table->timestamp('expires_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('personal_access_tokens');
+ }
+};
diff --git a/database/migrations/2024_12_14_074756_create_permission_tables.php b/database/migrations/2024_12_14_074756_create_permission_tables.php
new file mode 100644
index 0000000..347947f
--- /dev/null
+++ b/database/migrations/2024_12_14_074756_create_permission_tables.php
@@ -0,0 +1,153 @@
+engine('InnoDB');
+ $table->bigIncrements('id'); // permission id
+ $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
+ $table->string('group_name')->nullable()->index();
+ $table->string('sub_group_name')->nullable()->index();
+ $table->string('action')->nullable()->index();
+ $table->string('guard_name'); // For MyISAM use string('guard_name', 25);
+ $table->timestamps();
+
+ $table->unique(['name', 'guard_name']);
+ $table->unique(['group_name', 'sub_group_name', 'action', 'guard_name']);
+ });
+
+ Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
+ //$table->engine('InnoDB');
+ $table->bigIncrements('id'); // role id
+ if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
+ $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
+ $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
+ }
+ $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
+ $table->string('style')->nullable();
+ $table->string('guard_name'); // For MyISAM use string('guard_name', 25);
+ $table->timestamps();
+ if ($teams || config('permission.testing')) {
+ $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
+ } else {
+ $table->unique(['name', 'guard_name']);
+ }
+ });
+
+ Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
+ $table->unsignedBigInteger($pivotPermission);
+
+ $table->string('model_type');
+ $table->unsignedBigInteger($columnNames['model_morph_key']);
+ $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
+
+ $table->foreign($pivotPermission)
+ ->references('id') // permission id
+ ->on($tableNames['permissions'])
+ ->onDelete('cascade');
+ if ($teams) {
+ $table->unsignedBigInteger($columnNames['team_foreign_key']);
+ $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
+
+ $table->primary(
+ [$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_permissions_permission_model_type_primary'
+ );
+ } else {
+ $table->primary(
+ [$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_permissions_permission_model_type_primary'
+ );
+ }
+ });
+
+ Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
+ $table->unsignedBigInteger($pivotRole);
+
+ $table->string('model_type');
+ $table->unsignedBigInteger($columnNames['model_morph_key']);
+ $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
+
+ $table->foreign($pivotRole)
+ ->references('id') // role id
+ ->on($tableNames['roles'])
+ ->onDelete('cascade');
+ if ($teams) {
+ $table->unsignedBigInteger($columnNames['team_foreign_key']);
+ $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
+
+ $table->primary(
+ [$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_roles_role_model_type_primary'
+ );
+ } else {
+ $table->primary(
+ [$pivotRole, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_roles_role_model_type_primary'
+ );
+ }
+ });
+
+ Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
+ $table->unsignedBigInteger($pivotPermission);
+ $table->unsignedBigInteger($pivotRole);
+
+ $table->foreign($pivotPermission)
+ ->references('id') // permission id
+ ->on($tableNames['permissions'])
+ ->onDelete('cascade');
+
+ $table->foreign($pivotRole)
+ ->references('id') // role id
+ ->on($tableNames['roles'])
+ ->onDelete('cascade');
+
+ $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
+ });
+
+ app('cache')
+ ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
+ ->forget(config('permission.cache.key'));
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ $tableNames = config('permission.table_names');
+
+ if (empty($tableNames)) {
+ throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
+ }
+
+ Schema::drop($tableNames['role_has_permissions']);
+ Schema::drop($tableNames['model_has_roles']);
+ Schema::drop($tableNames['model_has_permissions']);
+ Schema::drop($tableNames['roles']);
+ Schema::drop($tableNames['permissions']);
+ }
+};
diff --git a/database/migrations/2024_12_14_081739_add_two_factor_columns_to_users_table.php b/database/migrations/2024_12_14_081739_add_two_factor_columns_to_users_table.php
new file mode 100644
index 0000000..b490e24
--- /dev/null
+++ b/database/migrations/2024_12_14_081739_add_two_factor_columns_to_users_table.php
@@ -0,0 +1,46 @@
+text('two_factor_secret')
+ ->after('password')
+ ->nullable();
+
+ $table->text('two_factor_recovery_codes')
+ ->after('two_factor_secret')
+ ->nullable();
+
+ if (Fortify::confirmsTwoFactorAuthentication()) {
+ $table->timestamp('two_factor_confirmed_at')
+ ->after('two_factor_recovery_codes')
+ ->nullable();
+ }
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn(array_merge([
+ 'two_factor_secret',
+ 'two_factor_recovery_codes',
+ ], Fortify::confirmsTwoFactorAuthentication() ? [
+ 'two_factor_confirmed_at',
+ ] : []));
+ });
+ }
+};
diff --git a/database/migrations/2024_12_14_082234_create_settings_table.php b/database/migrations/2024_12_14_082234_create_settings_table.php
new file mode 100644
index 0000000..db08618
--- /dev/null
+++ b/database/migrations/2024_12_14_082234_create_settings_table.php
@@ -0,0 +1,37 @@
+mediumIncrements('id');
+
+ $table->string('key')->index();
+ $table->text('value');
+ $table->unsignedMediumInteger('user_id')->nullable()->index();
+
+ // Unique constraints
+ $table->unique(['user_id', 'key']);
+
+ // Relaciones
+ $table->foreign('user_id')->references('id')->on('users');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('settings');
+ }
+
+};
diff --git a/database/migrations/2024_12_14_083409_create_media_items_table.php b/database/migrations/2024_12_14_083409_create_media_items_table.php
new file mode 100644
index 0000000..aa1a5cb
--- /dev/null
+++ b/database/migrations/2024_12_14_083409_create_media_items_table.php
@@ -0,0 +1,48 @@
+mediumIncrements('id');
+
+ // Relación polimórfica
+ $table->unsignedMediumInteger('mediaable_id');
+ $table->string('mediaable_type');
+
+ $table->unsignedTinyInteger('type')->index(); // Tipo de medio: 'image', 'video', 'file', 'youtube'
+ $table->unsignedTinyInteger('sub_type')->index(); // Subtipo de medio: 'thumbnail', 'main', 'additional'
+
+ $table->string('url', 255)->nullable(); // URL del medio
+ $table->string('path')->nullable(); // Ruta del archivo si está almacenado localmente
+
+ $table->string('title')->nullable()->index(); // Título del medio
+ $table->mediumText('description')->nullable(); // Descripción del medio
+ $table->unsignedTinyInteger('order')->nullable(); // Orden de presentación
+
+ // Authoría
+ $table->timestamps();
+
+ // Índices
+ $table->index(['mediaable_type', 'mediaable_id']);
+ $table->index(['mediaable_type', 'mediaable_id', 'type']);
+ });
+
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('images');
+ }
+};
diff --git a/database/migrations/2024_12_14_092026_create_audits_table.php b/database/migrations/2024_12_14_092026_create_audits_table.php
new file mode 100644
index 0000000..709069d
--- /dev/null
+++ b/database/migrations/2024_12_14_092026_create_audits_table.php
@@ -0,0 +1,52 @@
+create($table, function (Blueprint $table) {
+
+ $morphPrefix = config('audit.user.morph_prefix', 'user');
+
+ $table->bigIncrements('id');
+ $table->string($morphPrefix . '_type')->nullable();
+ $table->unsignedBigInteger($morphPrefix . '_id')->nullable();
+ $table->string('event');
+ $table->morphs('auditable');
+ $table->text('old_values')->nullable();
+ $table->text('new_values')->nullable();
+ $table->text('url')->nullable();
+ $table->ipAddress('ip_address')->nullable();
+ $table->string('user_agent', 1023)->nullable();
+ $table->string('tags')->nullable();
+ $table->timestamps();
+
+ $table->index([$morphPrefix . '_id', $morphPrefix . '_type']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ $connection = config('audit.drivers.database.connection', config('database.default'));
+ $table = config('audit.drivers.database.table', 'audits');
+
+ Schema::connection($connection)->drop($table);
+ }
+};
diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php
new file mode 100644
index 0000000..88a695c
--- /dev/null
+++ b/database/seeders/PermissionSeeder.php
@@ -0,0 +1,14 @@
+ 'Quimiplastic S.A de C.V.',
+ 'app_faviconIcon' => '../assets/img/logo/koneko-02.png',
+ 'app_name' => 'Quimiplastic',
+ 'app_imageLogo' => '../assets/img/logo/koneko-02.png',
+
+ 'app_myLayout' => 'vertical',
+ 'app_myTheme' => 'theme-default',
+ 'app_myStyle' => 'light',
+ 'app_navbarType' => 'sticky',
+ 'app_menuFixed' => true,
+ 'app_menuCollapsed' => false,
+ 'app_headerType' => 'static',
+ 'app_showDropdownOnHover' => false,
+ 'app_authViewMode' => 'cover',
+ 'app_maxQuickLinks' => 5,
+
+
+
+ 'smtp.host' => 'webmail.koneko.mx',
+ 'smtp.port' => 465,
+ 'smtp.encryption' => 'tls',
+ 'smtp.username' => 'no-responder@koneko.mx',
+ 'smtp.password' => null,
+ 'smtp.from_email' => 'no-responder@koneko.mx',
+ 'smtp.from_name' => 'Koneko Soluciones en Tecnología',
+ 'smtp.reply_to_method' => 'smtp',
+ 'smtp.reply_to_email' => null,
+ 'smtp.reply_to_name' => null,
+
+
+
+ 'website.title',
+ 'website.favicon',
+ 'website.description',
+ 'website.image_logo',
+ 'website.image_logoDark',
+
+ 'admin.title',
+ 'admin.favicon',
+ 'admin.description',
+ 'admin.image_logo',
+ 'admin.image_logoDark',
+
+
+ 'favicon.icon' => null,
+
+ 'contact.phone_number' => '(222) 462 0903',
+ 'contact.phone_number_ext' => 'Ext. 5',
+ 'contact.email' => 'virtualcompras@live.com.mx',
+ 'contact.form.email' => 'contacto@conciergetravellife.com',
+ 'contact.form.email_cc' => 'arturo@koneko.mx',
+ 'contact.form.subject' => 'Has recibido un mensaje del formulario de covirsast.com',
+ 'contact.direccion' => '51 PTE 505 loc. 14, Puebla, Pue.',
+ 'contact.horario' => '9am - 7 pm',
+ 'contact.location.lat' => '19.024439',
+ 'contact.location.lng' => '-98.215777',
+
+ 'social.whatsapp' => '',
+ 'social.whatsapp.message' => '👋 Hola! Estoy buscando más información sobre Covirsa Soluciones en Tecnología. ¿Podrías proporcionarme los detalles que necesito? ¡Te lo agradecería mucho! 💻✨',
+
+ 'social.facebook' => 'https://www.facebook.com/covirsast/?locale=es_LA',
+ 'social.Whatsapp' => '2228 200 201',
+ 'social.Whatsapp.message' => '¡Hola! 🌟 Estoy interesado en obtener más información acerca de Concierge Travel. ¿Podrías ayudarme con los detalles? ¡Gracias de antemano! ✈️🏝',
+ 'social.Facebook' => 'test',
+ 'social.Instagram' => 'test',
+ 'social.Linkedin' => 'test',
+ 'social.Tiktok' => 'test',
+ 'social.X_twitter' => 'test',
+ 'social.Google' => 'test',
+ 'social.Pinterest' => 'test',
+ 'social.Youtube' => 'test',
+ 'social.Vimeo' => 'test',
+
+
+ 'chat.provider' => '',
+ 'chat.whatsapp.number' => '',
+ 'chat.whatsapp.message' => '👋 Hola! Estoy buscando más información sobre Covirsa Soluciones en Tecnología. ¿Podrías proporcionarme los detalles que necesito? ¡Te lo agradecería mucho! 💻✨',
+
+ 'webTpl.container' => 'custom-container',
+*/];
+
+ foreach ($settings_array as $key => $value) {
+ Setting::create([
+ 'key' => $key,
+ 'value' => $value,
+ ]);
+ };
+ }
+}
diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php
new file mode 100644
index 0000000..658a6bc
--- /dev/null
+++ b/database/seeders/UserSeeder.php
@@ -0,0 +1,97 @@
+exists($directory))
+ Storage::disk($disk)->deleteDirectory($directory);
+
+ //
+ $avatarImageService = new AvatarImageService();
+
+ // Super admin
+ $user = User::create([
+ 'name' => 'Koneko Admin',
+ 'email' => 'arturo@koneko.mx',
+ 'email_verified_at' => now(),
+ 'password' => bcrypt('LAdmin123'),
+ 'status' => User::STATUS_ENABLED,
+ ])->assignRole('SuperAdmin');
+
+ // Actualizamos la foto
+ $avatarImageService->updateProfilePhoto($user, new UploadedFile(
+ 'public/vendor/vuexy-admin/img/logo/koneko-02.png',
+ 'koneko-02.png'
+ ));
+
+
+ // admin
+ $avatarImageService = User::create([
+ 'name' => 'Admin',
+ 'email' => 'admin@koneko.mx',
+ 'email_verified_at' => now(),
+ 'password' => bcrypt('LAdmin123'),
+ 'status' => User::STATUS_ENABLED,
+ ])->assignRole('Admin');
+
+ $avatarImageService->updateProfilePhoto($user, new UploadedFile(
+ 'public/vendor/vuexy-admin/img/logo/koneko-03.png',
+ 'koneko-03.png'
+ ));
+
+ // Almacenista
+ $avatarImageService = User::create([
+ 'name' => 'Almacenista',
+ 'email' => 'almacenista@koneko.mx',
+ 'email_verified_at' => now(),
+ 'password' => bcrypt('LAdmin123'),
+ 'status' => User::STATUS_ENABLED,
+ ])->assignRole('Almacenista');
+
+ $avatarImageService->updateProfilePhoto($user, new UploadedFile(
+ 'public/vendor/vuexy-admin/img/logo/koneko-03.png',
+ 'koneko-03.png'
+ ));
+
+
+ // Usuarios CSV
+ $csvFile = fopen(base_path("database/data/users.csv"), "r");
+
+ $firstline = true;
+
+ while (($data = fgetcsv($csvFile, 2000, ",")) !== FALSE) {
+ if (!$firstline) {
+ User::create([
+ 'name' => $data['0'],
+ 'email' => $data['1'],
+ 'email_verified_at' => now(),
+ 'password' => bcrypt($data['3']),
+ 'status' => User::STATUS_ENABLED,
+ ])->assignRole($data['2']);
+ }
+
+ $firstline = false;
+ }
+
+ fclose($csvFile);
+ }
+}
diff --git a/resources/assets/css/demo.css b/resources/assets/css/demo.css
new file mode 100644
index 0000000..ec996c1
--- /dev/null
+++ b/resources/assets/css/demo.css
@@ -0,0 +1,129 @@
+/*
+* demo.css
+* File include item demo only specific css only
+******************************************************************************/
+
+.light-style .menu .app-brand.demo {
+ height: 64px;
+}
+
+.dark-style .menu .app-brand.demo {
+ height: 64px;
+}
+
+.app-brand-logo.demo {
+ -ms-flex-align: center;
+ align-items: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ display: -ms-flexbox;
+ display: flex;
+ width: 34px;
+ height: 24px;
+}
+
+.app-brand-logo.demo svg {
+ width: 35px;
+ height: 24px;
+}
+
+.app-brand-text.demo {
+ font-size: 1.375rem;
+}
+
+/* ! For .layout-navbar-fixed added fix padding top tpo .layout-page */
+.layout-navbar-fixed .layout-wrapper:not(.layout-without-menu) .layout-page {
+ padding-top: 64px !important;
+}
+.layout-navbar-fixed .layout-wrapper:not(.layout-horizontal):not(.layout-without-menu) .layout-page {
+ padding-top: 72px !important;
+}
+/* Navbar page z-index issue solution */
+.content-wrapper .navbar {
+ z-index: auto;
+}
+
+/*
+* Content
+******************************************************************************/
+
+.demo-blocks > * {
+ display: block !important;
+}
+
+.demo-inline-spacing > * {
+ margin: 1rem 0.375rem 0 0 !important;
+}
+
+/* ? .demo-vertical-spacing class is used to have vertical margins between elements. To remove margin-top from the first-child, use .demo-only-element class with .demo-vertical-spacing class. For example, we have used this class in forms-input-groups.html file. */
+.demo-vertical-spacing > * {
+ margin-top: 1rem !important;
+ margin-bottom: 0 !important;
+}
+.demo-vertical-spacing.demo-only-element > :first-child {
+ margin-top: 0 !important;
+}
+
+.demo-vertical-spacing-lg > * {
+ margin-top: 1.875rem !important;
+ margin-bottom: 0 !important;
+}
+.demo-vertical-spacing-lg.demo-only-element > :first-child {
+ margin-top: 0 !important;
+}
+
+.demo-vertical-spacing-xl > * {
+ margin-top: 5rem !important;
+ margin-bottom: 0 !important;
+}
+.demo-vertical-spacing-xl.demo-only-element > :first-child {
+ margin-top: 0 !important;
+}
+
+.rtl-only {
+ display: none !important;
+ text-align: left !important;
+ direction: ltr !important;
+}
+
+[dir='rtl'] .rtl-only {
+ display: block !important;
+}
+
+/* Dropdown buttons going out of small screens */
+@media (max-width: 576px) {
+ #dropdown-variation-demo .btn-group .text-truncate {
+ width: 254px;
+ position: relative;
+ }
+ #dropdown-variation-demo .btn-group .text-truncate::after {
+ position: absolute;
+ top: 45%;
+ right: 0.65rem;
+ }
+}
+
+/*
+* Layout demo
+******************************************************************************/
+
+.layout-demo-wrapper {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ margin-top: 1rem;
+}
+.layout-demo-placeholder img {
+ width: 900px;
+}
+.layout-demo-info {
+ text-align: center;
+ margin-top: 1rem;
+}
diff --git a/resources/assets/js/bootstrap-table/bootstrapTableManager.js b/resources/assets/js/bootstrap-table/bootstrapTableManager.js
new file mode 100644
index 0000000..9e272c0
--- /dev/null
+++ b/resources/assets/js/bootstrap-table/bootstrapTableManager.js
@@ -0,0 +1,245 @@
+import '../../vendor/libs/bootstrap-table/bootstrap-table';
+import '../notifications/LivewireNotification';
+
+class BootstrapTableManager {
+ constructor(bootstrapTableWrap, config = {}) {
+ const defaultConfig = {
+ header: [],
+ format: [],
+ search_columns: [],
+ actionColumn: false,
+ height: 'auto',
+ minHeight: 300,
+ bottomMargin : 195,
+ search: true,
+ showColumns: true,
+ showColumnsToggleAll: true,
+ showExport: true,
+ exportfileName: 'datatTable',
+ exportWithDatetime: true,
+ showFullscreen: true,
+ showPaginationSwitch: true,
+ showRefresh: true,
+ showToggle: true,
+ /*
+ smartDisplay: false,
+ searchOnEnterKey: true,
+ showHeader: false,
+ showFooter: true,
+ showRefresh: true,
+ showToggle: true,
+ showFullscreen: true,
+ detailView: true,
+ searchAlign: 'right',
+ buttonsAlign: 'right',
+ toolbarAlign: 'left',
+ paginationVAlign: 'bottom',
+ paginationHAlign: 'right',
+ paginationDetailHAlign: 'left',
+ paginationSuccessivelySize: 5,
+ paginationPagesBySide: 3,
+ paginationUseIntermediate: true,
+ */
+ clickToSelect: true,
+ minimumCountColumns: 4,
+ fixedColumns: true,
+ fixedNumber: 1,
+ idField: 'id',
+ pagination: true,
+ pageList: [25, 50, 100, 500, 1000],
+ sortName: 'id',
+ sortOrder: 'asc',
+ cookie: false,
+ cookieExpire: '365d',
+ cookieIdTable: 'myTableCookies', // Nombre único para las cookies de la tabla
+ cookieStorage: 'localStorage',
+ cookiePath: '/',
+ };
+
+ this.$bootstrapTable = $('.bootstrap-table', bootstrapTableWrap);
+ this.$toolbar = $('.bt-toolbar', bootstrapTableWrap);
+ this.$searchColumns = $('.search_columns', bootstrapTableWrap);
+ this.$btnRefresh = $('.btn-refresh', bootstrapTableWrap);
+ this.$btnClearFilters = $('.btn-clear-filters', bootstrapTableWrap);
+
+ this.config = { ...defaultConfig, ...config };
+
+ this.config.toolbar = `${bootstrapTableWrap} .bt-toolbar`;
+ this.config.height = this.config.height == 'auto'? this.getTableHeight(): this.config.height;
+ this.config.cookieIdTable = this.config.exportWithDatetime? this.config.cookieIdTable + '-' + this.getFormattedDateYMDHm(): this.config.cookieIdTable;
+
+ this.tableFormatters = {}; // Mueve la carga de formatters aquí
+
+ this.initTable();
+ }
+
+ /**
+ * Calcula la altura de la tabla.
+ */
+ getTableHeight() {
+ const btHeight = window.innerHeight - this.$toolbar.height() - this.bottomMargin;
+
+ return btHeight < this.config.minHeight ? this.config.minHeight : btHeight;
+ }
+
+ /**
+ * Genera un ID único para la tabla basado en una cookie.
+ */
+ getCookieId() {
+ const generateShortHash = (str) => {
+ let hash = 0;
+
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+
+ hash = (hash << 5) - hash + char;
+ hash &= hash; // Convertir a entero de 32 bits
+ }
+
+ return Math.abs(hash).toString().substring(0, 12);
+ };
+
+ return `bootstrap-table-cache-${generateShortHash(this.config.title)}`;
+ }
+
+ /**
+ * Carga los formatters dinámicamente
+ */
+ async loadFormatters() {
+ const formattersModules = import.meta.glob('../../../../../**/resources/assets/js/bootstrap-table/*Formatters.js');
+
+ const formatterPromises = Object.entries(formattersModules).map(async ([path, importer]) => {
+ const module = await importer();
+ Object.assign(this.tableFormatters, module);
+ });
+
+ await Promise.all(formatterPromises);
+ }
+
+ btColumns() {
+ const columns = [];
+
+ Object.entries(this.config.header).forEach(([key, value]) => {
+ const columnFormat = this.config.format[key] || {};
+
+ if (typeof columnFormat.formatter === 'object') {
+ const formatterName = columnFormat.formatter.name;
+ const formatterParams = columnFormat.formatter.params || {};
+
+ const formatterFunction = this.tableFormatters[formatterName];
+ if (formatterFunction) {
+ columnFormat.formatter = (value, row, index) => formatterFunction(value, row, index, formatterParams);
+ } else {
+ console.warn(`Formatter "${formatterName}" no encontrado para la columna "${key}"`);
+ }
+ } else if (typeof columnFormat.formatter === 'string') {
+ const formatterFunction = this.tableFormatters[columnFormat.formatter];
+ if (formatterFunction) {
+ columnFormat.formatter = formatterFunction;
+ }
+ }
+
+ if (columnFormat.onlyFormatter) {
+ columns.push({
+ align: 'center',
+ formatter: columnFormat.formatter || (() => ''),
+ forceHide: true,
+ switchable: false,
+ field: key,
+ title: value,
+ });
+ return;
+ }
+
+ const column = {
+ title: value,
+ field: key,
+ sortable: true,
+ };
+
+ columns.push({ ...column, ...columnFormat });
+ });
+
+ return columns;
+ }
+
+
+
+ /**
+ * Petición AJAX para la tabla.
+ */
+ ajaxRequest(params) {
+ const url = `${window.location.href}?${$.param(params.data)}&${$('.bt-toolbar :input').serialize()}`;
+
+ $.get(url).then((res) => params.success(res));
+ }
+
+ toValidFilename(str, extension = 'txt') {
+ return str
+ .normalize("NFD") // 🔹 Normaliza caracteres con tilde
+ .replace(/[\u0300-\u036f]/g, "") // 🔹 Elimina acentos y diacríticos
+ .replace(/[<>:"\/\\|?*\x00-\x1F]/g, '') // 🔹 Elimina caracteres inválidos
+ .replace(/\s+/g, '-') // 🔹 Reemplaza espacios con guiones
+ .replace(/-+/g, '-') // 🔹 Evita múltiples guiones seguidos
+ .replace(/^-+|-+$/g, '') // 🔹 Elimina guiones al inicio y fin
+ .toLowerCase() // 🔹 Convierte a minúsculas
+ + (extension ? '.' + extension.replace(/^\.+/, '') : ''); // 🔹 Asegura la extensión válida
+ }
+
+ getFormattedDateYMDHm(date = new Date()) {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0'); // 🔹 Asegura dos dígitos
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+
+ return `${year}${month}${day}-${hours}${minutes}`;
+ }
+
+
+ /**
+ * Inicia la tabla después de cargar los formatters
+ */
+ async initTable() {
+ await this.loadFormatters(); // Asegura que los formatters estén listos antes de inicializar
+
+ this.$bootstrapTable
+ .bootstrapTable('destroy').bootstrapTable({
+ height: this.config.height,
+ locale: 'es-MX',
+ ajax: (params) => this.ajaxRequest(params),
+ toolbar: this.config.toolbar,
+ search: this.config.search,
+ showColumns: this.config.showColumns,
+ showColumnsToggleAll: this.config.showColumnsToggleAll,
+ showExport: this.config.showExport,
+ exportTypes: ['csv', 'txt', 'xlsx'],
+ exportOptions: {
+ fileName: this.config.fileName,
+ },
+ showFullscreen: this.config.showFullscreen,
+ showPaginationSwitch: this.config.showPaginationSwitch,
+ showRefresh: this.config.showRefresh,
+ showToggle: this.config.showToggle,
+ clickToSelect: this.config.clickToSelect,
+ minimumCountColumns: this.config.minimumCountColumns,
+ fixedColumns: this.config.fixedColumns,
+ fixedNumber: this.config.fixedNumber,
+ idField: this.config.idField,
+ pagination: this.config.pagination,
+ pageList: this.config.pageList,
+ sidePagination: "server",
+ sortName: this.config.sortName,
+ sortOrder: this.config.sortOrder,
+ mobileResponsive: true,
+ resizable: true,
+ cookie: this.config.cookie,
+ cookieExpire: this.config.cookieExpire,
+ cookieIdTable: this.config.cookieIdTable,
+ columns: this.btColumns(),
+ });
+ }
+
+}
+
+window.BootstrapTableManager = BootstrapTableManager;
diff --git a/resources/assets/js/bootstrap-table/globalConfig.js b/resources/assets/js/bootstrap-table/globalConfig.js
new file mode 100644
index 0000000..7276fd9
--- /dev/null
+++ b/resources/assets/js/bootstrap-table/globalConfig.js
@@ -0,0 +1,132 @@
+const appRoutesElement = document.getElementById('app-routes');
+
+export const routes = appRoutesElement ? JSON.parse(appRoutesElement.textContent) : {};
+
+export const booleanStatusCatalog = {
+ activo: {
+ trueText: 'Activo',
+ falseText: 'Inactivo',
+ trueClass: 'badge bg-label-success',
+ falseClass: 'badge bg-label-danger',
+ },
+ habilitado: {
+ trueText: 'Habilitado',
+ falseText: 'Deshabilitado',
+ trueClass: 'badge bg-label-success',
+ falseClass: 'badge bg-label-danger',
+ trueIcon: 'ti ti-checkup-list',
+ falseIcon: 'ti ti-ban',
+ },
+ checkSI: {
+ trueText: 'SI',
+ falseIcon: '',
+ trueClass: 'badge bg-label-info',
+ falseText: '',
+ },
+ check: {
+ trueIcon: 'ti ti-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ checkbox: {
+ trueIcon: 'ti ti-checkbox',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ checklist: {
+ trueIcon: 'ti ti-checklist',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ phone_done: {
+ trueIcon: 'ti ti-phone-done',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ checkup_list: {
+ trueIcon: 'ti ti-checkup-list',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ list_check: {
+ trueIcon: 'ti ti-list-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ camera_check: {
+ trueIcon: 'ti ti-camera-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ mail_check: {
+ trueIcon: 'ti ti-mail-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ clock_check: {
+ trueIcon: 'ti ti-clock-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ user_check: {
+ trueIcon: 'ti ti-user-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ circle_check: {
+ trueIcon: 'ti ti-circle-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ shield_check: {
+ trueIcon: 'ti ti-shield-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ },
+ calendar_check: {
+ trueIcon: 'ti ti-calendar-check',
+ falseIcon: '',
+ trueClass: 'text-green-800',
+ falseClass: '',
+ }
+};
+
+export const badgeColorCatalog = {
+ primary: { color: 'primary' },
+ secondary: { color: 'secondary' },
+ success: { color: 'success' },
+ danger: { color: 'danger' },
+ warning: { color: 'warning' },
+ info: { color: 'info' },
+ dark: { color: 'dark' },
+ light: { color: 'light', textColor: 'text-dark' }
+};
+
+export const statusIntBadgeBgCatalogCss = {
+ 1: 'warning',
+ 2: 'info',
+ 10: 'success',
+ 12: 'danger',
+ 11: 'warning'
+};
+
+export const statusIntBadgeBgCatalog = {
+ 1: 'Inactivo',
+ 2: 'En proceso',
+ 10: 'Activo',
+ 11: 'Archivado',
+ 12: 'Cancelado',
+};
+
diff --git a/resources/assets/js/bootstrap-table/globalFormatters.js b/resources/assets/js/bootstrap-table/globalFormatters.js
new file mode 100644
index 0000000..909c5ab
--- /dev/null
+++ b/resources/assets/js/bootstrap-table/globalFormatters.js
@@ -0,0 +1,193 @@
+import { booleanStatusCatalog, statusIntBadgeBgCatalogCss, statusIntBadgeBgCatalog } from './globalConfig';
+import {routes} from '../../../../../laravel-vuexy-admin/resources/assets/js/bootstrap-table/globalConfig.js';
+
+export const userActionFormatter = (value, row, index) => {
+ if (!row.id) return '';
+
+ const showUrl = routes['admin.user.show'].replace(':id', row.id);
+ const editUrl = routes['admin.user.edit'].replace(':id', row.id);
+ const deleteUrl = routes['admin.user.delete'].replace(':id', row.id);
+
+ return `
+
+ `.trim();
+};
+
+export const dynamicBooleanFormatter = (value, row, index, options = {}) => {
+ const { tag = 'default', customOptions = {} } = options;
+ const catalogConfig = booleanStatusCatalog[tag] || {};
+
+ const finalOptions = {
+ ...catalogConfig,
+ ...customOptions, // Permite sobreescribir la configuración predeterminada
+ ...options // Permite pasar opciones rápidas
+ };
+
+ const {
+ trueIcon = '',
+ falseIcon = '',
+ trueText = 'Sí',
+ falseText = 'No',
+ trueClass = 'badge bg-label-success',
+ falseClass = 'badge bg-label-danger',
+ iconClass = 'text-green-800'
+ } = finalOptions;
+
+ const trueElement = !trueIcon && !trueText ? '' : `${trueIcon ? ` ` : ''}${trueText} `;
+ const falseElement = !falseIcon && !falseText ? '' : `${falseIcon ? ` ` : ''}${falseText} `;
+
+ return value? trueElement : falseElement;
+};
+
+export const dynamicBadgeFormatter = (value, row, index, options = {}) => {
+ const {
+ color = 'primary', // Valor por defecto
+ textColor = '', // Permite agregar color de texto si es necesario
+ additionalClass = '' // Permite añadir clases adicionales
+ } = options;
+
+ return `${value} `;
+};
+
+export const statusIntBadgeBgFormatter = (value, row, index) => {
+ return value
+ ? `${statusIntBadgeBgCatalog[value]} `
+ : '';
+}
+
+export const textNowrapFormatter = (value, row, index) => {
+ if (!value) return '';
+ return `${value} `;
+}
+
+
+export const toCurrencyFormatter = (value, row, index) => {
+ return isNaN(value) ? '' : Number(value).toCurrency();
+}
+
+export const numberFormatter = (value, row, index) => {
+ return isNaN(value) ? '' : Number(value);
+}
+
+export const monthFormatter = (value, row, index) => {
+ switch (parseInt(value)) {
+ case 1:
+ return 'Enero';
+ case 2:
+ return 'Febrero';
+ case 3:
+ return 'Marzo';
+ case 4:
+ return 'Abril';
+ case 5:
+ return 'Mayo';
+ case 6:
+ return 'Junio';
+ case 7:
+ return 'Julio';
+ case 8:
+ return 'Agosto';
+ case 9:
+ return 'Septiembre';
+ case 10:
+ return 'Octubre';
+ case 11:
+ return 'Noviembre';
+ case 12:
+ return 'Diciembre';
+ }
+}
+
+export const humaneTimeFormatter = (value, row, index) => {
+ return isNaN(value) ? '' : Number(value).humaneTime();
+}
+
+/**
+ * Genera la URL del avatar basado en iniciales o devuelve la foto de perfil si está disponible.
+ * @param {string} fullName - Nombre completo del usuario.
+ * @param {string|null} profilePhoto - Ruta de la foto de perfil.
+ * @returns {string} - URL del avatar.
+ */
+function getAvatarUrl(fullName, profilePhoto) {
+ const baseUrl = window.baseUrl || '';
+
+ if (profilePhoto) {
+ return `${baseUrl}storage/profile-photos/${profilePhoto}`;
+ }
+
+ return `${baseUrl}admin/usuario/avatar/?name=${fullName}`;
+}
+
+/**
+ * Formatea la columna del perfil de usuario con avatar, nombre y correo.
+ */
+export const userProfileFormatter = (value, row, index) => {
+ if (!row.id) return '';
+
+ const profileUrl = routes['admin.user.show'].replace(':id', row.id);
+ const avatar = getAvatarUrl(row.full_name, row.profile_photo_path);
+ const email = row.email ? row.email : 'Sin correo';
+
+ return `
+
+ `;
+};
+
+/**
+ * Formatea la columna del perfil de contacto con avatar, nombre y correo.
+ */
+export const contactProfileFormatter = (value, row, index) => {
+ if (!row.id) return '';
+
+ const profileUrl = routes['admin.contact.show'].replace(':id', row.id);
+ const avatar = getAvatarUrl(row.full_name, row.profile_photo_path);
+ const email = row.email ? row.email : 'Sin correo';
+
+ return `
+
+ `;
+};
+
+
+
+export const creatorFormatter = (value, row, index) => {
+ if (!row.creator) return '';
+
+ const email = row.creator_email || 'Sin correo';
+ const showUrl = routes['admin.user.show'].replace(':id', row.id);
+
+
+ return `
+
+ `;
+};
+
diff --git a/resources/assets/js/config.js b/resources/assets/js/config.js
new file mode 100644
index 0000000..a1316d7
--- /dev/null
+++ b/resources/assets/js/config.js
@@ -0,0 +1,53 @@
+/**
+ * Config
+ * -------------------------------------------------------------------------------------
+ * ! IMPORTANT: Make sure you clear the browser local storage In order to see the config changes in the template.
+ * ! To clear local storage: (https://www.leadshook.com/help/how-to-clear-local-storage-in-google-chrome-browser/).
+ */
+
+'use strict';
+
+// JS global variables
+window.config = {
+ colors: {
+ primary: '#7367f0',
+ secondary: '#808390',
+ success: '#28c76f',
+ info: '#00bad1',
+ warning: '#ff9f43',
+ danger: '#FF4C51',
+ dark: '#4b4b4b',
+ black: '#000',
+ white: '#fff',
+ cardColor: '#fff',
+ bodyBg: '#f8f7fa',
+ bodyColor: '#6d6b77',
+ headingColor: '#444050',
+ textMuted: '#acaab1',
+ borderColor: '#e6e6e8'
+ },
+ colors_label: {
+ primary: '#7367f029',
+ secondary: '#a8aaae29',
+ success: '#28c76f29',
+ info: '#00cfe829',
+ warning: '#ff9f4329',
+ danger: '#ea545529',
+ dark: '#4b4b4b29'
+ },
+ colors_dark: {
+ cardColor: '#2f3349',
+ bodyBg: '#25293c',
+ bodyColor: '#b2b1cb',
+ headingColor: '#cfcce4',
+ textMuted: '#8285a0',
+ borderColor: '#565b79'
+ },
+ enableMenuLocalStorage: true // Enable menu state with local storage support
+};
+
+window.assetsPath = document.documentElement.getAttribute('data-assets-path');
+window.baseUrl = document.documentElement.getAttribute('data-base-url');
+window.quicklinksUpdateUrl = document.documentElement.getAttribute('data-quicklinks-update-url');
+window.templateName = document.documentElement.getAttribute('data-template');
+window.rtlSupport = false; // set true for rtl support (rtl + ltr), false for ltr only.
diff --git a/resources/assets/js/forms/formConvasHelper.js b/resources/assets/js/forms/formConvasHelper.js
new file mode 100644
index 0000000..3dc6b70
--- /dev/null
+++ b/resources/assets/js/forms/formConvasHelper.js
@@ -0,0 +1,477 @@
+/**
+ * FormCanvasHelper
+ *
+ * Clase para orquestar la interacción entre un formulario dentro de un Offcanvas
+ * de Bootstrap y el estado de Livewire (modo create/edit/delete), además de
+ * manipular ciertos componentes externos como Select2.
+ *
+ * Se diseñó teniendo en cuenta que el DOM del Offcanvas puede reconstruirse
+ * (re-render) de manera frecuente, por lo que muchos getters reacceden al DOM
+ * dinámicamente.
+ */
+export default class FormCanvasHelper {
+ /**
+ * @param {string} offcanvasId - ID del elemento Offcanvas en el DOM.
+ * @param {object} liveWireInstance - Instancia de Livewire asociada al formulario.
+ */
+ constructor(offcanvasId, liveWireInstance) {
+ this.offcanvasId = offcanvasId;
+ this.liveWireInstance = liveWireInstance;
+
+ // Validamos referencias mínimas para evitar errores tempranos
+ // Si alguna falta, se mostrará un error en consola.
+ this.validateInitialDomRefs();
+ }
+
+ /**
+ * Verifica la existencia básica de elementos en el DOM.
+ * Muestra errores en consola si faltan elementos críticos.
+ */
+ validateInitialDomRefs() {
+ const offcanvasEl = document.getElementById(this.offcanvasId);
+
+ if (!offcanvasEl) {
+ console.error(`❌ No se encontró el contenedor Offcanvas con ID: ${this.offcanvasId}`);
+ return;
+ }
+
+ const formEl = offcanvasEl.querySelector('form');
+ if (!formEl) {
+ console.error(`❌ No se encontró el formulario dentro de #${this.offcanvasId}`);
+ return;
+ }
+
+ const offcanvasTitle = offcanvasEl.querySelector('.offcanvas-title');
+ const submitButtons = formEl.querySelectorAll('.btn-submit');
+ const resetButtons = formEl.querySelectorAll('.btn-reset');
+
+ if (!offcanvasTitle || !submitButtons.length || !resetButtons.length) {
+ console.error(`❌ Faltan el título, botones de submit o reset dentro de #${this.offcanvasId}`);
+ }
+ }
+
+ /**
+ * Getter para el contenedor Offcanvas actual.
+ * Retorna siempre la referencia más reciente del DOM.
+ */
+ get offcanvasEl() {
+ return document.getElementById(this.offcanvasId);
+ }
+
+ /**
+ * Getter para el formulario dentro del Offcanvas.
+ */
+ get formEl() {
+ return this.offcanvasEl?.querySelector('form') ?? null;
+ }
+
+ /**
+ * Getter para el título del Offcanvas.
+ */
+ get offcanvasTitleEl() {
+ return this.offcanvasEl?.querySelector('.offcanvas-title') ?? null;
+ }
+
+ /**
+ * Getter para la instancia de Bootstrap Offcanvas.
+ * Siempre retorna la instancia más reciente en caso de re-render.
+ */
+ get offcanvasInstance() {
+ if (!this.offcanvasEl) return null;
+ return bootstrap.Offcanvas.getOrCreateInstance(this.offcanvasEl);
+ }
+
+ /**
+ * Retorna todos los botones de submit en el formulario.
+ */
+ get submitButtons() {
+ return this.formEl?.querySelectorAll('.btn-submit') ?? [];
+ }
+
+ /**
+ * Retorna todos los botones de reset en el formulario.
+ */
+ get resetButtons() {
+ return this.formEl?.querySelectorAll('.btn-reset') ?? [];
+ }
+
+ /**
+ * Método principal para manejar la recarga del Offcanvas según los estados en Livewire.
+ * Se encarga de resetear el formulario, limpiar errores y cerrar/abrir el Offcanvas
+ * según sea necesario.
+ *
+ * @param {string|null} triggerMode - Forzar la acción (e.g., 'reset', 'create'). Si no se especifica, se verifica según Livewire.
+ */
+ reloadOffcanvas(triggerMode = null) {
+ setTimeout(() => {
+ const mode = this.liveWireInstance.get('mode');
+ const successProcess = this.liveWireInstance.get('successProcess');
+ const validationError = this.liveWireInstance.get('validationError');
+
+ // Si se completa la acción o triggerMode = 'reset',
+ // reseteamos completamente y cerramos el Offcanvas.
+ if (triggerMode === 'reset' || successProcess) {
+ this.resetFormAndState('create');
+
+ return;
+ }
+
+ // Forzar modo create si se solicita explícitamente
+ if (triggerMode === 'create') {
+ // Evitamos re-reset si ya estamos en 'create'
+ if (mode === 'create') return;
+
+ this.resetFormAndState('create');
+
+ this.focusOnOpen();
+
+ return;
+ }
+
+ // Si no, simplemente preparamos la UI según el modo actual.
+ this.prepareOffcanvasUI(mode);
+
+ // Si hay errores de validación, reabrimos el Offcanvas para mostrarlos.
+ if (validationError) {
+ this.liveWireInstance.set('validationError', null, false);
+
+ return;
+ }
+
+ // Si estamos en edit o delete, solo abrimos el Offcanvas.
+ if (mode === 'edit' || mode === 'delete') {
+ this.clearErrors();
+
+ if(mode === 'edit') {
+ this.focusOnOpen();
+ }
+
+ return;
+ }
+ }, 20);
+ }
+
+ /**
+ * Reabre o fuerza la apertura del Offcanvas si hay errores de validación
+ * o si el modo de Livewire es 'edit' o 'delete'.
+ *
+ * Normalmente se llama cuando hay un dispatch/evento de Livewire,
+ * por ejemplo si el servidor devuelve un error de validación (para mostrarlo)
+ * o si se acaba de cargar un registro para editar o eliminar.
+ *
+ * - Si hay `validationError`, forzamos la reapertura con `toggleOffcanvas(true, true)`
+ * para que se refresque correctamente y el usuario vea los errores.
+ * - Si el modo es 'edit' o 'delete', simplemente mostramos el Offcanvas sin forzar
+ * un refresco de la interfaz.
+ */
+ refresh() {
+ setTimeout(() => {
+ const mode = this.liveWireInstance.get('mode');
+ const successProcess = this.liveWireInstance.get('successProcess');
+ const validationError = this.liveWireInstance.get('validationError');
+
+ // cerramos el Offcanvas.
+ if (successProcess) {
+ this.toggleOffcanvas(false);
+
+ this.resetFormAndState('create');
+
+ return;
+ }
+
+
+ if (validationError) {
+ // Forzamos la reapertura para que se rendericen
+ this.toggleOffcanvas(true, true);
+
+ return;
+ }
+
+ if (mode === 'edit' || mode === 'delete') {
+ // Abrimos el Offcanvas para edición o eliminación
+ this.toggleOffcanvas(true);
+
+ return;
+ }
+ }, 10);
+ }
+
+
+ /**
+ * Prepara la UI del Offcanvas según el modo actual: cambia texto de botones, título,
+ * habilita o deshabilita campos, etc.
+ *
+ * @param {string} mode - Modo actual en Livewire: 'create', 'edit' o 'delete'
+ */
+ prepareOffcanvasUI(mode) {
+ // Configura el texto y estilo de botones
+ this.configureButtons(mode);
+
+ // Ajusta el título del Offcanvas
+ this.configureTitle(mode);
+
+ // Activa o desactiva campos según el modo
+ this.configureReadonlyMode(mode === 'delete');
+ }
+
+ /**
+ * Cierra o muestra el Offcanvas.
+ *
+ * @param {boolean} show - true para mostrar, false para ocultar.
+ * @param {boolean} force - true para forzar el refresco rápido del Offcanvas.
+ */
+ toggleOffcanvas(show = false, force = false) {
+ const instance = this.offcanvasInstance;
+
+ if (!instance) return;
+
+ if (show) {
+ if (force) {
+ // "Force" hace un hide + show para asegurar un nuevo render
+ instance.hide();
+ setTimeout(() => instance.show(), 10);
+
+ } else {
+ instance.show();
+ }
+
+ } else {
+ instance.hide();
+ }
+ }
+
+ /**
+ * Resetea el formulario y el estado en Livewire (modo, id, errores).
+ *
+ * @param {string} targetMode - Modo al que queremos resetear, típicamente 'create'.
+ */
+ resetFormAndState(targetMode) {
+ if (!this.formEl) return;
+
+ // Restablecemos en Livewire
+ this.liveWireInstance.set('successProcess', null, false);
+ this.liveWireInstance.set('validationError', null, false);
+ this.liveWireInstance.set('mode', targetMode, false);
+ this.liveWireInstance.set('id', null, false);
+
+ // Limpiamos el formulario
+ this.formEl.reset();
+ this.clearErrors();
+
+ // Restablecemos valores por defecto del formulario
+ const defaults = this.liveWireInstance.get('defaultValues');
+ if (defaults && typeof defaults === 'object') {
+ Object.entries(defaults).forEach(([key, value]) => {
+ this.liveWireInstance.set(key, value, false);
+ });
+ }
+
+ // Limpiar select2 automáticamente
+ $(this.formEl)
+ .find('select.select2-hidden-accessible')
+ .each(function () {
+ $(this).val(null).trigger('change');
+ });
+
+ // Desactivamos el modo lectura
+ this.configureReadonlyMode(false);
+
+ // Reconfiguramos el Offcanvas UI
+ this.prepareOffcanvasUI(targetMode);
+ }
+
+ /**
+ * Configura el texto y estilo de los botones de submit y reset
+ * según el modo de Livewire.
+ *
+ * @param {string} mode - 'create', 'edit' o 'delete'
+ */
+ configureButtons(mode) {
+ const singularName = this.liveWireInstance.get('singularName');
+
+ // Limpiar clases previas
+ this.submitButtons.forEach(button => {
+ button.classList.remove('btn-danger', 'btn-primary');
+ });
+ this.resetButtons.forEach(button => {
+ button.classList.remove('btn-text-secondary', 'btn-label-secondary');
+ });
+
+ // Configurar botón de submit según el modo
+ this.submitButtons.forEach(button => {
+ switch (mode) {
+ case 'create':
+ button.classList.add('btn-primary');
+ button.textContent = `Crear ${singularName.toLowerCase()}`;
+ break;
+ case 'edit':
+ button.classList.add('btn-primary');
+ button.textContent = `Guardar cambios`;
+ break;
+ case 'delete':
+ button.classList.add('btn-danger');
+ button.textContent = `Eliminar ${singularName.toLowerCase()}`;
+ break;
+ }
+ });
+
+ // Configurar botones de reset según el modo
+ this.resetButtons.forEach(button => {
+ // Cambia la clase dependiendo si se trata de un modo 'delete' o no
+ const buttonClass = (mode === 'delete') ? 'btn-text-secondary' : 'btn-label-secondary';
+ button.classList.add(buttonClass);
+ });
+ }
+
+ /**
+ * Ajusta el título del Offcanvas según el modo y la propiedad configurada en Livewire.
+ *
+ * @param {string} mode - 'create', 'edit' o 'delete'
+ */
+ configureTitle(mode) {
+ if (!this.offcanvasTitleEl) return;
+
+ const capitalizeFirstLetter =(str) => {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+ }
+
+ const singularName = this.liveWireInstance.get('singularName');
+ const columnNameLabel = this.liveWireInstance.get('columnNameLabel');
+ const editName = this.liveWireInstance.get(columnNameLabel);
+
+ switch (mode) {
+ case 'create':
+ this.offcanvasTitleEl.innerHTML = ` ${capitalizeFirstLetter(singularName)} `;
+ break;
+ case 'edit':
+ this.offcanvasTitleEl.innerHTML = `${editName} `;
+ break;
+ case 'delete':
+ this.offcanvasTitleEl.innerHTML = `${editName} `;
+ break;
+ }
+ }
+
+ /**
+ * Configura el modo de solo lectura/edición en los campos del formulario.
+ * Deshabilita inputs y maneja el "readonly" en checkboxes/radios.
+ *
+ * @param {boolean} readOnly - true si queremos modo lectura, false para edición.
+ */
+ configureReadonlyMode(readOnly) {
+ if (!this.formEl) return;
+
+ const inputs = this.formEl.querySelectorAll('input, textarea, select');
+
+ inputs.forEach(el => {
+ // Saltar campos marcados como "data-always-enabled"
+ if (el.hasAttribute('data-always-enabled')) return;
+
+ // Para selects
+ if (el.tagName === 'SELECT') {
+ if ($(el).hasClass('select2-hidden-accessible')) {
+ // Deshabilitar select2
+ $(el).prop('disabled', readOnly).trigger('change.select2');
+ } else {
+ this.toggleSelectReadonly(el, readOnly);
+ }
+ return;
+ }
+
+ // Para checkboxes / radios
+ if (el.type === 'checkbox' || el.type === 'radio') {
+ this.toggleCheckboxReadonly(el, readOnly);
+ return;
+ }
+
+ // Para inputs de texto / textarea
+ el.readOnly = readOnly;
+ });
+ }
+
+ /**
+ * Alterna modo "readonly" en un checkbox/radio simulando la inhabilitación
+ * sin marcarlo como 'disabled' (para mantener su apariencia).
+ *
+ * @param {HTMLElement} checkbox - Elemento checkbox o radio.
+ * @param {boolean} enabled - true si se quiere modo lectura, false en caso contrario.
+ */
+ toggleCheckboxReadonly(checkbox, enabled) {
+ if (enabled) {
+ checkbox.setAttribute('readonly-mode', 'true');
+ checkbox.style.pointerEvents = 'none';
+ checkbox.onclick = function (event) {
+ event.preventDefault();
+ };
+ } else {
+ checkbox.removeAttribute('readonly-mode');
+ checkbox.style.pointerEvents = '';
+ checkbox.onclick = null;
+ }
+ }
+
+ /**
+ * Alterna modo "readonly" para un convencional.
+ *
+ * @param {HTMLElement} select - Elemento select.
+ * @param {boolean} enabled - true si queremos readonly, false si editable.
+ */
+ toggleSelectReadonly(select, enabled) {
+ if (enabled) {
+ select.setAttribute('readonly-mode', 'true');
+ select.style.pointerEvents = 'none';
+ select.tabIndex = -1;
+ } else {
+ select.removeAttribute('readonly-mode');
+ select.style.pointerEvents = '';
+ select.tabIndex = '';
+ }
+ }
+
+ /**
+ * Hace focus en el elemento con el selector dado.
+ */
+ focusOnOpen() {
+ const focusSelector = this.liveWireInstance.get('focusOnOpen'); // Obtiene el selector de Livewire
+
+ if (!focusSelector) return;
+
+ setTimeout(() => {
+ // Buscar el elemento real en el DOM
+ const focusElement = document.getElementById(focusSelector);
+
+ // Si existe, hacer focus
+ if (focusElement) {
+ focusElement.focus();
+ } else {
+ console.warn(`Elemento no encontrado: ${focusSelector}`);
+ }
+ }, 250);
+ }
+
+ /**
+ * Limpia mensajes de error y la clase 'is-invalid' en el formulario.
+ */
+ clearErrors() {
+ if (!this.formEl) return;
+
+ // Remover mensajes de error en texto
+ this.formEl.querySelectorAll('.text-danger').forEach(el => el.remove());
+
+ // Remover la clase 'is-invalid' de los inputs afectados
+ this.formEl.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
+
+ // Remover las notificaciones
+ this.formEl.querySelectorAll('.notification-container').forEach(el => el.innerHTML = '');
+
+ // Removemos el checkbox de confirmación de eliminar
+ const confirmDeletion = this.formEl.querySelector('.confirm-deletion');
+
+ if (confirmDeletion) {
+ confirmDeletion.remove();
+ }
+ }
+}
+
+// Exponemos la clase en window para acceso global (si fuese necesario)
+window.FormCanvasHelper = FormCanvasHelper;
diff --git a/resources/assets/js/forms/formCustomListener.js b/resources/assets/js/forms/formCustomListener.js
new file mode 100644
index 0000000..644a7d3
--- /dev/null
+++ b/resources/assets/js/forms/formCustomListener.js
@@ -0,0 +1,245 @@
+export default class FormCustomListener {
+ constructor(config = {}) {
+ const defaultConfig = {
+ formSelector: '.form-custom-listener', // Selector para formularios
+ buttonSelectors: [], // Selectores específicos para botones
+ callbacks: [], // Callbacks correspondientes a los botones específicos
+ allowedInputTags: ['INPUT', 'SELECT', 'TEXTAREA'], // Tags permitidos para cambios
+ validationConfig: null, // Nueva propiedad para la configuración de validación
+ dispatchOnSubmit: null // Callback Livewire para disparar al enviar el formulario
+ };
+
+ this.config = { ...defaultConfig, ...config };
+
+ // Aseguramos que los métodos que dependen de `this` estén vinculados al contexto correcto
+ this.defaultButtonHandler = this.defaultButtonHandler.bind(this);
+ this.formValidationInstance = null;
+
+ this.initForms();
+ }
+
+ /**
+ * Inicializa los formularios encontrados en el DOM.
+ */
+ initForms() {
+ const forms = document.querySelectorAll(this.config.formSelector);
+
+ if (forms.length === 0) {
+ console.error(`No se encontraron formularios con el selector ${this.config.formSelector}.`);
+ return;
+ }
+
+ forms.forEach(form => {
+ if (form.dataset.initialized === 'true') {
+ console.warn(`Formulario ya inicializado: ${form}`);
+ return;
+ }
+
+ this.initFormEvents(form);
+
+ // Si se pasó configuración de validación, inicialízala
+ if (this.config.validationConfig) {
+ this.initializeValidation(form);
+ }
+
+ form.dataset.initialized = 'true'; // Marcar formulario como inicializado
+ });
+ }
+
+ /**
+ * Configura los eventos para un formulario individual.
+ * @param {HTMLElement} form - El formulario que será manejado.
+ */
+ initFormEvents(form) {
+ const buttons = this.getButtons(form);
+
+ buttons.forEach(({ button, callback }, index) => {
+ if (button) {
+ button.addEventListener('click', () => {
+ this.handleButtonClick(index, form, buttons, callback);
+ });
+ }
+ });
+
+ form.addEventListener('input', event =>
+ this.handleInputChange(
+ event,
+ form,
+ buttons.map(b => b.button)
+ )
+ );
+ }
+
+ /**
+ * Obtiene los botones y sus callbacks según la configuración.
+ * @param {HTMLElement} form - El formulario del cual obtener botones.
+ * @returns {Array} Array de objetos con { button, callback }.
+ */
+ getButtons(form) {
+ const buttons = [];
+
+ this.config.buttonSelectors.forEach((selector, index) => {
+ const buttonList = Array.from(form.querySelectorAll(selector));
+ const callback = this.config.callbacks[index];
+
+ buttonList.forEach(button => {
+ buttons.push({ button, callback });
+ });
+ });
+
+ return buttons;
+ }
+
+ /**
+ * Maneja los cambios en los campos de entrada.
+ * @param {Event} event - El evento del cambio.
+ * @param {HTMLElement} form - El formulario actual.
+ * @param {HTMLElement[]} buttons - Array de botones en el formulario.
+ */
+ handleInputChange(event, form, buttons) {
+ const target = event.target;
+
+ if (['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) {
+ this.toggleButtonsState(buttons, true);
+ }
+ }
+
+ /**
+ * Maneja el clic en un botón específico.
+ * @param {number} index - Índice del botón.
+ * @param {HTMLElement} form - El formulario actual.
+ * @param {Array} buttons - Array de objetos { button, callback }.
+ * @param {function|null} callback - Callback definido para el botón.
+ */
+ handleButtonClick(index, form, buttons, callback) {
+ if (typeof callback === 'function') {
+ callback(
+ form,
+ buttons[index].button,
+ buttons.map(b => b.button)
+ );
+ } else {
+ this.defaultButtonHandler(
+ form,
+ buttons[index].button,
+ buttons.map(b => b.button)
+ );
+ }
+ }
+
+ /**
+ * Maneja la acción cuando el formulario es válido.
+ * Este método puede ser sobreescrito para personalizar el comportamiento.
+ */
+ handleFormValid(form) {
+ // Ejecutar callback opcional (si lo proporcionaste)
+ if (typeof this.config.handleValidForm === 'function') {
+ this.config.handleValidForm(form);
+ } else if (this.config.dispatchOnSubmit) {
+ this.handleValidForm(form);
+ } else {
+ form.submit();
+ }
+ }
+
+ /**
+ * Método que maneja la acción cuando el formulario es válido.
+ * Al ser un método de la clase, no necesitamos usar bind.
+ */
+ handleValidForm(form) {
+ const saveButton = form.querySelector('#save_website_button');
+ const allButtons = Array.from(form.querySelectorAll('.btn'));
+
+ this.toggleButtonsState(allButtons, false); // Deshabilitar todos los botones
+ this.toggleFormFields(form, false); // Deshabilitar todos los campos del formulario
+ this.setButtonLoadingState(saveButton, true); // Poner en estado de carga al botón anfitrión
+
+ // Enviar la solicitud de Livewire correspondiente al enviar el formulario
+ Livewire.dispatch(this.config.dispatchOnSubmit);
+ }
+
+ /**
+ * Manejador por defecto para los botones.
+ * @param {HTMLElement} form - El formulario actual.
+ * @param {HTMLElement} hostButton - El botón anfitrión que disparó el evento.
+ * @param {HTMLElement[]} allButtons - Todos los botones relevantes del formulario.
+ */
+ defaultButtonHandler(form, hostButton, allButtons) {
+ this.toggleButtonsState(allButtons, false); // Deshabilitar todos los botones
+ this.toggleFormFields(form, false); // Deshabilitar todos los campos del formulario
+ this.setButtonLoadingState(hostButton, true); // Poner en estado de carga al botón anfitrión
+ }
+
+ /**
+ * Deshabilita o habilita los campos del formulario.
+ * @param {HTMLElement} form - El formulario actual.
+ * @param {boolean} isEnabled - Si los campos deben habilitarse.
+ */
+ toggleFormFields(form, isEnabled) {
+ form.querySelectorAll('input, select, textarea').forEach(field => {
+ field.disabled = !isEnabled;
+ });
+ }
+
+ /**
+ * Habilita o deshabilita los botones.
+ * @param {HTMLElement[]} buttons - Array de botones.
+ * @param {boolean} isEnabled - Si los botones deben habilitarse.
+ */
+ toggleButtonsState(buttons, isEnabled) {
+ buttons.forEach(button => {
+ if (button) button.disabled = !isEnabled;
+ });
+ }
+
+ /**
+ * Cambia el estado de carga de un botón.
+ * @param {HTMLElement} button - Botón que se manejará.
+ * @param {boolean} isLoading - Si el botón está en estado de carga.
+ */
+ setButtonLoadingState(button, isLoading) {
+ if (!button) return;
+
+ const loadingText = button.getAttribute('data-loading-text');
+ if (loadingText && isLoading) {
+ button.setAttribute('data-original-text', button.innerHTML);
+ button.innerHTML = loadingText;
+ button.disabled = true;
+ } else if (!isLoading) {
+ button.innerHTML = button.getAttribute('data-original-text') || button.innerHTML;
+ button.disabled = false;
+ }
+ }
+
+ /**
+ * Inicializa la validación del formulario con la configuración proporcionada.
+ * @param {HTMLElement} form - El formulario que va a ser validado.
+ */
+ initializeValidation(form) {
+ if (this.config.validationConfig) {
+ this.formValidationInstance = FormValidation.formValidation(form, this.config.validationConfig).on(
+ 'core.form.valid',
+ () => this.handleFormValid(form)
+ );
+ }
+ }
+
+ reloadValidation() {
+ const form = document.querySelector(this.config.formSelector);
+
+ // Verificar si el formulario existe y si la validación está inicializada
+ if (form && this.formValidationInstance) {
+ try {
+ // En lugar de destruir la validación, simplemente reiniciamos la validación.
+ this.formValidationInstance.resetForm(); // Resetear el formulario, limpiando los errores
+
+ // Reinicializar la validación con la configuración actual
+ this.initializeValidation(form);
+ } catch (error) {
+ console.error('Error al reiniciar la validación:', error);
+ }
+ } else {
+ console.warn('Formulario no encontrado o instancia de validación no disponible.');
+ }
+ }
+}
diff --git a/resources/assets/js/layout/quicklinks-navbar.js b/resources/assets/js/layout/quicklinks-navbar.js
new file mode 100644
index 0000000..1fd95cb
--- /dev/null
+++ b/resources/assets/js/layout/quicklinks-navbar.js
@@ -0,0 +1,56 @@
+/**
+ * Quicklinks Navbar
+ */
+
+'use strict';
+
+$(function () {
+ // Navbar Quicklinks with autosuggest (typeahead)
+ const $dropdownShortcuts = $('.dropdown-shortcuts'),
+ $dropdownShortcutsAdd = $('.dropdown-shortcuts-add'),
+ $dropdownShortcutsRemove = $('.dropdown-shortcuts-remove');
+
+ const route = document.documentElement.getAttribute('data-route');
+
+ if ($dropdownShortcuts.length) {
+ $dropdownShortcutsAdd.on('click', function () {
+ $.ajax({
+ url: quicklinksUpdateUrl,
+ method: 'POST',
+ data: {
+ action: 'update',
+ route: route
+ },
+ headers: {
+ 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
+ },
+ success: function (response) {
+ location.reload();
+ },
+ error: function (xhr) {
+ console.error(xhr.responseJSON.message);
+ }
+ });
+ });
+
+ $dropdownShortcutsRemove.on('click', function () {
+ $.ajax({
+ url: quicklinksUpdateUrl,
+ method: 'POST',
+ data: {
+ action: 'remove',
+ route: route
+ },
+ headers: {
+ 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
+ },
+ success: function (response) {
+ location.reload();
+ },
+ error: function (xhr) {
+ console.error(xhr.responseJSON.message);
+ }
+ });
+ });
+ }
+});
diff --git a/resources/assets/js/layout/search-navbar.js b/resources/assets/js/layout/search-navbar.js
new file mode 100644
index 0000000..7eb2085
--- /dev/null
+++ b/resources/assets/js/layout/search-navbar.js
@@ -0,0 +1,201 @@
+/**
+ * Search Navbar
+ */
+
+'use strict';
+
+$(function () {
+ window.Helpers.initSidebarToggle();
+ // Toggle Universal Sidebar
+
+ // Navbar Search with autosuggest (typeahead)
+ var searchToggler = $('.search-toggler'),
+ searchInputWrapper = $('.search-input-wrapper'),
+ searchInput = $('.search-input'),
+ contentBackdrop = $('.content-backdrop');
+
+ // Open search input on click of search icon
+ if (searchToggler.length) {
+ searchToggler.on('click', function () {
+ if (searchInputWrapper.length) {
+ searchInputWrapper.toggleClass('d-none');
+ searchInput.trigger('focus');
+ }
+ });
+
+ document.addEventListener('keydown', function (event) {
+ const ctrlKey = event.ctrlKey;
+ const slashKey = event.key === '/'; // Usa 'key' para obtener la tecla como texto
+
+ if (ctrlKey && slashKey) {
+ const searchInputWrapper = document.querySelector('.search-input-wrapper');
+ const searchInput = document.querySelector('.search-input');
+
+ if (searchInputWrapper) {
+ searchInputWrapper.classList.toggle('d-none'); // Alterna la visibilidad
+ if (searchInput) {
+ searchInput.focus(); // Coloca el foco en el input
+ }
+ }
+ }
+ });
+
+ // Note: Following code is required to update container class of typeahead dropdown width on focus of search input. setTimeout is required to allow time to initiate Typeahead UI.
+ setTimeout(function () {
+ var twitterTypeahead = $('.twitter-typeahead');
+
+ searchInput.on('focus', function () {
+ if (searchInputWrapper.hasClass('container-xxl')) {
+ searchInputWrapper.find(twitterTypeahead).addClass('container-xxl');
+ twitterTypeahead.removeClass('container-fluid');
+ } else if (searchInputWrapper.hasClass('container-fluid')) {
+ searchInputWrapper.find(twitterTypeahead).addClass('container-fluid');
+ twitterTypeahead.removeClass('container-xxl');
+ }
+ });
+ }, 10);
+ }
+
+ if (searchInput.length) {
+ // Función para normalizar cadenas (eliminar acentos)
+ function normalizeString(str) {
+ return str
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .toLowerCase();
+ }
+
+ // Filter config con soporte para ignorar acentos
+ var filterConfig = function (data) {
+ return function findMatches(q, cb) {
+ let matches = [];
+
+ // Normalizar la consulta
+ const normalizedQuery = normalizeString(q);
+
+ data.filter(function (i) {
+ const normalizedName = normalizeString(i.name);
+
+ if (normalizedName.startsWith(normalizedQuery)) {
+ matches.push(i);
+ } else if (
+ !normalizedName.startsWith(normalizedQuery) &&
+ normalizedName.includes(normalizedQuery)
+ ) {
+ matches.push(i);
+
+ // Ordenar por coincidencia secundaria
+ matches.sort(function (a, b) {
+ return b.name < a.name ? 1 : -1;
+ });
+ }
+ });
+
+ cb(matches);
+ };
+ };
+
+ // Search JSON
+ var searchJson = 'search-navbar'; // For vertical layout
+
+ if ($('#layout-menu').hasClass('menu-horizontal')) {
+ var searchJson = 'search-navbar'; // For vertical layout
+ }
+
+ // Search API AJAX call
+ var searchData = $.ajax({
+ url: assetsPath + '../../admin/' + searchJson, //? Use your own search api instead
+ dataType: 'json',
+ async: false
+ }).responseJSON;
+
+ // Init typeahead on searchInput
+ searchInput.each(function () {
+ var $this = $(this);
+
+ searchInput
+ .typeahead(
+ {
+ hint: false,
+ classNames: {
+ menu: 'tt-menu navbar-search-suggestion',
+ cursor: 'active',
+ suggestion: 'suggestion d-flex justify-content-between px-4 py-2 w-100'
+ }
+ },
+ // Páginas
+ {
+ name: 'pages',
+ display: 'name',
+ limit: 8,
+ source: filterConfig(searchData.pages),
+ templates: {
+ header: '',
+ suggestion: function ({ url, icon, name }) {
+ return (
+ '' +
+ '' +
+ ' ' +
+ '' +
+ name +
+ ' ' +
+ '
' +
+ ' '
+ );
+ },
+ notFound:
+ '' +
+ '' +
+ '
No se encontro resultados
' +
+ '
'
+ }
+ }
+ )
+ //On typeahead result render.
+ .on('typeahead:render', function () {
+ // Show content backdrop,
+ contentBackdrop.addClass('show').removeClass('fade');
+ })
+ // On typeahead select
+ .on('typeahead:select', function (ev, suggestion) {
+ // Open selected page
+ if (suggestion.url !== 'javascript:;') window.location = suggestion.url;
+ })
+ // On typeahead close
+ .on('typeahead:close', function () {
+ // Clear search
+ searchInput.val('');
+ $this.typeahead('val', '');
+
+ // Hide search input wrapper
+ searchInputWrapper.addClass('d-none');
+
+ // Fade content backdrop
+ contentBackdrop.addClass('fade').removeClass('show');
+ });
+
+ // On searchInput keyup, Fade content backdrop if search input is blank
+ searchInput.on('keyup', function () {
+ if (searchInput.val() == '') contentBackdrop.addClass('fade').removeClass('show');
+ });
+ });
+
+ // Init PerfectScrollbar in search result
+ var psSearch;
+
+ $('.navbar-search-suggestion').each(function () {
+ psSearch = new PerfectScrollbar($(this)[0], {
+ wheelPropagation: false,
+ suppressScrollX: true
+ });
+ });
+
+ searchInput.on('keyup', function () {
+ psSearch.update();
+ });
+ }
+});
diff --git a/resources/assets/js/main.js b/resources/assets/js/main.js
new file mode 100644
index 0000000..25da9e3
--- /dev/null
+++ b/resources/assets/js/main.js
@@ -0,0 +1,375 @@
+import './layout/quicklinks-navbar.js';
+import './layout/search-navbar.js';
+
+('use strict');
+
+window.isRtl = window.Helpers.isRtl();
+window.isDarkStyle = window.Helpers.isDarkStyle();
+
+let menu,
+ animate,
+ isHorizontalLayout = false;
+
+if (document.getElementById('layout-menu')) {
+ isHorizontalLayout = document.getElementById('layout-menu').classList.contains('menu-horizontal');
+}
+
+(function () {
+ setTimeout(function () {
+ window.Helpers.initCustomOptionCheck();
+ }, 1000);
+
+ if (typeof Waves !== 'undefined') {
+ Waves.init();
+ Waves.attach(
+ ".btn[class*='btn-']:not(.position-relative):not([class*='btn-outline-']):not([class*='btn-label-'])",
+ ['waves-light']
+ );
+ Waves.attach("[class*='btn-outline-']:not(.position-relative)");
+ Waves.attach("[class*='btn-label-']:not(.position-relative)");
+ Waves.attach('.pagination .page-item .page-link');
+ Waves.attach('.dropdown-menu .dropdown-item');
+ Waves.attach('.light-style .list-group .list-group-item-action');
+ Waves.attach('.dark-style .list-group .list-group-item-action', ['waves-light']);
+ Waves.attach('.nav-tabs:not(.nav-tabs-widget) .nav-item .nav-link');
+ Waves.attach('.nav-pills .nav-item .nav-link', ['waves-light']);
+ }
+
+ // Initialize menu
+ //-----------------
+
+ let layoutMenuEl = document.querySelectorAll('#layout-menu');
+ layoutMenuEl.forEach(function (element) {
+ menu = new Menu(element, {
+ orientation: isHorizontalLayout ? 'horizontal' : 'vertical',
+ closeChildren: isHorizontalLayout ? true : false,
+ // ? This option only works with Horizontal menu
+ showDropdownOnHover: localStorage.getItem('templateCustomizer-' + templateName + '--ShowDropdownOnHover') // If value(showDropdownOnHover) is set in local storage
+ ? localStorage.getItem('templateCustomizer-' + templateName + '--ShowDropdownOnHover') === 'true' // Use the local storage value
+ : window.templateCustomizer !== undefined // If value is set in config.js
+ ? window.templateCustomizer.settings.defaultShowDropdownOnHover // Use the config.js value
+ : true // Use this if you are not using the config.js and want to set value directly from here
+ });
+ // Change parameter to true if you want scroll animation
+ window.Helpers.scrollToActive((animate = false));
+ window.Helpers.mainMenu = menu;
+ });
+
+ // Initialize menu togglers and bind click on each
+ let menuToggler = document.querySelectorAll('.layout-menu-toggle');
+ menuToggler.forEach(item => {
+ item.addEventListener('click', event => {
+ event.preventDefault();
+ window.Helpers.toggleCollapsed();
+ // Enable menu state with local storage support if enableMenuLocalStorage = true from config.js
+ if (config.enableMenuLocalStorage && !window.Helpers.isSmallScreen()) {
+ try {
+ localStorage.setItem(
+ 'templateCustomizer-' + templateName + '--LayoutCollapsed',
+ String(window.Helpers.isCollapsed())
+ );
+ // Update customizer checkbox state on click of menu toggler
+ let layoutCollapsedCustomizerOptions = document.querySelector(
+ '.template-customizer-layouts-options'
+ );
+ if (layoutCollapsedCustomizerOptions) {
+ let layoutCollapsedVal = window.Helpers.isCollapsed() ? 'collapsed' : 'expanded';
+ layoutCollapsedCustomizerOptions.querySelector(`input[value="${layoutCollapsedVal}"]`).click();
+ }
+ } catch (e) {}
+ }
+ });
+ });
+
+ // Menu swipe gesture
+
+ // Detect swipe gesture on the target element and call swipe In
+ window.Helpers.swipeIn('.drag-target', function (e) {
+ window.Helpers.setCollapsed(false);
+ });
+
+ // Detect swipe gesture on the target element and call swipe Out
+ window.Helpers.swipeOut('#layout-menu', function (e) {
+ if (window.Helpers.isSmallScreen()) window.Helpers.setCollapsed(true);
+ });
+
+ // Display in main menu when menu scrolls
+ let menuInnerContainer = document.getElementsByClassName('menu-inner'),
+ menuInnerShadow = document.getElementsByClassName('menu-inner-shadow')[0];
+ if (menuInnerContainer.length > 0 && menuInnerShadow) {
+ menuInnerContainer[0].addEventListener('ps-scroll-y', function () {
+ if (this.querySelector('.ps__thumb-y').offsetTop) {
+ menuInnerShadow.style.display = 'block';
+ } else {
+ menuInnerShadow.style.display = 'none';
+ }
+ });
+ }
+
+ // Update light/dark image based on current style
+ function switchImage(style) {
+ if (style === 'system') {
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ style = 'dark';
+ } else {
+ style = 'light';
+ }
+ }
+ const switchImagesList = [].slice.call(document.querySelectorAll('[data-app-' + style + '-img]'));
+ switchImagesList.map(function (imageEl) {
+ const setImage = imageEl.getAttribute('data-app-' + style + '-img');
+ imageEl.src = baseUrl + 'vendor/vuexy-admin/img/' + setImage; // Using window.assetsPath to get the exact relative path
+ });
+ }
+
+ //Style Switcher (Light/Dark/System Mode)
+ let styleSwitcher = document.querySelector('.dropdown-style-switcher');
+
+ // Active class on style switcher dropdown items
+ const activeStyle = document.documentElement.getAttribute('data-style');
+
+ // Get style from local storage or use 'system' as default
+ let storedStyle =
+ localStorage.getItem('templateCustomizer-' + templateName + '--Style') || //if no template style then use Customizer style
+ (window.templateCustomizer?.settings?.defaultStyle ?? 'light'); //!if there is no Customizer then use default style as light
+
+ // Set style on click of style switcher item if template customizer is enabled
+ if (window.templateCustomizer && styleSwitcher) {
+ let styleSwitcherItems = [].slice.call(styleSwitcher.children[1].querySelectorAll('.dropdown-item'));
+ styleSwitcherItems.forEach(function (item) {
+ item.classList.remove('active');
+ item.addEventListener('click', function () {
+ let currentStyle = this.getAttribute('data-theme');
+ if (currentStyle === 'light') {
+ window.templateCustomizer.setStyle('light');
+ } else if (currentStyle === 'dark') {
+ window.templateCustomizer.setStyle('dark');
+ } else {
+ window.templateCustomizer.setStyle('system');
+ }
+ });
+
+ if (item.getAttribute('data-theme') === activeStyle) {
+ // Add 'active' class to the item if it matches the activeStyle
+ item.classList.add('active');
+ }
+ });
+
+ // Update style switcher icon based on the stored style
+
+ const styleSwitcherIcon = styleSwitcher.querySelector('i');
+
+ if (storedStyle === 'light') {
+ styleSwitcherIcon.classList.add('ti-sun');
+ new bootstrap.Tooltip(styleSwitcherIcon, {
+ title: 'Light Mode',
+ fallbackPlacements: ['bottom']
+ });
+ } else if (storedStyle === 'dark') {
+ styleSwitcherIcon.classList.add('ti-moon-stars');
+ new bootstrap.Tooltip(styleSwitcherIcon, {
+ title: 'Dark Mode',
+ fallbackPlacements: ['bottom']
+ });
+ } else {
+ styleSwitcherIcon.classList.add('ti-device-desktop-analytics');
+ new bootstrap.Tooltip(styleSwitcherIcon, {
+ title: 'System Mode',
+ fallbackPlacements: ['bottom']
+ });
+ }
+ }
+
+ // Run switchImage function based on the stored style
+ switchImage(storedStyle);
+
+ let languageDropdown = document.getElementsByClassName('dropdown-language');
+
+ if (languageDropdown.length) {
+ let dropdownItems = languageDropdown[0].querySelectorAll('.dropdown-item');
+ const dropdownActiveItem = languageDropdown[0].querySelector('.dropdown-item.active');
+
+ directionChange(dropdownActiveItem.dataset.textDirection);
+
+ for (let i = 0; i < dropdownItems.length; i++) {
+ dropdownItems[i].addEventListener('click', function () {
+ let textDirection = this.getAttribute('data-text-direction');
+ window.templateCustomizer.setLang(this.getAttribute('data-language'));
+ directionChange(textDirection);
+ });
+ }
+ function directionChange(textDirection) {
+ if (textDirection === 'rtl') {
+ if (localStorage.getItem('templateCustomizer-' + templateName + '--Rtl') !== 'true')
+ window.templateCustomizer ? window.templateCustomizer.setRtl(true) : '';
+ } else {
+ if (localStorage.getItem('templateCustomizer-' + templateName + '--Rtl') === 'true')
+ window.templateCustomizer ? window.templateCustomizer.setRtl(false) : '';
+ }
+ }
+ }
+
+ // add on click javascript for template customizer reset button id template-customizer-reset-btn
+
+ setTimeout(function () {
+ let templateCustomizerResetBtn = document.querySelector('.template-customizer-reset-btn');
+ if (templateCustomizerResetBtn) {
+ templateCustomizerResetBtn.onclick = function () {
+ window.location.href = baseUrl + 'lang/en';
+ };
+ }
+ }, 1500);
+
+ // Notification
+ // ------------
+ const notificationMarkAsReadAll = document.querySelector('.dropdown-notifications-all');
+ const notificationMarkAsReadList = document.querySelectorAll('.dropdown-notifications-read');
+
+ // Notification: Mark as all as read
+ if (notificationMarkAsReadAll) {
+ notificationMarkAsReadAll.addEventListener('click', event => {
+ notificationMarkAsReadList.forEach(item => {
+ item.closest('.dropdown-notifications-item').classList.add('marked-as-read');
+ });
+ });
+ }
+ // Notification: Mark as read/unread onclick of dot
+ if (notificationMarkAsReadList) {
+ notificationMarkAsReadList.forEach(item => {
+ item.addEventListener('click', event => {
+ item.closest('.dropdown-notifications-item').classList.toggle('marked-as-read');
+ });
+ });
+ }
+
+ // Notification: Mark as read/unread onclick of dot
+ const notificationArchiveMessageList = document.querySelectorAll('.dropdown-notifications-archive');
+ notificationArchiveMessageList.forEach(item => {
+ item.addEventListener('click', event => {
+ item.closest('.dropdown-notifications-item').remove();
+ });
+ });
+
+ // Init helpers & misc
+ // --------------------
+
+ // Init BS Tooltip
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
+ tooltipTriggerList.map(function (tooltipTriggerEl) {
+ return new bootstrap.Tooltip(tooltipTriggerEl);
+ });
+
+ // Accordion active class
+ const accordionActiveFunction = function (e) {
+ if (e.type == 'show.bs.collapse' || e.type == 'show.bs.collapse') {
+ e.target.closest('.accordion-item').classList.add('active');
+ } else {
+ e.target.closest('.accordion-item').classList.remove('active');
+ }
+ };
+
+ const accordionTriggerList = [].slice.call(document.querySelectorAll('.accordion'));
+ const accordionList = accordionTriggerList.map(function (accordionTriggerEl) {
+ accordionTriggerEl.addEventListener('show.bs.collapse', accordionActiveFunction);
+ accordionTriggerEl.addEventListener('hide.bs.collapse', accordionActiveFunction);
+ });
+
+ // If layout is RTL add .dropdown-menu-end class to .dropdown-menu
+ // if (isRtl) {
+ // Helpers._addClass('dropdown-menu-end', document.querySelectorAll('#layout-navbar .dropdown-menu'));
+ // }
+
+ // Auto update layout based on screen size
+ window.Helpers.setAutoUpdate(true);
+
+ // Toggle Password Visibility
+ window.Helpers.initPasswordToggle();
+
+ // Speech To Text
+ window.Helpers.initSpeechToText();
+
+ // Init PerfectScrollbar in Navbar Dropdown (i.e notification)
+ window.Helpers.initNavbarDropdownScrollbar();
+
+ let horizontalMenuTemplate = document.querySelector("[data-template^='horizontal-menu']");
+ if (horizontalMenuTemplate) {
+ // if screen size is small then set navbar fixed
+ if (window.innerWidth < window.Helpers.LAYOUT_BREAKPOINT) {
+ window.Helpers.setNavbarFixed('fixed');
+ } else {
+ window.Helpers.setNavbarFixed('');
+ }
+ }
+
+ // On window resize listener
+ // -------------------------
+ document.addEventListener(
+ 'resize',
+ function (event) {
+ // Hide open search input and set value blank
+ if (window.innerWidth >= window.Helpers.LAYOUT_BREAKPOINT) {
+ if (document.querySelector('.search-input-wrapper')) {
+ document.querySelector('.search-input-wrapper').classList.add('d-none');
+ document.querySelector('.search-input').value = '';
+ }
+ }
+ // Horizontal Layout : Update menu based on window size
+ if (horizontalMenuTemplate) {
+ // if screen size is small then set navbar fixed
+ if (window.innerWidth < window.Helpers.LAYOUT_BREAKPOINT) {
+ window.Helpers.setNavbarFixed('fixed');
+ } else {
+ window.Helpers.setNavbarFixed('');
+ }
+ setTimeout(function () {
+ if (window.innerWidth < window.Helpers.LAYOUT_BREAKPOINT) {
+ if (document.getElementById('layout-menu')) {
+ if (document.getElementById('layout-menu').classList.contains('menu-horizontal')) {
+ menu.switchMenu('vertical');
+ }
+ }
+ } else {
+ if (document.getElementById('layout-menu')) {
+ if (document.getElementById('layout-menu').classList.contains('menu-vertical')) {
+ menu.switchMenu('horizontal');
+ }
+ }
+ }
+ }, 100);
+ }
+ },
+ true
+ );
+
+ // Manage menu expanded/collapsed with templateCustomizer & local storage
+ //------------------------------------------------------------------
+
+ // If current layout is horizontal OR current window screen is small (overlay menu) than return from here
+ if (isHorizontalLayout || window.Helpers.isSmallScreen()) {
+ return;
+ }
+
+ // If current layout is vertical and current window screen is > small
+
+ // Auto update menu collapsed/expanded based on the themeConfig
+ if (typeof TemplateCustomizer !== 'undefined') {
+ if (window.templateCustomizer.settings.defaultMenuCollapsed) {
+ window.Helpers.setCollapsed(true, false);
+ } else {
+ window.Helpers.setCollapsed(false, false);
+ }
+ }
+
+ // Manage menu expanded/collapsed state with local storage support If enableMenuLocalStorage = true in config.js
+ if (typeof config !== 'undefined') {
+ if (config.enableMenuLocalStorage) {
+ try {
+ if (localStorage.getItem('templateCustomizer-' + templateName + '--LayoutCollapsed') !== null)
+ window.Helpers.setCollapsed(
+ localStorage.getItem('templateCustomizer-' + templateName + '--LayoutCollapsed') === 'true',
+ false
+ );
+ } catch (e) {}
+ }
+ }
+})();
diff --git a/resources/assets/js/maps/LeafletMapHelper.js b/resources/assets/js/maps/LeafletMapHelper.js
new file mode 100644
index 0000000..4821267
--- /dev/null
+++ b/resources/assets/js/maps/LeafletMapHelper.js
@@ -0,0 +1,133 @@
+import './../../vendor/libs/leaflet/leaflet'
+
+export const LeafletMapHelper = (() => {
+ let mapInstance, markerInstance;
+
+ const DEFAULT_COORDS = [19.4326, -99.1332]; // Coordenadas de CDMX por defecto
+
+ // Valida coordenadas
+ const isValidCoordinate = (lat, lng) => {
+ return lat && !isNaN(lat) && lat >= -90 && lat <= 90 && lat !== 0 &&
+ lng && !isNaN(lng) && lng >= -180 && lng <= 180 && lng !== 0;
+ };
+
+ // Crea opciones del mapa según el modo
+ const getMapOptions = (mode) => ({
+ scrollWheelZoom: mode !== 'delete',
+ dragging: mode !== 'delete',
+ doubleClickZoom: mode !== 'delete',
+ boxZoom: mode !== 'delete',
+ keyboard: mode !== 'delete',
+ zoomControl: mode !== 'delete',
+ touchZoom: mode !== 'delete'
+ });
+
+ // Destruir el mapa existente
+ const destroyMap = () => {
+ if (mapInstance) {
+ mapInstance.off();
+ mapInstance.remove();
+ mapInstance = null;
+ }
+ removeMarker();
+ };
+
+ // Crear marcador en el mapa
+ const createMarker = (lat, lng, draggable = false, onDragEnd) => {
+ if (isValidCoordinate(lat, lng)) {
+ markerInstance = L.marker([lat, lng], { draggable }).addTo(mapInstance)
+ .bindPopup('Ubicación seleccionada ').openPopup();
+
+ if (draggable && onDragEnd) {
+ markerInstance.on('dragend', (e) => {
+ const { lat, lng } = e.target.getLatLng();
+ onDragEnd(lat, lng);
+ });
+ }
+ }
+ };
+
+ // Eliminar marcador
+ const removeMarker = () => {
+ if (markerInstance) {
+ markerInstance.remove();
+ markerInstance = null;
+ }
+ };
+
+ // Actualizar coordenadas en formulario
+ const updateCoordinates = (lat, lng, latSelector, lngSelector, livewireInstance) => {
+ const latInput = document.querySelector(latSelector);
+ const lngInput = document.querySelector(lngSelector);
+
+ if (!latInput || !lngInput) {
+ console.warn(`⚠️ No se encontró el elemento del DOM para latitud (${latSelector}) o longitud (${lngSelector})`);
+ return;
+ }
+
+ latInput.value = lat ? lat.toFixed(6) : '';
+ lngInput.value = lng ? lng.toFixed(6) : '';
+
+ if (livewireInstance) {
+ livewireInstance.lat = lat ? lat.toFixed(6) : null;
+ livewireInstance.lng = lng ? lng.toFixed(6) : null;
+ }
+ };
+
+ // Inicializar el mapa
+ const initializeMap = (locationInputs, mode = 'create', livewireInstance = null) => {
+ const mapElement = document.getElementById(locationInputs.mapId);
+
+ if (!mapElement) {
+ console.error(`❌ No se encontró el contenedor del mapa con ID: ${locationInputs.mapId}`);
+ return;
+ }
+
+ let latElement = document.querySelector(locationInputs.lat);
+ let lngElement = document.querySelector(locationInputs.lng);
+
+ if (!latElement || !lngElement) {
+ console.error(`❌ No se encontraron los campos de latitud (${locationInputs.lat}) o longitud (${locationInputs.lng})`);
+ return;
+ }
+
+ let lat = parseFloat(latElement.value);
+ let lng = parseFloat(lngElement.value);
+
+ const mapCoords = isValidCoordinate(lat, lng) ? [lat, lng] : DEFAULT_COORDS;
+ const zoomLevel = isValidCoordinate(lat, lng) ? 16 : 13;
+
+ if (mapInstance) destroyMap();
+
+ mapInstance = L.map(locationInputs.mapId, getMapOptions(mode)).setView(mapCoords, zoomLevel);
+ L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png').addTo(mapInstance);
+
+ if (mode !== 'create') createMarker(lat, lng, mode === 'edit', (lat, lng) => updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance));
+
+ if (mode !== 'delete') {
+ mapInstance.on('click', (e) => {
+ const { lat, lng } = e.latlng;
+ removeMarker();
+ createMarker(lat, lng, true, (lat, lng) => updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance));
+ updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance);
+ });
+ }
+
+ /*
+ const btnClearElement = document.querySelector(locationInputs.btnClear);
+
+ if(!btnClearElement){
+ console.error(`❌ No se encontró el botón de limpiar con ID: ${locationInputs.btnClear}`);return;
+ }
+ */
+ };
+
+ return {
+ initializeMap,
+ clearCoordinates: () => {
+ removeMarker();
+ },
+ };
+})();
+
+window.LeafletMapHelper = LeafletMapHelper;
diff --git a/resources/assets/js/maps/LocationIQSearchHelper.js b/resources/assets/js/maps/LocationIQSearchHelper.js
new file mode 100644
index 0000000..18941d3
--- /dev/null
+++ b/resources/assets/js/maps/LocationIQSearchHelper.js
@@ -0,0 +1,12 @@
+export class LocationIQSearchHelper {
+ constructor(apiKey) {
+ this.apiKey = apiKey;
+ this.baseUrl = 'https://us1.locationiq.com/v1/search.php';
+ }
+
+ async searchAddress(query) {
+ const response = await fetch(`${this.baseUrl}?key=${this.apiKey}&q=${query}&format=json`);
+ if (!response.ok) throw new Error('Error al buscar la dirección');
+ return await response.json();
+ }
+}
diff --git a/resources/assets/js/notifications/LivewireNotification.js b/resources/assets/js/notifications/LivewireNotification.js
new file mode 100644
index 0000000..1a87713
--- /dev/null
+++ b/resources/assets/js/notifications/LivewireNotification.js
@@ -0,0 +1,207 @@
+export default class LivewireNotification {
+ constructor(config = {}) {
+ const defaultConfig = {
+ delay: 9000, // Tiempo predeterminado para las notificaciones
+ onNotificationShown: null, // Callback al mostrar una notificación
+ onNotificationRemoved: null, // Callback al eliminar una notificación
+ onNotificationClosed: null // Callback al cerrar una notificación mediante botón
+ };
+
+ this.config = { ...defaultConfig, ...config };
+ this.initLivewireNotification();
+ }
+
+ /**
+ * Inicializa la escucha de notificaciones desde Livewire.
+ */
+ initLivewireNotification() {
+ // Mostrar notificación almacenada después de la recarga
+ const storedNotification = localStorage.getItem('pendingNotification');
+
+ if (storedNotification) {
+ const event = JSON.parse(storedNotification);
+ this.showStoredNotification(event);
+ localStorage.removeItem('pendingNotification'); // Limpiar después de mostrar
+ }
+
+ // Escuchar nuevas notificaciones desde Livewire
+ Livewire.on('notification', event => {
+ if (event.deferReload) {
+ // Guardar la notificación en localStorage para mostrar después de la recarga
+ localStorage.setItem('pendingNotification', JSON.stringify(event));
+
+ window.location.reload();
+ } else {
+ // Mostrar la notificación inmediatamente
+ this.showNotification(event);
+ }
+ });
+
+ // Escuchar evento personalizado para almacenar la notificación en localStorage
+ document.addEventListener('store-notification', (event) => {
+ const notification = {
+ type: event.detail.type || 'info',
+ message: event.detail.message || 'Notificación',
+ delay: event.detail.delay || 5000,
+ target: event.detail.target || 'body'
+ };
+ localStorage.setItem('pendingNotification', JSON.stringify(notification));
+ });
+ }
+
+ /**
+ * Método para emitir notificaciones desde JavaScript.
+ * @param {Object} options - Opciones de la notificación.
+ * @param {Function} callback - Callback opcional que se ejecutará después de mostrar la notificación.
+ * @param {number} customTimeout - Timeout personalizado (opcional).
+ */
+ emitNotification(options, callback, customTimeout) {
+ const event = {
+ target: options.target || 'body',
+ message: options.message || 'Notificación',
+ type: options.type || 'info',
+ deferReload: options.deferReload || false,
+ delay: customTimeout || options.delay || this.config.delay // Usar el timeout personalizado o el predeterminado
+ };
+
+ // Mostrar la notificación
+ this.showNotification(event);
+
+ // Ejecutar callback si está definido
+ if (typeof callback === 'function') {
+ callback(event);
+ }
+ }
+
+ /**
+ * Muestra una notificación almacenada.
+ * @param {Object} event - Datos del evento de notificación.
+ */
+ showStoredNotification(event) {
+ this.showNotification(event);
+ }
+
+ /**
+ * Muestra una notificación.
+ * @param {Object} event - Datos del evento de notificación.
+ */
+ showNotification(event) {
+ setTimeout(() => {
+ const targetElement = document.querySelector(event.target);
+
+ if (!targetElement) {
+ console.error(`Target ${event.target} no encontrado. Mostrando en el contenedor global.`);
+
+ this.showInGlobalContainer(event);
+ return;
+ }
+
+ // Crear un contenedor para notificaciones si no existe
+ if (!targetElement.querySelector('.notification-container')) {
+ const container = document.createElement('div');
+
+ container.className = 'notification-container';
+ targetElement.appendChild(container);
+ }
+
+ const notificationContainer = targetElement.querySelector('.notification-container');
+ const notificationElement = this.renderNotification(notificationContainer, event);
+
+ // Callback opcional al mostrar la notificación
+ if (typeof this.config.onNotificationShown === 'function') {
+ this.config.onNotificationShown(notificationElement, event);
+ }
+
+ // Configurar el timeout para eliminar la notificación
+ this.setdelay(notificationElement, event);
+
+ // Configurar el evento para el botón de cierre
+ this.setupCloseButton(notificationElement, event);
+ }, 5);
+ }
+
+ /**
+ * Renderiza una notificación en el contenedor global (body).
+ * @param {Object} event - Datos del evento de notificación.
+ */
+ showInGlobalContainer(event) {
+ const globalContainer = document.body;
+ if (!globalContainer.querySelector('.notification-container')) {
+ const container = document.createElement('div');
+
+ container.className = 'notification-container';
+ globalContainer.appendChild(container);
+ }
+
+ const notificationContainer = globalContainer.querySelector('.notification-container');
+ const notificationElement = this.renderNotification(notificationContainer, event);
+
+ if (typeof this.config.onNotificationShown === 'function') {
+ this.config.onNotificationShown(notificationElement, event);
+ }
+
+ this.setdelay(notificationElement, event);
+ this.setupCloseButton(notificationElement, event);
+ }
+
+ /**
+ * Renderiza una notificación en el contenedor.
+ * @param {HTMLElement} container - Contenedor de notificaciones.
+ * @param {Object} event - Evento de notificación con tipo y mensaje.
+ * @returns {HTMLElement} - Elemento de la notificación recién creada.
+ */
+ renderNotification(container, event) {
+ const notificationElement = document.createElement('div');
+
+ notificationElement.className = `alert alert-${event.type} alert-dismissible fade show`;
+ notificationElement.role = 'alert';
+ notificationElement.innerHTML = `${event.message} `;
+
+ container.appendChild(notificationElement);
+
+ return notificationElement;
+ }
+
+ /**
+ * Configura un timeout para limpiar una notificación específica.
+ * @param {HTMLElement} notificationElement - Elemento de la notificación.
+ * @param {Object} event - Evento asociado a la notificación.
+ */
+ setdelay(notificationElement, event) {
+ const timeout = event.delay || this.config.delay;
+
+ setTimeout(() => {
+ if (notificationElement && notificationElement.parentElement) {
+ notificationElement.remove();
+
+ // Callback opcional al eliminar la notificación
+ if (typeof this.config.onNotificationRemoved === 'function') {
+ this.config.onNotificationRemoved(notificationElement, event);
+ }
+ }
+ }, timeout);
+ }
+
+ /**
+ * Configura el cierre manual de una notificación mediante el botón "Cerrar".
+ * @param {HTMLElement} notificationElement - Elemento de la notificación.
+ * @param {Object} event - Evento asociado a la notificación.
+ */
+ setupCloseButton(notificationElement, event) {
+ const closeButton = notificationElement.querySelector('.btn-close');
+ if (closeButton) {
+ closeButton.addEventListener('click', () => {
+ notificationElement.remove();
+
+ // Callback opcional al cerrar la notificación manualmente
+ if (typeof this.config.onNotificationClosed === 'function') {
+ this.config.onNotificationClosed(notificationElement, event);
+ }
+ });
+ }
+ }
+}
+
+if(!window.livewireNotification) {
+ window.livewireNotification = new LivewireNotification();
+}
diff --git a/resources/assets/vendor/fonts/bootstrap-icons.scss b/resources/assets/vendor/fonts/bootstrap-icons.scss
new file mode 100644
index 0000000..209e8cf
--- /dev/null
+++ b/resources/assets/vendor/fonts/bootstrap-icons.scss
@@ -0,0 +1,2090 @@
+/*!
+ * Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/)
+ * Copyright 2019-2024 The Bootstrap Authors
+ * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
+ */
+
+$bootstrap-icons-font: "bootstrap-icons" !default;
+$bootstrap-icons-font-dir: "bootstrap-icons" !default;
+$bootstrap-icons-font-file: "#{$bootstrap-icons-font-dir}/#{$bootstrap-icons-font}" !default;
+$bootstrap-icons-font-hash: "24e3eb84d0bcaf83d77f904c78ac1f47" !default;
+$bootstrap-icons-font-src: url("#{$bootstrap-icons-font-file}.woff2?#{$bootstrap-icons-font-hash}") format("woff2"),
+ url("#{$bootstrap-icons-font-file}.woff?#{$bootstrap-icons-font-hash}") format("woff") !default;
+
+@font-face {
+ font-display: block;
+ font-family: $bootstrap-icons-font;
+ src: $bootstrap-icons-font-src;
+}
+
+.bi::before,
+[class^="bi-"]::before,
+[class*=" bi-"]::before {
+ display: inline-block;
+ font-family: $bootstrap-icons-font !important;
+ font-style: normal;
+ font-weight: normal !important;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ vertical-align: -.125em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+$bootstrap-icons-map: (
+ "123": "\f67f",
+ "alarm-fill": "\f101",
+ "alarm": "\f102",
+ "align-bottom": "\f103",
+ "align-center": "\f104",
+ "align-end": "\f105",
+ "align-middle": "\f106",
+ "align-start": "\f107",
+ "align-top": "\f108",
+ "alt": "\f109",
+ "app-indicator": "\f10a",
+ "app": "\f10b",
+ "archive-fill": "\f10c",
+ "archive": "\f10d",
+ "arrow-90deg-down": "\f10e",
+ "arrow-90deg-left": "\f10f",
+ "arrow-90deg-right": "\f110",
+ "arrow-90deg-up": "\f111",
+ "arrow-bar-down": "\f112",
+ "arrow-bar-left": "\f113",
+ "arrow-bar-right": "\f114",
+ "arrow-bar-up": "\f115",
+ "arrow-clockwise": "\f116",
+ "arrow-counterclockwise": "\f117",
+ "arrow-down-circle-fill": "\f118",
+ "arrow-down-circle": "\f119",
+ "arrow-down-left-circle-fill": "\f11a",
+ "arrow-down-left-circle": "\f11b",
+ "arrow-down-left-square-fill": "\f11c",
+ "arrow-down-left-square": "\f11d",
+ "arrow-down-left": "\f11e",
+ "arrow-down-right-circle-fill": "\f11f",
+ "arrow-down-right-circle": "\f120",
+ "arrow-down-right-square-fill": "\f121",
+ "arrow-down-right-square": "\f122",
+ "arrow-down-right": "\f123",
+ "arrow-down-short": "\f124",
+ "arrow-down-square-fill": "\f125",
+ "arrow-down-square": "\f126",
+ "arrow-down-up": "\f127",
+ "arrow-down": "\f128",
+ "arrow-left-circle-fill": "\f129",
+ "arrow-left-circle": "\f12a",
+ "arrow-left-right": "\f12b",
+ "arrow-left-short": "\f12c",
+ "arrow-left-square-fill": "\f12d",
+ "arrow-left-square": "\f12e",
+ "arrow-left": "\f12f",
+ "arrow-repeat": "\f130",
+ "arrow-return-left": "\f131",
+ "arrow-return-right": "\f132",
+ "arrow-right-circle-fill": "\f133",
+ "arrow-right-circle": "\f134",
+ "arrow-right-short": "\f135",
+ "arrow-right-square-fill": "\f136",
+ "arrow-right-square": "\f137",
+ "arrow-right": "\f138",
+ "arrow-up-circle-fill": "\f139",
+ "arrow-up-circle": "\f13a",
+ "arrow-up-left-circle-fill": "\f13b",
+ "arrow-up-left-circle": "\f13c",
+ "arrow-up-left-square-fill": "\f13d",
+ "arrow-up-left-square": "\f13e",
+ "arrow-up-left": "\f13f",
+ "arrow-up-right-circle-fill": "\f140",
+ "arrow-up-right-circle": "\f141",
+ "arrow-up-right-square-fill": "\f142",
+ "arrow-up-right-square": "\f143",
+ "arrow-up-right": "\f144",
+ "arrow-up-short": "\f145",
+ "arrow-up-square-fill": "\f146",
+ "arrow-up-square": "\f147",
+ "arrow-up": "\f148",
+ "arrows-angle-contract": "\f149",
+ "arrows-angle-expand": "\f14a",
+ "arrows-collapse": "\f14b",
+ "arrows-expand": "\f14c",
+ "arrows-fullscreen": "\f14d",
+ "arrows-move": "\f14e",
+ "aspect-ratio-fill": "\f14f",
+ "aspect-ratio": "\f150",
+ "asterisk": "\f151",
+ "at": "\f152",
+ "award-fill": "\f153",
+ "award": "\f154",
+ "back": "\f155",
+ "backspace-fill": "\f156",
+ "backspace-reverse-fill": "\f157",
+ "backspace-reverse": "\f158",
+ "backspace": "\f159",
+ "badge-3d-fill": "\f15a",
+ "badge-3d": "\f15b",
+ "badge-4k-fill": "\f15c",
+ "badge-4k": "\f15d",
+ "badge-8k-fill": "\f15e",
+ "badge-8k": "\f15f",
+ "badge-ad-fill": "\f160",
+ "badge-ad": "\f161",
+ "badge-ar-fill": "\f162",
+ "badge-ar": "\f163",
+ "badge-cc-fill": "\f164",
+ "badge-cc": "\f165",
+ "badge-hd-fill": "\f166",
+ "badge-hd": "\f167",
+ "badge-tm-fill": "\f168",
+ "badge-tm": "\f169",
+ "badge-vo-fill": "\f16a",
+ "badge-vo": "\f16b",
+ "badge-vr-fill": "\f16c",
+ "badge-vr": "\f16d",
+ "badge-wc-fill": "\f16e",
+ "badge-wc": "\f16f",
+ "bag-check-fill": "\f170",
+ "bag-check": "\f171",
+ "bag-dash-fill": "\f172",
+ "bag-dash": "\f173",
+ "bag-fill": "\f174",
+ "bag-plus-fill": "\f175",
+ "bag-plus": "\f176",
+ "bag-x-fill": "\f177",
+ "bag-x": "\f178",
+ "bag": "\f179",
+ "bar-chart-fill": "\f17a",
+ "bar-chart-line-fill": "\f17b",
+ "bar-chart-line": "\f17c",
+ "bar-chart-steps": "\f17d",
+ "bar-chart": "\f17e",
+ "basket-fill": "\f17f",
+ "basket": "\f180",
+ "basket2-fill": "\f181",
+ "basket2": "\f182",
+ "basket3-fill": "\f183",
+ "basket3": "\f184",
+ "battery-charging": "\f185",
+ "battery-full": "\f186",
+ "battery-half": "\f187",
+ "battery": "\f188",
+ "bell-fill": "\f189",
+ "bell": "\f18a",
+ "bezier": "\f18b",
+ "bezier2": "\f18c",
+ "bicycle": "\f18d",
+ "binoculars-fill": "\f18e",
+ "binoculars": "\f18f",
+ "blockquote-left": "\f190",
+ "blockquote-right": "\f191",
+ "book-fill": "\f192",
+ "book-half": "\f193",
+ "book": "\f194",
+ "bookmark-check-fill": "\f195",
+ "bookmark-check": "\f196",
+ "bookmark-dash-fill": "\f197",
+ "bookmark-dash": "\f198",
+ "bookmark-fill": "\f199",
+ "bookmark-heart-fill": "\f19a",
+ "bookmark-heart": "\f19b",
+ "bookmark-plus-fill": "\f19c",
+ "bookmark-plus": "\f19d",
+ "bookmark-star-fill": "\f19e",
+ "bookmark-star": "\f19f",
+ "bookmark-x-fill": "\f1a0",
+ "bookmark-x": "\f1a1",
+ "bookmark": "\f1a2",
+ "bookmarks-fill": "\f1a3",
+ "bookmarks": "\f1a4",
+ "bookshelf": "\f1a5",
+ "bootstrap-fill": "\f1a6",
+ "bootstrap-reboot": "\f1a7",
+ "bootstrap": "\f1a8",
+ "border-all": "\f1a9",
+ "border-bottom": "\f1aa",
+ "border-center": "\f1ab",
+ "border-inner": "\f1ac",
+ "border-left": "\f1ad",
+ "border-middle": "\f1ae",
+ "border-outer": "\f1af",
+ "border-right": "\f1b0",
+ "border-style": "\f1b1",
+ "border-top": "\f1b2",
+ "border-width": "\f1b3",
+ "border": "\f1b4",
+ "bounding-box-circles": "\f1b5",
+ "bounding-box": "\f1b6",
+ "box-arrow-down-left": "\f1b7",
+ "box-arrow-down-right": "\f1b8",
+ "box-arrow-down": "\f1b9",
+ "box-arrow-in-down-left": "\f1ba",
+ "box-arrow-in-down-right": "\f1bb",
+ "box-arrow-in-down": "\f1bc",
+ "box-arrow-in-left": "\f1bd",
+ "box-arrow-in-right": "\f1be",
+ "box-arrow-in-up-left": "\f1bf",
+ "box-arrow-in-up-right": "\f1c0",
+ "box-arrow-in-up": "\f1c1",
+ "box-arrow-left": "\f1c2",
+ "box-arrow-right": "\f1c3",
+ "box-arrow-up-left": "\f1c4",
+ "box-arrow-up-right": "\f1c5",
+ "box-arrow-up": "\f1c6",
+ "box-seam": "\f1c7",
+ "box": "\f1c8",
+ "braces": "\f1c9",
+ "bricks": "\f1ca",
+ "briefcase-fill": "\f1cb",
+ "briefcase": "\f1cc",
+ "brightness-alt-high-fill": "\f1cd",
+ "brightness-alt-high": "\f1ce",
+ "brightness-alt-low-fill": "\f1cf",
+ "brightness-alt-low": "\f1d0",
+ "brightness-high-fill": "\f1d1",
+ "brightness-high": "\f1d2",
+ "brightness-low-fill": "\f1d3",
+ "brightness-low": "\f1d4",
+ "broadcast-pin": "\f1d5",
+ "broadcast": "\f1d6",
+ "brush-fill": "\f1d7",
+ "brush": "\f1d8",
+ "bucket-fill": "\f1d9",
+ "bucket": "\f1da",
+ "bug-fill": "\f1db",
+ "bug": "\f1dc",
+ "building": "\f1dd",
+ "bullseye": "\f1de",
+ "calculator-fill": "\f1df",
+ "calculator": "\f1e0",
+ "calendar-check-fill": "\f1e1",
+ "calendar-check": "\f1e2",
+ "calendar-date-fill": "\f1e3",
+ "calendar-date": "\f1e4",
+ "calendar-day-fill": "\f1e5",
+ "calendar-day": "\f1e6",
+ "calendar-event-fill": "\f1e7",
+ "calendar-event": "\f1e8",
+ "calendar-fill": "\f1e9",
+ "calendar-minus-fill": "\f1ea",
+ "calendar-minus": "\f1eb",
+ "calendar-month-fill": "\f1ec",
+ "calendar-month": "\f1ed",
+ "calendar-plus-fill": "\f1ee",
+ "calendar-plus": "\f1ef",
+ "calendar-range-fill": "\f1f0",
+ "calendar-range": "\f1f1",
+ "calendar-week-fill": "\f1f2",
+ "calendar-week": "\f1f3",
+ "calendar-x-fill": "\f1f4",
+ "calendar-x": "\f1f5",
+ "calendar": "\f1f6",
+ "calendar2-check-fill": "\f1f7",
+ "calendar2-check": "\f1f8",
+ "calendar2-date-fill": "\f1f9",
+ "calendar2-date": "\f1fa",
+ "calendar2-day-fill": "\f1fb",
+ "calendar2-day": "\f1fc",
+ "calendar2-event-fill": "\f1fd",
+ "calendar2-event": "\f1fe",
+ "calendar2-fill": "\f1ff",
+ "calendar2-minus-fill": "\f200",
+ "calendar2-minus": "\f201",
+ "calendar2-month-fill": "\f202",
+ "calendar2-month": "\f203",
+ "calendar2-plus-fill": "\f204",
+ "calendar2-plus": "\f205",
+ "calendar2-range-fill": "\f206",
+ "calendar2-range": "\f207",
+ "calendar2-week-fill": "\f208",
+ "calendar2-week": "\f209",
+ "calendar2-x-fill": "\f20a",
+ "calendar2-x": "\f20b",
+ "calendar2": "\f20c",
+ "calendar3-event-fill": "\f20d",
+ "calendar3-event": "\f20e",
+ "calendar3-fill": "\f20f",
+ "calendar3-range-fill": "\f210",
+ "calendar3-range": "\f211",
+ "calendar3-week-fill": "\f212",
+ "calendar3-week": "\f213",
+ "calendar3": "\f214",
+ "calendar4-event": "\f215",
+ "calendar4-range": "\f216",
+ "calendar4-week": "\f217",
+ "calendar4": "\f218",
+ "camera-fill": "\f219",
+ "camera-reels-fill": "\f21a",
+ "camera-reels": "\f21b",
+ "camera-video-fill": "\f21c",
+ "camera-video-off-fill": "\f21d",
+ "camera-video-off": "\f21e",
+ "camera-video": "\f21f",
+ "camera": "\f220",
+ "camera2": "\f221",
+ "capslock-fill": "\f222",
+ "capslock": "\f223",
+ "card-checklist": "\f224",
+ "card-heading": "\f225",
+ "card-image": "\f226",
+ "card-list": "\f227",
+ "card-text": "\f228",
+ "caret-down-fill": "\f229",
+ "caret-down-square-fill": "\f22a",
+ "caret-down-square": "\f22b",
+ "caret-down": "\f22c",
+ "caret-left-fill": "\f22d",
+ "caret-left-square-fill": "\f22e",
+ "caret-left-square": "\f22f",
+ "caret-left": "\f230",
+ "caret-right-fill": "\f231",
+ "caret-right-square-fill": "\f232",
+ "caret-right-square": "\f233",
+ "caret-right": "\f234",
+ "caret-up-fill": "\f235",
+ "caret-up-square-fill": "\f236",
+ "caret-up-square": "\f237",
+ "caret-up": "\f238",
+ "cart-check-fill": "\f239",
+ "cart-check": "\f23a",
+ "cart-dash-fill": "\f23b",
+ "cart-dash": "\f23c",
+ "cart-fill": "\f23d",
+ "cart-plus-fill": "\f23e",
+ "cart-plus": "\f23f",
+ "cart-x-fill": "\f240",
+ "cart-x": "\f241",
+ "cart": "\f242",
+ "cart2": "\f243",
+ "cart3": "\f244",
+ "cart4": "\f245",
+ "cash-stack": "\f246",
+ "cash": "\f247",
+ "cast": "\f248",
+ "chat-dots-fill": "\f249",
+ "chat-dots": "\f24a",
+ "chat-fill": "\f24b",
+ "chat-left-dots-fill": "\f24c",
+ "chat-left-dots": "\f24d",
+ "chat-left-fill": "\f24e",
+ "chat-left-quote-fill": "\f24f",
+ "chat-left-quote": "\f250",
+ "chat-left-text-fill": "\f251",
+ "chat-left-text": "\f252",
+ "chat-left": "\f253",
+ "chat-quote-fill": "\f254",
+ "chat-quote": "\f255",
+ "chat-right-dots-fill": "\f256",
+ "chat-right-dots": "\f257",
+ "chat-right-fill": "\f258",
+ "chat-right-quote-fill": "\f259",
+ "chat-right-quote": "\f25a",
+ "chat-right-text-fill": "\f25b",
+ "chat-right-text": "\f25c",
+ "chat-right": "\f25d",
+ "chat-square-dots-fill": "\f25e",
+ "chat-square-dots": "\f25f",
+ "chat-square-fill": "\f260",
+ "chat-square-quote-fill": "\f261",
+ "chat-square-quote": "\f262",
+ "chat-square-text-fill": "\f263",
+ "chat-square-text": "\f264",
+ "chat-square": "\f265",
+ "chat-text-fill": "\f266",
+ "chat-text": "\f267",
+ "chat": "\f268",
+ "check-all": "\f269",
+ "check-circle-fill": "\f26a",
+ "check-circle": "\f26b",
+ "check-square-fill": "\f26c",
+ "check-square": "\f26d",
+ "check": "\f26e",
+ "check2-all": "\f26f",
+ "check2-circle": "\f270",
+ "check2-square": "\f271",
+ "check2": "\f272",
+ "chevron-bar-contract": "\f273",
+ "chevron-bar-down": "\f274",
+ "chevron-bar-expand": "\f275",
+ "chevron-bar-left": "\f276",
+ "chevron-bar-right": "\f277",
+ "chevron-bar-up": "\f278",
+ "chevron-compact-down": "\f279",
+ "chevron-compact-left": "\f27a",
+ "chevron-compact-right": "\f27b",
+ "chevron-compact-up": "\f27c",
+ "chevron-contract": "\f27d",
+ "chevron-double-down": "\f27e",
+ "chevron-double-left": "\f27f",
+ "chevron-double-right": "\f280",
+ "chevron-double-up": "\f281",
+ "chevron-down": "\f282",
+ "chevron-expand": "\f283",
+ "chevron-left": "\f284",
+ "chevron-right": "\f285",
+ "chevron-up": "\f286",
+ "circle-fill": "\f287",
+ "circle-half": "\f288",
+ "circle-square": "\f289",
+ "circle": "\f28a",
+ "clipboard-check": "\f28b",
+ "clipboard-data": "\f28c",
+ "clipboard-minus": "\f28d",
+ "clipboard-plus": "\f28e",
+ "clipboard-x": "\f28f",
+ "clipboard": "\f290",
+ "clock-fill": "\f291",
+ "clock-history": "\f292",
+ "clock": "\f293",
+ "cloud-arrow-down-fill": "\f294",
+ "cloud-arrow-down": "\f295",
+ "cloud-arrow-up-fill": "\f296",
+ "cloud-arrow-up": "\f297",
+ "cloud-check-fill": "\f298",
+ "cloud-check": "\f299",
+ "cloud-download-fill": "\f29a",
+ "cloud-download": "\f29b",
+ "cloud-drizzle-fill": "\f29c",
+ "cloud-drizzle": "\f29d",
+ "cloud-fill": "\f29e",
+ "cloud-fog-fill": "\f29f",
+ "cloud-fog": "\f2a0",
+ "cloud-fog2-fill": "\f2a1",
+ "cloud-fog2": "\f2a2",
+ "cloud-hail-fill": "\f2a3",
+ "cloud-hail": "\f2a4",
+ "cloud-haze-fill": "\f2a6",
+ "cloud-haze": "\f2a7",
+ "cloud-haze2-fill": "\f2a8",
+ "cloud-lightning-fill": "\f2a9",
+ "cloud-lightning-rain-fill": "\f2aa",
+ "cloud-lightning-rain": "\f2ab",
+ "cloud-lightning": "\f2ac",
+ "cloud-minus-fill": "\f2ad",
+ "cloud-minus": "\f2ae",
+ "cloud-moon-fill": "\f2af",
+ "cloud-moon": "\f2b0",
+ "cloud-plus-fill": "\f2b1",
+ "cloud-plus": "\f2b2",
+ "cloud-rain-fill": "\f2b3",
+ "cloud-rain-heavy-fill": "\f2b4",
+ "cloud-rain-heavy": "\f2b5",
+ "cloud-rain": "\f2b6",
+ "cloud-slash-fill": "\f2b7",
+ "cloud-slash": "\f2b8",
+ "cloud-sleet-fill": "\f2b9",
+ "cloud-sleet": "\f2ba",
+ "cloud-snow-fill": "\f2bb",
+ "cloud-snow": "\f2bc",
+ "cloud-sun-fill": "\f2bd",
+ "cloud-sun": "\f2be",
+ "cloud-upload-fill": "\f2bf",
+ "cloud-upload": "\f2c0",
+ "cloud": "\f2c1",
+ "clouds-fill": "\f2c2",
+ "clouds": "\f2c3",
+ "cloudy-fill": "\f2c4",
+ "cloudy": "\f2c5",
+ "code-slash": "\f2c6",
+ "code-square": "\f2c7",
+ "code": "\f2c8",
+ "collection-fill": "\f2c9",
+ "collection-play-fill": "\f2ca",
+ "collection-play": "\f2cb",
+ "collection": "\f2cc",
+ "columns-gap": "\f2cd",
+ "columns": "\f2ce",
+ "command": "\f2cf",
+ "compass-fill": "\f2d0",
+ "compass": "\f2d1",
+ "cone-striped": "\f2d2",
+ "cone": "\f2d3",
+ "controller": "\f2d4",
+ "cpu-fill": "\f2d5",
+ "cpu": "\f2d6",
+ "credit-card-2-back-fill": "\f2d7",
+ "credit-card-2-back": "\f2d8",
+ "credit-card-2-front-fill": "\f2d9",
+ "credit-card-2-front": "\f2da",
+ "credit-card-fill": "\f2db",
+ "credit-card": "\f2dc",
+ "crop": "\f2dd",
+ "cup-fill": "\f2de",
+ "cup-straw": "\f2df",
+ "cup": "\f2e0",
+ "cursor-fill": "\f2e1",
+ "cursor-text": "\f2e2",
+ "cursor": "\f2e3",
+ "dash-circle-dotted": "\f2e4",
+ "dash-circle-fill": "\f2e5",
+ "dash-circle": "\f2e6",
+ "dash-square-dotted": "\f2e7",
+ "dash-square-fill": "\f2e8",
+ "dash-square": "\f2e9",
+ "dash": "\f2ea",
+ "diagram-2-fill": "\f2eb",
+ "diagram-2": "\f2ec",
+ "diagram-3-fill": "\f2ed",
+ "diagram-3": "\f2ee",
+ "diamond-fill": "\f2ef",
+ "diamond-half": "\f2f0",
+ "diamond": "\f2f1",
+ "dice-1-fill": "\f2f2",
+ "dice-1": "\f2f3",
+ "dice-2-fill": "\f2f4",
+ "dice-2": "\f2f5",
+ "dice-3-fill": "\f2f6",
+ "dice-3": "\f2f7",
+ "dice-4-fill": "\f2f8",
+ "dice-4": "\f2f9",
+ "dice-5-fill": "\f2fa",
+ "dice-5": "\f2fb",
+ "dice-6-fill": "\f2fc",
+ "dice-6": "\f2fd",
+ "disc-fill": "\f2fe",
+ "disc": "\f2ff",
+ "discord": "\f300",
+ "display-fill": "\f301",
+ "display": "\f302",
+ "distribute-horizontal": "\f303",
+ "distribute-vertical": "\f304",
+ "door-closed-fill": "\f305",
+ "door-closed": "\f306",
+ "door-open-fill": "\f307",
+ "door-open": "\f308",
+ "dot": "\f309",
+ "download": "\f30a",
+ "droplet-fill": "\f30b",
+ "droplet-half": "\f30c",
+ "droplet": "\f30d",
+ "earbuds": "\f30e",
+ "easel-fill": "\f30f",
+ "easel": "\f310",
+ "egg-fill": "\f311",
+ "egg-fried": "\f312",
+ "egg": "\f313",
+ "eject-fill": "\f314",
+ "eject": "\f315",
+ "emoji-angry-fill": "\f316",
+ "emoji-angry": "\f317",
+ "emoji-dizzy-fill": "\f318",
+ "emoji-dizzy": "\f319",
+ "emoji-expressionless-fill": "\f31a",
+ "emoji-expressionless": "\f31b",
+ "emoji-frown-fill": "\f31c",
+ "emoji-frown": "\f31d",
+ "emoji-heart-eyes-fill": "\f31e",
+ "emoji-heart-eyes": "\f31f",
+ "emoji-laughing-fill": "\f320",
+ "emoji-laughing": "\f321",
+ "emoji-neutral-fill": "\f322",
+ "emoji-neutral": "\f323",
+ "emoji-smile-fill": "\f324",
+ "emoji-smile-upside-down-fill": "\f325",
+ "emoji-smile-upside-down": "\f326",
+ "emoji-smile": "\f327",
+ "emoji-sunglasses-fill": "\f328",
+ "emoji-sunglasses": "\f329",
+ "emoji-wink-fill": "\f32a",
+ "emoji-wink": "\f32b",
+ "envelope-fill": "\f32c",
+ "envelope-open-fill": "\f32d",
+ "envelope-open": "\f32e",
+ "envelope": "\f32f",
+ "eraser-fill": "\f330",
+ "eraser": "\f331",
+ "exclamation-circle-fill": "\f332",
+ "exclamation-circle": "\f333",
+ "exclamation-diamond-fill": "\f334",
+ "exclamation-diamond": "\f335",
+ "exclamation-octagon-fill": "\f336",
+ "exclamation-octagon": "\f337",
+ "exclamation-square-fill": "\f338",
+ "exclamation-square": "\f339",
+ "exclamation-triangle-fill": "\f33a",
+ "exclamation-triangle": "\f33b",
+ "exclamation": "\f33c",
+ "exclude": "\f33d",
+ "eye-fill": "\f33e",
+ "eye-slash-fill": "\f33f",
+ "eye-slash": "\f340",
+ "eye": "\f341",
+ "eyedropper": "\f342",
+ "eyeglasses": "\f343",
+ "facebook": "\f344",
+ "file-arrow-down-fill": "\f345",
+ "file-arrow-down": "\f346",
+ "file-arrow-up-fill": "\f347",
+ "file-arrow-up": "\f348",
+ "file-bar-graph-fill": "\f349",
+ "file-bar-graph": "\f34a",
+ "file-binary-fill": "\f34b",
+ "file-binary": "\f34c",
+ "file-break-fill": "\f34d",
+ "file-break": "\f34e",
+ "file-check-fill": "\f34f",
+ "file-check": "\f350",
+ "file-code-fill": "\f351",
+ "file-code": "\f352",
+ "file-diff-fill": "\f353",
+ "file-diff": "\f354",
+ "file-earmark-arrow-down-fill": "\f355",
+ "file-earmark-arrow-down": "\f356",
+ "file-earmark-arrow-up-fill": "\f357",
+ "file-earmark-arrow-up": "\f358",
+ "file-earmark-bar-graph-fill": "\f359",
+ "file-earmark-bar-graph": "\f35a",
+ "file-earmark-binary-fill": "\f35b",
+ "file-earmark-binary": "\f35c",
+ "file-earmark-break-fill": "\f35d",
+ "file-earmark-break": "\f35e",
+ "file-earmark-check-fill": "\f35f",
+ "file-earmark-check": "\f360",
+ "file-earmark-code-fill": "\f361",
+ "file-earmark-code": "\f362",
+ "file-earmark-diff-fill": "\f363",
+ "file-earmark-diff": "\f364",
+ "file-earmark-easel-fill": "\f365",
+ "file-earmark-easel": "\f366",
+ "file-earmark-excel-fill": "\f367",
+ "file-earmark-excel": "\f368",
+ "file-earmark-fill": "\f369",
+ "file-earmark-font-fill": "\f36a",
+ "file-earmark-font": "\f36b",
+ "file-earmark-image-fill": "\f36c",
+ "file-earmark-image": "\f36d",
+ "file-earmark-lock-fill": "\f36e",
+ "file-earmark-lock": "\f36f",
+ "file-earmark-lock2-fill": "\f370",
+ "file-earmark-lock2": "\f371",
+ "file-earmark-medical-fill": "\f372",
+ "file-earmark-medical": "\f373",
+ "file-earmark-minus-fill": "\f374",
+ "file-earmark-minus": "\f375",
+ "file-earmark-music-fill": "\f376",
+ "file-earmark-music": "\f377",
+ "file-earmark-person-fill": "\f378",
+ "file-earmark-person": "\f379",
+ "file-earmark-play-fill": "\f37a",
+ "file-earmark-play": "\f37b",
+ "file-earmark-plus-fill": "\f37c",
+ "file-earmark-plus": "\f37d",
+ "file-earmark-post-fill": "\f37e",
+ "file-earmark-post": "\f37f",
+ "file-earmark-ppt-fill": "\f380",
+ "file-earmark-ppt": "\f381",
+ "file-earmark-richtext-fill": "\f382",
+ "file-earmark-richtext": "\f383",
+ "file-earmark-ruled-fill": "\f384",
+ "file-earmark-ruled": "\f385",
+ "file-earmark-slides-fill": "\f386",
+ "file-earmark-slides": "\f387",
+ "file-earmark-spreadsheet-fill": "\f388",
+ "file-earmark-spreadsheet": "\f389",
+ "file-earmark-text-fill": "\f38a",
+ "file-earmark-text": "\f38b",
+ "file-earmark-word-fill": "\f38c",
+ "file-earmark-word": "\f38d",
+ "file-earmark-x-fill": "\f38e",
+ "file-earmark-x": "\f38f",
+ "file-earmark-zip-fill": "\f390",
+ "file-earmark-zip": "\f391",
+ "file-earmark": "\f392",
+ "file-easel-fill": "\f393",
+ "file-easel": "\f394",
+ "file-excel-fill": "\f395",
+ "file-excel": "\f396",
+ "file-fill": "\f397",
+ "file-font-fill": "\f398",
+ "file-font": "\f399",
+ "file-image-fill": "\f39a",
+ "file-image": "\f39b",
+ "file-lock-fill": "\f39c",
+ "file-lock": "\f39d",
+ "file-lock2-fill": "\f39e",
+ "file-lock2": "\f39f",
+ "file-medical-fill": "\f3a0",
+ "file-medical": "\f3a1",
+ "file-minus-fill": "\f3a2",
+ "file-minus": "\f3a3",
+ "file-music-fill": "\f3a4",
+ "file-music": "\f3a5",
+ "file-person-fill": "\f3a6",
+ "file-person": "\f3a7",
+ "file-play-fill": "\f3a8",
+ "file-play": "\f3a9",
+ "file-plus-fill": "\f3aa",
+ "file-plus": "\f3ab",
+ "file-post-fill": "\f3ac",
+ "file-post": "\f3ad",
+ "file-ppt-fill": "\f3ae",
+ "file-ppt": "\f3af",
+ "file-richtext-fill": "\f3b0",
+ "file-richtext": "\f3b1",
+ "file-ruled-fill": "\f3b2",
+ "file-ruled": "\f3b3",
+ "file-slides-fill": "\f3b4",
+ "file-slides": "\f3b5",
+ "file-spreadsheet-fill": "\f3b6",
+ "file-spreadsheet": "\f3b7",
+ "file-text-fill": "\f3b8",
+ "file-text": "\f3b9",
+ "file-word-fill": "\f3ba",
+ "file-word": "\f3bb",
+ "file-x-fill": "\f3bc",
+ "file-x": "\f3bd",
+ "file-zip-fill": "\f3be",
+ "file-zip": "\f3bf",
+ "file": "\f3c0",
+ "files-alt": "\f3c1",
+ "files": "\f3c2",
+ "film": "\f3c3",
+ "filter-circle-fill": "\f3c4",
+ "filter-circle": "\f3c5",
+ "filter-left": "\f3c6",
+ "filter-right": "\f3c7",
+ "filter-square-fill": "\f3c8",
+ "filter-square": "\f3c9",
+ "filter": "\f3ca",
+ "flag-fill": "\f3cb",
+ "flag": "\f3cc",
+ "flower1": "\f3cd",
+ "flower2": "\f3ce",
+ "flower3": "\f3cf",
+ "folder-check": "\f3d0",
+ "folder-fill": "\f3d1",
+ "folder-minus": "\f3d2",
+ "folder-plus": "\f3d3",
+ "folder-symlink-fill": "\f3d4",
+ "folder-symlink": "\f3d5",
+ "folder-x": "\f3d6",
+ "folder": "\f3d7",
+ "folder2-open": "\f3d8",
+ "folder2": "\f3d9",
+ "fonts": "\f3da",
+ "forward-fill": "\f3db",
+ "forward": "\f3dc",
+ "front": "\f3dd",
+ "fullscreen-exit": "\f3de",
+ "fullscreen": "\f3df",
+ "funnel-fill": "\f3e0",
+ "funnel": "\f3e1",
+ "gear-fill": "\f3e2",
+ "gear-wide-connected": "\f3e3",
+ "gear-wide": "\f3e4",
+ "gear": "\f3e5",
+ "gem": "\f3e6",
+ "geo-alt-fill": "\f3e7",
+ "geo-alt": "\f3e8",
+ "geo-fill": "\f3e9",
+ "geo": "\f3ea",
+ "gift-fill": "\f3eb",
+ "gift": "\f3ec",
+ "github": "\f3ed",
+ "globe": "\f3ee",
+ "globe2": "\f3ef",
+ "google": "\f3f0",
+ "graph-down": "\f3f1",
+ "graph-up": "\f3f2",
+ "grid-1x2-fill": "\f3f3",
+ "grid-1x2": "\f3f4",
+ "grid-3x2-gap-fill": "\f3f5",
+ "grid-3x2-gap": "\f3f6",
+ "grid-3x2": "\f3f7",
+ "grid-3x3-gap-fill": "\f3f8",
+ "grid-3x3-gap": "\f3f9",
+ "grid-3x3": "\f3fa",
+ "grid-fill": "\f3fb",
+ "grid": "\f3fc",
+ "grip-horizontal": "\f3fd",
+ "grip-vertical": "\f3fe",
+ "hammer": "\f3ff",
+ "hand-index-fill": "\f400",
+ "hand-index-thumb-fill": "\f401",
+ "hand-index-thumb": "\f402",
+ "hand-index": "\f403",
+ "hand-thumbs-down-fill": "\f404",
+ "hand-thumbs-down": "\f405",
+ "hand-thumbs-up-fill": "\f406",
+ "hand-thumbs-up": "\f407",
+ "handbag-fill": "\f408",
+ "handbag": "\f409",
+ "hash": "\f40a",
+ "hdd-fill": "\f40b",
+ "hdd-network-fill": "\f40c",
+ "hdd-network": "\f40d",
+ "hdd-rack-fill": "\f40e",
+ "hdd-rack": "\f40f",
+ "hdd-stack-fill": "\f410",
+ "hdd-stack": "\f411",
+ "hdd": "\f412",
+ "headphones": "\f413",
+ "headset": "\f414",
+ "heart-fill": "\f415",
+ "heart-half": "\f416",
+ "heart": "\f417",
+ "heptagon-fill": "\f418",
+ "heptagon-half": "\f419",
+ "heptagon": "\f41a",
+ "hexagon-fill": "\f41b",
+ "hexagon-half": "\f41c",
+ "hexagon": "\f41d",
+ "hourglass-bottom": "\f41e",
+ "hourglass-split": "\f41f",
+ "hourglass-top": "\f420",
+ "hourglass": "\f421",
+ "house-door-fill": "\f422",
+ "house-door": "\f423",
+ "house-fill": "\f424",
+ "house": "\f425",
+ "hr": "\f426",
+ "hurricane": "\f427",
+ "image-alt": "\f428",
+ "image-fill": "\f429",
+ "image": "\f42a",
+ "images": "\f42b",
+ "inbox-fill": "\f42c",
+ "inbox": "\f42d",
+ "inboxes-fill": "\f42e",
+ "inboxes": "\f42f",
+ "info-circle-fill": "\f430",
+ "info-circle": "\f431",
+ "info-square-fill": "\f432",
+ "info-square": "\f433",
+ "info": "\f434",
+ "input-cursor-text": "\f435",
+ "input-cursor": "\f436",
+ "instagram": "\f437",
+ "intersect": "\f438",
+ "journal-album": "\f439",
+ "journal-arrow-down": "\f43a",
+ "journal-arrow-up": "\f43b",
+ "journal-bookmark-fill": "\f43c",
+ "journal-bookmark": "\f43d",
+ "journal-check": "\f43e",
+ "journal-code": "\f43f",
+ "journal-medical": "\f440",
+ "journal-minus": "\f441",
+ "journal-plus": "\f442",
+ "journal-richtext": "\f443",
+ "journal-text": "\f444",
+ "journal-x": "\f445",
+ "journal": "\f446",
+ "journals": "\f447",
+ "joystick": "\f448",
+ "justify-left": "\f449",
+ "justify-right": "\f44a",
+ "justify": "\f44b",
+ "kanban-fill": "\f44c",
+ "kanban": "\f44d",
+ "key-fill": "\f44e",
+ "key": "\f44f",
+ "keyboard-fill": "\f450",
+ "keyboard": "\f451",
+ "ladder": "\f452",
+ "lamp-fill": "\f453",
+ "lamp": "\f454",
+ "laptop-fill": "\f455",
+ "laptop": "\f456",
+ "layer-backward": "\f457",
+ "layer-forward": "\f458",
+ "layers-fill": "\f459",
+ "layers-half": "\f45a",
+ "layers": "\f45b",
+ "layout-sidebar-inset-reverse": "\f45c",
+ "layout-sidebar-inset": "\f45d",
+ "layout-sidebar-reverse": "\f45e",
+ "layout-sidebar": "\f45f",
+ "layout-split": "\f460",
+ "layout-text-sidebar-reverse": "\f461",
+ "layout-text-sidebar": "\f462",
+ "layout-text-window-reverse": "\f463",
+ "layout-text-window": "\f464",
+ "layout-three-columns": "\f465",
+ "layout-wtf": "\f466",
+ "life-preserver": "\f467",
+ "lightbulb-fill": "\f468",
+ "lightbulb-off-fill": "\f469",
+ "lightbulb-off": "\f46a",
+ "lightbulb": "\f46b",
+ "lightning-charge-fill": "\f46c",
+ "lightning-charge": "\f46d",
+ "lightning-fill": "\f46e",
+ "lightning": "\f46f",
+ "link-45deg": "\f470",
+ "link": "\f471",
+ "linkedin": "\f472",
+ "list-check": "\f473",
+ "list-nested": "\f474",
+ "list-ol": "\f475",
+ "list-stars": "\f476",
+ "list-task": "\f477",
+ "list-ul": "\f478",
+ "list": "\f479",
+ "lock-fill": "\f47a",
+ "lock": "\f47b",
+ "mailbox": "\f47c",
+ "mailbox2": "\f47d",
+ "map-fill": "\f47e",
+ "map": "\f47f",
+ "markdown-fill": "\f480",
+ "markdown": "\f481",
+ "mask": "\f482",
+ "megaphone-fill": "\f483",
+ "megaphone": "\f484",
+ "menu-app-fill": "\f485",
+ "menu-app": "\f486",
+ "menu-button-fill": "\f487",
+ "menu-button-wide-fill": "\f488",
+ "menu-button-wide": "\f489",
+ "menu-button": "\f48a",
+ "menu-down": "\f48b",
+ "menu-up": "\f48c",
+ "mic-fill": "\f48d",
+ "mic-mute-fill": "\f48e",
+ "mic-mute": "\f48f",
+ "mic": "\f490",
+ "minecart-loaded": "\f491",
+ "minecart": "\f492",
+ "moisture": "\f493",
+ "moon-fill": "\f494",
+ "moon-stars-fill": "\f495",
+ "moon-stars": "\f496",
+ "moon": "\f497",
+ "mouse-fill": "\f498",
+ "mouse": "\f499",
+ "mouse2-fill": "\f49a",
+ "mouse2": "\f49b",
+ "mouse3-fill": "\f49c",
+ "mouse3": "\f49d",
+ "music-note-beamed": "\f49e",
+ "music-note-list": "\f49f",
+ "music-note": "\f4a0",
+ "music-player-fill": "\f4a1",
+ "music-player": "\f4a2",
+ "newspaper": "\f4a3",
+ "node-minus-fill": "\f4a4",
+ "node-minus": "\f4a5",
+ "node-plus-fill": "\f4a6",
+ "node-plus": "\f4a7",
+ "nut-fill": "\f4a8",
+ "nut": "\f4a9",
+ "octagon-fill": "\f4aa",
+ "octagon-half": "\f4ab",
+ "octagon": "\f4ac",
+ "option": "\f4ad",
+ "outlet": "\f4ae",
+ "paint-bucket": "\f4af",
+ "palette-fill": "\f4b0",
+ "palette": "\f4b1",
+ "palette2": "\f4b2",
+ "paperclip": "\f4b3",
+ "paragraph": "\f4b4",
+ "patch-check-fill": "\f4b5",
+ "patch-check": "\f4b6",
+ "patch-exclamation-fill": "\f4b7",
+ "patch-exclamation": "\f4b8",
+ "patch-minus-fill": "\f4b9",
+ "patch-minus": "\f4ba",
+ "patch-plus-fill": "\f4bb",
+ "patch-plus": "\f4bc",
+ "patch-question-fill": "\f4bd",
+ "patch-question": "\f4be",
+ "pause-btn-fill": "\f4bf",
+ "pause-btn": "\f4c0",
+ "pause-circle-fill": "\f4c1",
+ "pause-circle": "\f4c2",
+ "pause-fill": "\f4c3",
+ "pause": "\f4c4",
+ "peace-fill": "\f4c5",
+ "peace": "\f4c6",
+ "pen-fill": "\f4c7",
+ "pen": "\f4c8",
+ "pencil-fill": "\f4c9",
+ "pencil-square": "\f4ca",
+ "pencil": "\f4cb",
+ "pentagon-fill": "\f4cc",
+ "pentagon-half": "\f4cd",
+ "pentagon": "\f4ce",
+ "people-fill": "\f4cf",
+ "people": "\f4d0",
+ "percent": "\f4d1",
+ "person-badge-fill": "\f4d2",
+ "person-badge": "\f4d3",
+ "person-bounding-box": "\f4d4",
+ "person-check-fill": "\f4d5",
+ "person-check": "\f4d6",
+ "person-circle": "\f4d7",
+ "person-dash-fill": "\f4d8",
+ "person-dash": "\f4d9",
+ "person-fill": "\f4da",
+ "person-lines-fill": "\f4db",
+ "person-plus-fill": "\f4dc",
+ "person-plus": "\f4dd",
+ "person-square": "\f4de",
+ "person-x-fill": "\f4df",
+ "person-x": "\f4e0",
+ "person": "\f4e1",
+ "phone-fill": "\f4e2",
+ "phone-landscape-fill": "\f4e3",
+ "phone-landscape": "\f4e4",
+ "phone-vibrate-fill": "\f4e5",
+ "phone-vibrate": "\f4e6",
+ "phone": "\f4e7",
+ "pie-chart-fill": "\f4e8",
+ "pie-chart": "\f4e9",
+ "pin-angle-fill": "\f4ea",
+ "pin-angle": "\f4eb",
+ "pin-fill": "\f4ec",
+ "pin": "\f4ed",
+ "pip-fill": "\f4ee",
+ "pip": "\f4ef",
+ "play-btn-fill": "\f4f0",
+ "play-btn": "\f4f1",
+ "play-circle-fill": "\f4f2",
+ "play-circle": "\f4f3",
+ "play-fill": "\f4f4",
+ "play": "\f4f5",
+ "plug-fill": "\f4f6",
+ "plug": "\f4f7",
+ "plus-circle-dotted": "\f4f8",
+ "plus-circle-fill": "\f4f9",
+ "plus-circle": "\f4fa",
+ "plus-square-dotted": "\f4fb",
+ "plus-square-fill": "\f4fc",
+ "plus-square": "\f4fd",
+ "plus": "\f4fe",
+ "power": "\f4ff",
+ "printer-fill": "\f500",
+ "printer": "\f501",
+ "puzzle-fill": "\f502",
+ "puzzle": "\f503",
+ "question-circle-fill": "\f504",
+ "question-circle": "\f505",
+ "question-diamond-fill": "\f506",
+ "question-diamond": "\f507",
+ "question-octagon-fill": "\f508",
+ "question-octagon": "\f509",
+ "question-square-fill": "\f50a",
+ "question-square": "\f50b",
+ "question": "\f50c",
+ "rainbow": "\f50d",
+ "receipt-cutoff": "\f50e",
+ "receipt": "\f50f",
+ "reception-0": "\f510",
+ "reception-1": "\f511",
+ "reception-2": "\f512",
+ "reception-3": "\f513",
+ "reception-4": "\f514",
+ "record-btn-fill": "\f515",
+ "record-btn": "\f516",
+ "record-circle-fill": "\f517",
+ "record-circle": "\f518",
+ "record-fill": "\f519",
+ "record": "\f51a",
+ "record2-fill": "\f51b",
+ "record2": "\f51c",
+ "reply-all-fill": "\f51d",
+ "reply-all": "\f51e",
+ "reply-fill": "\f51f",
+ "reply": "\f520",
+ "rss-fill": "\f521",
+ "rss": "\f522",
+ "rulers": "\f523",
+ "save-fill": "\f524",
+ "save": "\f525",
+ "save2-fill": "\f526",
+ "save2": "\f527",
+ "scissors": "\f528",
+ "screwdriver": "\f529",
+ "search": "\f52a",
+ "segmented-nav": "\f52b",
+ "server": "\f52c",
+ "share-fill": "\f52d",
+ "share": "\f52e",
+ "shield-check": "\f52f",
+ "shield-exclamation": "\f530",
+ "shield-fill-check": "\f531",
+ "shield-fill-exclamation": "\f532",
+ "shield-fill-minus": "\f533",
+ "shield-fill-plus": "\f534",
+ "shield-fill-x": "\f535",
+ "shield-fill": "\f536",
+ "shield-lock-fill": "\f537",
+ "shield-lock": "\f538",
+ "shield-minus": "\f539",
+ "shield-plus": "\f53a",
+ "shield-shaded": "\f53b",
+ "shield-slash-fill": "\f53c",
+ "shield-slash": "\f53d",
+ "shield-x": "\f53e",
+ "shield": "\f53f",
+ "shift-fill": "\f540",
+ "shift": "\f541",
+ "shop-window": "\f542",
+ "shop": "\f543",
+ "shuffle": "\f544",
+ "signpost-2-fill": "\f545",
+ "signpost-2": "\f546",
+ "signpost-fill": "\f547",
+ "signpost-split-fill": "\f548",
+ "signpost-split": "\f549",
+ "signpost": "\f54a",
+ "sim-fill": "\f54b",
+ "sim": "\f54c",
+ "skip-backward-btn-fill": "\f54d",
+ "skip-backward-btn": "\f54e",
+ "skip-backward-circle-fill": "\f54f",
+ "skip-backward-circle": "\f550",
+ "skip-backward-fill": "\f551",
+ "skip-backward": "\f552",
+ "skip-end-btn-fill": "\f553",
+ "skip-end-btn": "\f554",
+ "skip-end-circle-fill": "\f555",
+ "skip-end-circle": "\f556",
+ "skip-end-fill": "\f557",
+ "skip-end": "\f558",
+ "skip-forward-btn-fill": "\f559",
+ "skip-forward-btn": "\f55a",
+ "skip-forward-circle-fill": "\f55b",
+ "skip-forward-circle": "\f55c",
+ "skip-forward-fill": "\f55d",
+ "skip-forward": "\f55e",
+ "skip-start-btn-fill": "\f55f",
+ "skip-start-btn": "\f560",
+ "skip-start-circle-fill": "\f561",
+ "skip-start-circle": "\f562",
+ "skip-start-fill": "\f563",
+ "skip-start": "\f564",
+ "slack": "\f565",
+ "slash-circle-fill": "\f566",
+ "slash-circle": "\f567",
+ "slash-square-fill": "\f568",
+ "slash-square": "\f569",
+ "slash": "\f56a",
+ "sliders": "\f56b",
+ "smartwatch": "\f56c",
+ "snow": "\f56d",
+ "snow2": "\f56e",
+ "snow3": "\f56f",
+ "sort-alpha-down-alt": "\f570",
+ "sort-alpha-down": "\f571",
+ "sort-alpha-up-alt": "\f572",
+ "sort-alpha-up": "\f573",
+ "sort-down-alt": "\f574",
+ "sort-down": "\f575",
+ "sort-numeric-down-alt": "\f576",
+ "sort-numeric-down": "\f577",
+ "sort-numeric-up-alt": "\f578",
+ "sort-numeric-up": "\f579",
+ "sort-up-alt": "\f57a",
+ "sort-up": "\f57b",
+ "soundwave": "\f57c",
+ "speaker-fill": "\f57d",
+ "speaker": "\f57e",
+ "speedometer": "\f57f",
+ "speedometer2": "\f580",
+ "spellcheck": "\f581",
+ "square-fill": "\f582",
+ "square-half": "\f583",
+ "square": "\f584",
+ "stack": "\f585",
+ "star-fill": "\f586",
+ "star-half": "\f587",
+ "star": "\f588",
+ "stars": "\f589",
+ "stickies-fill": "\f58a",
+ "stickies": "\f58b",
+ "sticky-fill": "\f58c",
+ "sticky": "\f58d",
+ "stop-btn-fill": "\f58e",
+ "stop-btn": "\f58f",
+ "stop-circle-fill": "\f590",
+ "stop-circle": "\f591",
+ "stop-fill": "\f592",
+ "stop": "\f593",
+ "stoplights-fill": "\f594",
+ "stoplights": "\f595",
+ "stopwatch-fill": "\f596",
+ "stopwatch": "\f597",
+ "subtract": "\f598",
+ "suit-club-fill": "\f599",
+ "suit-club": "\f59a",
+ "suit-diamond-fill": "\f59b",
+ "suit-diamond": "\f59c",
+ "suit-heart-fill": "\f59d",
+ "suit-heart": "\f59e",
+ "suit-spade-fill": "\f59f",
+ "suit-spade": "\f5a0",
+ "sun-fill": "\f5a1",
+ "sun": "\f5a2",
+ "sunglasses": "\f5a3",
+ "sunrise-fill": "\f5a4",
+ "sunrise": "\f5a5",
+ "sunset-fill": "\f5a6",
+ "sunset": "\f5a7",
+ "symmetry-horizontal": "\f5a8",
+ "symmetry-vertical": "\f5a9",
+ "table": "\f5aa",
+ "tablet-fill": "\f5ab",
+ "tablet-landscape-fill": "\f5ac",
+ "tablet-landscape": "\f5ad",
+ "tablet": "\f5ae",
+ "tag-fill": "\f5af",
+ "tag": "\f5b0",
+ "tags-fill": "\f5b1",
+ "tags": "\f5b2",
+ "telegram": "\f5b3",
+ "telephone-fill": "\f5b4",
+ "telephone-forward-fill": "\f5b5",
+ "telephone-forward": "\f5b6",
+ "telephone-inbound-fill": "\f5b7",
+ "telephone-inbound": "\f5b8",
+ "telephone-minus-fill": "\f5b9",
+ "telephone-minus": "\f5ba",
+ "telephone-outbound-fill": "\f5bb",
+ "telephone-outbound": "\f5bc",
+ "telephone-plus-fill": "\f5bd",
+ "telephone-plus": "\f5be",
+ "telephone-x-fill": "\f5bf",
+ "telephone-x": "\f5c0",
+ "telephone": "\f5c1",
+ "terminal-fill": "\f5c2",
+ "terminal": "\f5c3",
+ "text-center": "\f5c4",
+ "text-indent-left": "\f5c5",
+ "text-indent-right": "\f5c6",
+ "text-left": "\f5c7",
+ "text-paragraph": "\f5c8",
+ "text-right": "\f5c9",
+ "textarea-resize": "\f5ca",
+ "textarea-t": "\f5cb",
+ "textarea": "\f5cc",
+ "thermometer-half": "\f5cd",
+ "thermometer-high": "\f5ce",
+ "thermometer-low": "\f5cf",
+ "thermometer-snow": "\f5d0",
+ "thermometer-sun": "\f5d1",
+ "thermometer": "\f5d2",
+ "three-dots-vertical": "\f5d3",
+ "three-dots": "\f5d4",
+ "toggle-off": "\f5d5",
+ "toggle-on": "\f5d6",
+ "toggle2-off": "\f5d7",
+ "toggle2-on": "\f5d8",
+ "toggles": "\f5d9",
+ "toggles2": "\f5da",
+ "tools": "\f5db",
+ "tornado": "\f5dc",
+ "trash-fill": "\f5dd",
+ "trash": "\f5de",
+ "trash2-fill": "\f5df",
+ "trash2": "\f5e0",
+ "tree-fill": "\f5e1",
+ "tree": "\f5e2",
+ "triangle-fill": "\f5e3",
+ "triangle-half": "\f5e4",
+ "triangle": "\f5e5",
+ "trophy-fill": "\f5e6",
+ "trophy": "\f5e7",
+ "tropical-storm": "\f5e8",
+ "truck-flatbed": "\f5e9",
+ "truck": "\f5ea",
+ "tsunami": "\f5eb",
+ "tv-fill": "\f5ec",
+ "tv": "\f5ed",
+ "twitch": "\f5ee",
+ "twitter": "\f5ef",
+ "type-bold": "\f5f0",
+ "type-h1": "\f5f1",
+ "type-h2": "\f5f2",
+ "type-h3": "\f5f3",
+ "type-italic": "\f5f4",
+ "type-strikethrough": "\f5f5",
+ "type-underline": "\f5f6",
+ "type": "\f5f7",
+ "ui-checks-grid": "\f5f8",
+ "ui-checks": "\f5f9",
+ "ui-radios-grid": "\f5fa",
+ "ui-radios": "\f5fb",
+ "umbrella-fill": "\f5fc",
+ "umbrella": "\f5fd",
+ "union": "\f5fe",
+ "unlock-fill": "\f5ff",
+ "unlock": "\f600",
+ "upc-scan": "\f601",
+ "upc": "\f602",
+ "upload": "\f603",
+ "vector-pen": "\f604",
+ "view-list": "\f605",
+ "view-stacked": "\f606",
+ "vinyl-fill": "\f607",
+ "vinyl": "\f608",
+ "voicemail": "\f609",
+ "volume-down-fill": "\f60a",
+ "volume-down": "\f60b",
+ "volume-mute-fill": "\f60c",
+ "volume-mute": "\f60d",
+ "volume-off-fill": "\f60e",
+ "volume-off": "\f60f",
+ "volume-up-fill": "\f610",
+ "volume-up": "\f611",
+ "vr": "\f612",
+ "wallet-fill": "\f613",
+ "wallet": "\f614",
+ "wallet2": "\f615",
+ "watch": "\f616",
+ "water": "\f617",
+ "whatsapp": "\f618",
+ "wifi-1": "\f619",
+ "wifi-2": "\f61a",
+ "wifi-off": "\f61b",
+ "wifi": "\f61c",
+ "wind": "\f61d",
+ "window-dock": "\f61e",
+ "window-sidebar": "\f61f",
+ "window": "\f620",
+ "wrench": "\f621",
+ "x-circle-fill": "\f622",
+ "x-circle": "\f623",
+ "x-diamond-fill": "\f624",
+ "x-diamond": "\f625",
+ "x-octagon-fill": "\f626",
+ "x-octagon": "\f627",
+ "x-square-fill": "\f628",
+ "x-square": "\f629",
+ "x": "\f62a",
+ "youtube": "\f62b",
+ "zoom-in": "\f62c",
+ "zoom-out": "\f62d",
+ "bank": "\f62e",
+ "bank2": "\f62f",
+ "bell-slash-fill": "\f630",
+ "bell-slash": "\f631",
+ "cash-coin": "\f632",
+ "check-lg": "\f633",
+ "coin": "\f634",
+ "currency-bitcoin": "\f635",
+ "currency-dollar": "\f636",
+ "currency-euro": "\f637",
+ "currency-exchange": "\f638",
+ "currency-pound": "\f639",
+ "currency-yen": "\f63a",
+ "dash-lg": "\f63b",
+ "exclamation-lg": "\f63c",
+ "file-earmark-pdf-fill": "\f63d",
+ "file-earmark-pdf": "\f63e",
+ "file-pdf-fill": "\f63f",
+ "file-pdf": "\f640",
+ "gender-ambiguous": "\f641",
+ "gender-female": "\f642",
+ "gender-male": "\f643",
+ "gender-trans": "\f644",
+ "headset-vr": "\f645",
+ "info-lg": "\f646",
+ "mastodon": "\f647",
+ "messenger": "\f648",
+ "piggy-bank-fill": "\f649",
+ "piggy-bank": "\f64a",
+ "pin-map-fill": "\f64b",
+ "pin-map": "\f64c",
+ "plus-lg": "\f64d",
+ "question-lg": "\f64e",
+ "recycle": "\f64f",
+ "reddit": "\f650",
+ "safe-fill": "\f651",
+ "safe2-fill": "\f652",
+ "safe2": "\f653",
+ "sd-card-fill": "\f654",
+ "sd-card": "\f655",
+ "skype": "\f656",
+ "slash-lg": "\f657",
+ "translate": "\f658",
+ "x-lg": "\f659",
+ "safe": "\f65a",
+ "apple": "\f65b",
+ "microsoft": "\f65d",
+ "windows": "\f65e",
+ "behance": "\f65c",
+ "dribbble": "\f65f",
+ "line": "\f660",
+ "medium": "\f661",
+ "paypal": "\f662",
+ "pinterest": "\f663",
+ "signal": "\f664",
+ "snapchat": "\f665",
+ "spotify": "\f666",
+ "stack-overflow": "\f667",
+ "strava": "\f668",
+ "wordpress": "\f669",
+ "vimeo": "\f66a",
+ "activity": "\f66b",
+ "easel2-fill": "\f66c",
+ "easel2": "\f66d",
+ "easel3-fill": "\f66e",
+ "easel3": "\f66f",
+ "fan": "\f670",
+ "fingerprint": "\f671",
+ "graph-down-arrow": "\f672",
+ "graph-up-arrow": "\f673",
+ "hypnotize": "\f674",
+ "magic": "\f675",
+ "person-rolodex": "\f676",
+ "person-video": "\f677",
+ "person-video2": "\f678",
+ "person-video3": "\f679",
+ "person-workspace": "\f67a",
+ "radioactive": "\f67b",
+ "webcam-fill": "\f67c",
+ "webcam": "\f67d",
+ "yin-yang": "\f67e",
+ "bandaid-fill": "\f680",
+ "bandaid": "\f681",
+ "bluetooth": "\f682",
+ "body-text": "\f683",
+ "boombox": "\f684",
+ "boxes": "\f685",
+ "dpad-fill": "\f686",
+ "dpad": "\f687",
+ "ear-fill": "\f688",
+ "ear": "\f689",
+ "envelope-check-fill": "\f68b",
+ "envelope-check": "\f68c",
+ "envelope-dash-fill": "\f68e",
+ "envelope-dash": "\f68f",
+ "envelope-exclamation-fill": "\f691",
+ "envelope-exclamation": "\f692",
+ "envelope-plus-fill": "\f693",
+ "envelope-plus": "\f694",
+ "envelope-slash-fill": "\f696",
+ "envelope-slash": "\f697",
+ "envelope-x-fill": "\f699",
+ "envelope-x": "\f69a",
+ "explicit-fill": "\f69b",
+ "explicit": "\f69c",
+ "git": "\f69d",
+ "infinity": "\f69e",
+ "list-columns-reverse": "\f69f",
+ "list-columns": "\f6a0",
+ "meta": "\f6a1",
+ "nintendo-switch": "\f6a4",
+ "pc-display-horizontal": "\f6a5",
+ "pc-display": "\f6a6",
+ "pc-horizontal": "\f6a7",
+ "pc": "\f6a8",
+ "playstation": "\f6a9",
+ "plus-slash-minus": "\f6aa",
+ "projector-fill": "\f6ab",
+ "projector": "\f6ac",
+ "qr-code-scan": "\f6ad",
+ "qr-code": "\f6ae",
+ "quora": "\f6af",
+ "quote": "\f6b0",
+ "robot": "\f6b1",
+ "send-check-fill": "\f6b2",
+ "send-check": "\f6b3",
+ "send-dash-fill": "\f6b4",
+ "send-dash": "\f6b5",
+ "send-exclamation-fill": "\f6b7",
+ "send-exclamation": "\f6b8",
+ "send-fill": "\f6b9",
+ "send-plus-fill": "\f6ba",
+ "send-plus": "\f6bb",
+ "send-slash-fill": "\f6bc",
+ "send-slash": "\f6bd",
+ "send-x-fill": "\f6be",
+ "send-x": "\f6bf",
+ "send": "\f6c0",
+ "steam": "\f6c1",
+ "terminal-dash": "\f6c3",
+ "terminal-plus": "\f6c4",
+ "terminal-split": "\f6c5",
+ "ticket-detailed-fill": "\f6c6",
+ "ticket-detailed": "\f6c7",
+ "ticket-fill": "\f6c8",
+ "ticket-perforated-fill": "\f6c9",
+ "ticket-perforated": "\f6ca",
+ "ticket": "\f6cb",
+ "tiktok": "\f6cc",
+ "window-dash": "\f6cd",
+ "window-desktop": "\f6ce",
+ "window-fullscreen": "\f6cf",
+ "window-plus": "\f6d0",
+ "window-split": "\f6d1",
+ "window-stack": "\f6d2",
+ "window-x": "\f6d3",
+ "xbox": "\f6d4",
+ "ethernet": "\f6d5",
+ "hdmi-fill": "\f6d6",
+ "hdmi": "\f6d7",
+ "usb-c-fill": "\f6d8",
+ "usb-c": "\f6d9",
+ "usb-fill": "\f6da",
+ "usb-plug-fill": "\f6db",
+ "usb-plug": "\f6dc",
+ "usb-symbol": "\f6dd",
+ "usb": "\f6de",
+ "boombox-fill": "\f6df",
+ "displayport": "\f6e1",
+ "gpu-card": "\f6e2",
+ "memory": "\f6e3",
+ "modem-fill": "\f6e4",
+ "modem": "\f6e5",
+ "motherboard-fill": "\f6e6",
+ "motherboard": "\f6e7",
+ "optical-audio-fill": "\f6e8",
+ "optical-audio": "\f6e9",
+ "pci-card": "\f6ea",
+ "router-fill": "\f6eb",
+ "router": "\f6ec",
+ "thunderbolt-fill": "\f6ef",
+ "thunderbolt": "\f6f0",
+ "usb-drive-fill": "\f6f1",
+ "usb-drive": "\f6f2",
+ "usb-micro-fill": "\f6f3",
+ "usb-micro": "\f6f4",
+ "usb-mini-fill": "\f6f5",
+ "usb-mini": "\f6f6",
+ "cloud-haze2": "\f6f7",
+ "device-hdd-fill": "\f6f8",
+ "device-hdd": "\f6f9",
+ "device-ssd-fill": "\f6fa",
+ "device-ssd": "\f6fb",
+ "displayport-fill": "\f6fc",
+ "mortarboard-fill": "\f6fd",
+ "mortarboard": "\f6fe",
+ "terminal-x": "\f6ff",
+ "arrow-through-heart-fill": "\f700",
+ "arrow-through-heart": "\f701",
+ "badge-sd-fill": "\f702",
+ "badge-sd": "\f703",
+ "bag-heart-fill": "\f704",
+ "bag-heart": "\f705",
+ "balloon-fill": "\f706",
+ "balloon-heart-fill": "\f707",
+ "balloon-heart": "\f708",
+ "balloon": "\f709",
+ "box2-fill": "\f70a",
+ "box2-heart-fill": "\f70b",
+ "box2-heart": "\f70c",
+ "box2": "\f70d",
+ "braces-asterisk": "\f70e",
+ "calendar-heart-fill": "\f70f",
+ "calendar-heart": "\f710",
+ "calendar2-heart-fill": "\f711",
+ "calendar2-heart": "\f712",
+ "chat-heart-fill": "\f713",
+ "chat-heart": "\f714",
+ "chat-left-heart-fill": "\f715",
+ "chat-left-heart": "\f716",
+ "chat-right-heart-fill": "\f717",
+ "chat-right-heart": "\f718",
+ "chat-square-heart-fill": "\f719",
+ "chat-square-heart": "\f71a",
+ "clipboard-check-fill": "\f71b",
+ "clipboard-data-fill": "\f71c",
+ "clipboard-fill": "\f71d",
+ "clipboard-heart-fill": "\f71e",
+ "clipboard-heart": "\f71f",
+ "clipboard-minus-fill": "\f720",
+ "clipboard-plus-fill": "\f721",
+ "clipboard-pulse": "\f722",
+ "clipboard-x-fill": "\f723",
+ "clipboard2-check-fill": "\f724",
+ "clipboard2-check": "\f725",
+ "clipboard2-data-fill": "\f726",
+ "clipboard2-data": "\f727",
+ "clipboard2-fill": "\f728",
+ "clipboard2-heart-fill": "\f729",
+ "clipboard2-heart": "\f72a",
+ "clipboard2-minus-fill": "\f72b",
+ "clipboard2-minus": "\f72c",
+ "clipboard2-plus-fill": "\f72d",
+ "clipboard2-plus": "\f72e",
+ "clipboard2-pulse-fill": "\f72f",
+ "clipboard2-pulse": "\f730",
+ "clipboard2-x-fill": "\f731",
+ "clipboard2-x": "\f732",
+ "clipboard2": "\f733",
+ "emoji-kiss-fill": "\f734",
+ "emoji-kiss": "\f735",
+ "envelope-heart-fill": "\f736",
+ "envelope-heart": "\f737",
+ "envelope-open-heart-fill": "\f738",
+ "envelope-open-heart": "\f739",
+ "envelope-paper-fill": "\f73a",
+ "envelope-paper-heart-fill": "\f73b",
+ "envelope-paper-heart": "\f73c",
+ "envelope-paper": "\f73d",
+ "filetype-aac": "\f73e",
+ "filetype-ai": "\f73f",
+ "filetype-bmp": "\f740",
+ "filetype-cs": "\f741",
+ "filetype-css": "\f742",
+ "filetype-csv": "\f743",
+ "filetype-doc": "\f744",
+ "filetype-docx": "\f745",
+ "filetype-exe": "\f746",
+ "filetype-gif": "\f747",
+ "filetype-heic": "\f748",
+ "filetype-html": "\f749",
+ "filetype-java": "\f74a",
+ "filetype-jpg": "\f74b",
+ "filetype-js": "\f74c",
+ "filetype-jsx": "\f74d",
+ "filetype-key": "\f74e",
+ "filetype-m4p": "\f74f",
+ "filetype-md": "\f750",
+ "filetype-mdx": "\f751",
+ "filetype-mov": "\f752",
+ "filetype-mp3": "\f753",
+ "filetype-mp4": "\f754",
+ "filetype-otf": "\f755",
+ "filetype-pdf": "\f756",
+ "filetype-php": "\f757",
+ "filetype-png": "\f758",
+ "filetype-ppt": "\f75a",
+ "filetype-psd": "\f75b",
+ "filetype-py": "\f75c",
+ "filetype-raw": "\f75d",
+ "filetype-rb": "\f75e",
+ "filetype-sass": "\f75f",
+ "filetype-scss": "\f760",
+ "filetype-sh": "\f761",
+ "filetype-svg": "\f762",
+ "filetype-tiff": "\f763",
+ "filetype-tsx": "\f764",
+ "filetype-ttf": "\f765",
+ "filetype-txt": "\f766",
+ "filetype-wav": "\f767",
+ "filetype-woff": "\f768",
+ "filetype-xls": "\f76a",
+ "filetype-xml": "\f76b",
+ "filetype-yml": "\f76c",
+ "heart-arrow": "\f76d",
+ "heart-pulse-fill": "\f76e",
+ "heart-pulse": "\f76f",
+ "heartbreak-fill": "\f770",
+ "heartbreak": "\f771",
+ "hearts": "\f772",
+ "hospital-fill": "\f773",
+ "hospital": "\f774",
+ "house-heart-fill": "\f775",
+ "house-heart": "\f776",
+ "incognito": "\f777",
+ "magnet-fill": "\f778",
+ "magnet": "\f779",
+ "person-heart": "\f77a",
+ "person-hearts": "\f77b",
+ "phone-flip": "\f77c",
+ "plugin": "\f77d",
+ "postage-fill": "\f77e",
+ "postage-heart-fill": "\f77f",
+ "postage-heart": "\f780",
+ "postage": "\f781",
+ "postcard-fill": "\f782",
+ "postcard-heart-fill": "\f783",
+ "postcard-heart": "\f784",
+ "postcard": "\f785",
+ "search-heart-fill": "\f786",
+ "search-heart": "\f787",
+ "sliders2-vertical": "\f788",
+ "sliders2": "\f789",
+ "trash3-fill": "\f78a",
+ "trash3": "\f78b",
+ "valentine": "\f78c",
+ "valentine2": "\f78d",
+ "wrench-adjustable-circle-fill": "\f78e",
+ "wrench-adjustable-circle": "\f78f",
+ "wrench-adjustable": "\f790",
+ "filetype-json": "\f791",
+ "filetype-pptx": "\f792",
+ "filetype-xlsx": "\f793",
+ "1-circle-fill": "\f796",
+ "1-circle": "\f797",
+ "1-square-fill": "\f798",
+ "1-square": "\f799",
+ "2-circle-fill": "\f79c",
+ "2-circle": "\f79d",
+ "2-square-fill": "\f79e",
+ "2-square": "\f79f",
+ "3-circle-fill": "\f7a2",
+ "3-circle": "\f7a3",
+ "3-square-fill": "\f7a4",
+ "3-square": "\f7a5",
+ "4-circle-fill": "\f7a8",
+ "4-circle": "\f7a9",
+ "4-square-fill": "\f7aa",
+ "4-square": "\f7ab",
+ "5-circle-fill": "\f7ae",
+ "5-circle": "\f7af",
+ "5-square-fill": "\f7b0",
+ "5-square": "\f7b1",
+ "6-circle-fill": "\f7b4",
+ "6-circle": "\f7b5",
+ "6-square-fill": "\f7b6",
+ "6-square": "\f7b7",
+ "7-circle-fill": "\f7ba",
+ "7-circle": "\f7bb",
+ "7-square-fill": "\f7bc",
+ "7-square": "\f7bd",
+ "8-circle-fill": "\f7c0",
+ "8-circle": "\f7c1",
+ "8-square-fill": "\f7c2",
+ "8-square": "\f7c3",
+ "9-circle-fill": "\f7c6",
+ "9-circle": "\f7c7",
+ "9-square-fill": "\f7c8",
+ "9-square": "\f7c9",
+ "airplane-engines-fill": "\f7ca",
+ "airplane-engines": "\f7cb",
+ "airplane-fill": "\f7cc",
+ "airplane": "\f7cd",
+ "alexa": "\f7ce",
+ "alipay": "\f7cf",
+ "android": "\f7d0",
+ "android2": "\f7d1",
+ "box-fill": "\f7d2",
+ "box-seam-fill": "\f7d3",
+ "browser-chrome": "\f7d4",
+ "browser-edge": "\f7d5",
+ "browser-firefox": "\f7d6",
+ "browser-safari": "\f7d7",
+ "c-circle-fill": "\f7da",
+ "c-circle": "\f7db",
+ "c-square-fill": "\f7dc",
+ "c-square": "\f7dd",
+ "capsule-pill": "\f7de",
+ "capsule": "\f7df",
+ "car-front-fill": "\f7e0",
+ "car-front": "\f7e1",
+ "cassette-fill": "\f7e2",
+ "cassette": "\f7e3",
+ "cc-circle-fill": "\f7e6",
+ "cc-circle": "\f7e7",
+ "cc-square-fill": "\f7e8",
+ "cc-square": "\f7e9",
+ "cup-hot-fill": "\f7ea",
+ "cup-hot": "\f7eb",
+ "currency-rupee": "\f7ec",
+ "dropbox": "\f7ed",
+ "escape": "\f7ee",
+ "fast-forward-btn-fill": "\f7ef",
+ "fast-forward-btn": "\f7f0",
+ "fast-forward-circle-fill": "\f7f1",
+ "fast-forward-circle": "\f7f2",
+ "fast-forward-fill": "\f7f3",
+ "fast-forward": "\f7f4",
+ "filetype-sql": "\f7f5",
+ "fire": "\f7f6",
+ "google-play": "\f7f7",
+ "h-circle-fill": "\f7fa",
+ "h-circle": "\f7fb",
+ "h-square-fill": "\f7fc",
+ "h-square": "\f7fd",
+ "indent": "\f7fe",
+ "lungs-fill": "\f7ff",
+ "lungs": "\f800",
+ "microsoft-teams": "\f801",
+ "p-circle-fill": "\f804",
+ "p-circle": "\f805",
+ "p-square-fill": "\f806",
+ "p-square": "\f807",
+ "pass-fill": "\f808",
+ "pass": "\f809",
+ "prescription": "\f80a",
+ "prescription2": "\f80b",
+ "r-circle-fill": "\f80e",
+ "r-circle": "\f80f",
+ "r-square-fill": "\f810",
+ "r-square": "\f811",
+ "repeat-1": "\f812",
+ "repeat": "\f813",
+ "rewind-btn-fill": "\f814",
+ "rewind-btn": "\f815",
+ "rewind-circle-fill": "\f816",
+ "rewind-circle": "\f817",
+ "rewind-fill": "\f818",
+ "rewind": "\f819",
+ "train-freight-front-fill": "\f81a",
+ "train-freight-front": "\f81b",
+ "train-front-fill": "\f81c",
+ "train-front": "\f81d",
+ "train-lightrail-front-fill": "\f81e",
+ "train-lightrail-front": "\f81f",
+ "truck-front-fill": "\f820",
+ "truck-front": "\f821",
+ "ubuntu": "\f822",
+ "unindent": "\f823",
+ "unity": "\f824",
+ "universal-access-circle": "\f825",
+ "universal-access": "\f826",
+ "virus": "\f827",
+ "virus2": "\f828",
+ "wechat": "\f829",
+ "yelp": "\f82a",
+ "sign-stop-fill": "\f82b",
+ "sign-stop-lights-fill": "\f82c",
+ "sign-stop-lights": "\f82d",
+ "sign-stop": "\f82e",
+ "sign-turn-left-fill": "\f82f",
+ "sign-turn-left": "\f830",
+ "sign-turn-right-fill": "\f831",
+ "sign-turn-right": "\f832",
+ "sign-turn-slight-left-fill": "\f833",
+ "sign-turn-slight-left": "\f834",
+ "sign-turn-slight-right-fill": "\f835",
+ "sign-turn-slight-right": "\f836",
+ "sign-yield-fill": "\f837",
+ "sign-yield": "\f838",
+ "ev-station-fill": "\f839",
+ "ev-station": "\f83a",
+ "fuel-pump-diesel-fill": "\f83b",
+ "fuel-pump-diesel": "\f83c",
+ "fuel-pump-fill": "\f83d",
+ "fuel-pump": "\f83e",
+ "0-circle-fill": "\f83f",
+ "0-circle": "\f840",
+ "0-square-fill": "\f841",
+ "0-square": "\f842",
+ "rocket-fill": "\f843",
+ "rocket-takeoff-fill": "\f844",
+ "rocket-takeoff": "\f845",
+ "rocket": "\f846",
+ "stripe": "\f847",
+ "subscript": "\f848",
+ "superscript": "\f849",
+ "trello": "\f84a",
+ "envelope-at-fill": "\f84b",
+ "envelope-at": "\f84c",
+ "regex": "\f84d",
+ "text-wrap": "\f84e",
+ "sign-dead-end-fill": "\f84f",
+ "sign-dead-end": "\f850",
+ "sign-do-not-enter-fill": "\f851",
+ "sign-do-not-enter": "\f852",
+ "sign-intersection-fill": "\f853",
+ "sign-intersection-side-fill": "\f854",
+ "sign-intersection-side": "\f855",
+ "sign-intersection-t-fill": "\f856",
+ "sign-intersection-t": "\f857",
+ "sign-intersection-y-fill": "\f858",
+ "sign-intersection-y": "\f859",
+ "sign-intersection": "\f85a",
+ "sign-merge-left-fill": "\f85b",
+ "sign-merge-left": "\f85c",
+ "sign-merge-right-fill": "\f85d",
+ "sign-merge-right": "\f85e",
+ "sign-no-left-turn-fill": "\f85f",
+ "sign-no-left-turn": "\f860",
+ "sign-no-parking-fill": "\f861",
+ "sign-no-parking": "\f862",
+ "sign-no-right-turn-fill": "\f863",
+ "sign-no-right-turn": "\f864",
+ "sign-railroad-fill": "\f865",
+ "sign-railroad": "\f866",
+ "building-add": "\f867",
+ "building-check": "\f868",
+ "building-dash": "\f869",
+ "building-down": "\f86a",
+ "building-exclamation": "\f86b",
+ "building-fill-add": "\f86c",
+ "building-fill-check": "\f86d",
+ "building-fill-dash": "\f86e",
+ "building-fill-down": "\f86f",
+ "building-fill-exclamation": "\f870",
+ "building-fill-gear": "\f871",
+ "building-fill-lock": "\f872",
+ "building-fill-slash": "\f873",
+ "building-fill-up": "\f874",
+ "building-fill-x": "\f875",
+ "building-fill": "\f876",
+ "building-gear": "\f877",
+ "building-lock": "\f878",
+ "building-slash": "\f879",
+ "building-up": "\f87a",
+ "building-x": "\f87b",
+ "buildings-fill": "\f87c",
+ "buildings": "\f87d",
+ "bus-front-fill": "\f87e",
+ "bus-front": "\f87f",
+ "ev-front-fill": "\f880",
+ "ev-front": "\f881",
+ "globe-americas": "\f882",
+ "globe-asia-australia": "\f883",
+ "globe-central-south-asia": "\f884",
+ "globe-europe-africa": "\f885",
+ "house-add-fill": "\f886",
+ "house-add": "\f887",
+ "house-check-fill": "\f888",
+ "house-check": "\f889",
+ "house-dash-fill": "\f88a",
+ "house-dash": "\f88b",
+ "house-down-fill": "\f88c",
+ "house-down": "\f88d",
+ "house-exclamation-fill": "\f88e",
+ "house-exclamation": "\f88f",
+ "house-gear-fill": "\f890",
+ "house-gear": "\f891",
+ "house-lock-fill": "\f892",
+ "house-lock": "\f893",
+ "house-slash-fill": "\f894",
+ "house-slash": "\f895",
+ "house-up-fill": "\f896",
+ "house-up": "\f897",
+ "house-x-fill": "\f898",
+ "house-x": "\f899",
+ "person-add": "\f89a",
+ "person-down": "\f89b",
+ "person-exclamation": "\f89c",
+ "person-fill-add": "\f89d",
+ "person-fill-check": "\f89e",
+ "person-fill-dash": "\f89f",
+ "person-fill-down": "\f8a0",
+ "person-fill-exclamation": "\f8a1",
+ "person-fill-gear": "\f8a2",
+ "person-fill-lock": "\f8a3",
+ "person-fill-slash": "\f8a4",
+ "person-fill-up": "\f8a5",
+ "person-fill-x": "\f8a6",
+ "person-gear": "\f8a7",
+ "person-lock": "\f8a8",
+ "person-slash": "\f8a9",
+ "person-up": "\f8aa",
+ "scooter": "\f8ab",
+ "taxi-front-fill": "\f8ac",
+ "taxi-front": "\f8ad",
+ "amd": "\f8ae",
+ "database-add": "\f8af",
+ "database-check": "\f8b0",
+ "database-dash": "\f8b1",
+ "database-down": "\f8b2",
+ "database-exclamation": "\f8b3",
+ "database-fill-add": "\f8b4",
+ "database-fill-check": "\f8b5",
+ "database-fill-dash": "\f8b6",
+ "database-fill-down": "\f8b7",
+ "database-fill-exclamation": "\f8b8",
+ "database-fill-gear": "\f8b9",
+ "database-fill-lock": "\f8ba",
+ "database-fill-slash": "\f8bb",
+ "database-fill-up": "\f8bc",
+ "database-fill-x": "\f8bd",
+ "database-fill": "\f8be",
+ "database-gear": "\f8bf",
+ "database-lock": "\f8c0",
+ "database-slash": "\f8c1",
+ "database-up": "\f8c2",
+ "database-x": "\f8c3",
+ "database": "\f8c4",
+ "houses-fill": "\f8c5",
+ "houses": "\f8c6",
+ "nvidia": "\f8c7",
+ "person-vcard-fill": "\f8c8",
+ "person-vcard": "\f8c9",
+ "sina-weibo": "\f8ca",
+ "tencent-qq": "\f8cb",
+ "wikipedia": "\f8cc",
+ "alphabet-uppercase": "\f2a5",
+ "alphabet": "\f68a",
+ "amazon": "\f68d",
+ "arrows-collapse-vertical": "\f690",
+ "arrows-expand-vertical": "\f695",
+ "arrows-vertical": "\f698",
+ "arrows": "\f6a2",
+ "ban-fill": "\f6a3",
+ "ban": "\f6b6",
+ "bing": "\f6c2",
+ "cake": "\f6e0",
+ "cake2": "\f6ed",
+ "cookie": "\f6ee",
+ "copy": "\f759",
+ "crosshair": "\f769",
+ "crosshair2": "\f794",
+ "emoji-astonished-fill": "\f795",
+ "emoji-astonished": "\f79a",
+ "emoji-grimace-fill": "\f79b",
+ "emoji-grimace": "\f7a0",
+ "emoji-grin-fill": "\f7a1",
+ "emoji-grin": "\f7a6",
+ "emoji-surprise-fill": "\f7a7",
+ "emoji-surprise": "\f7ac",
+ "emoji-tear-fill": "\f7ad",
+ "emoji-tear": "\f7b2",
+ "envelope-arrow-down-fill": "\f7b3",
+ "envelope-arrow-down": "\f7b8",
+ "envelope-arrow-up-fill": "\f7b9",
+ "envelope-arrow-up": "\f7be",
+ "feather": "\f7bf",
+ "feather2": "\f7c4",
+ "floppy-fill": "\f7c5",
+ "floppy": "\f7d8",
+ "floppy2-fill": "\f7d9",
+ "floppy2": "\f7e4",
+ "gitlab": "\f7e5",
+ "highlighter": "\f7f8",
+ "marker-tip": "\f802",
+ "nvme-fill": "\f803",
+ "nvme": "\f80c",
+ "opencollective": "\f80d",
+ "pci-card-network": "\f8cd",
+ "pci-card-sound": "\f8ce",
+ "radar": "\f8cf",
+ "send-arrow-down-fill": "\f8d0",
+ "send-arrow-down": "\f8d1",
+ "send-arrow-up-fill": "\f8d2",
+ "send-arrow-up": "\f8d3",
+ "sim-slash-fill": "\f8d4",
+ "sim-slash": "\f8d5",
+ "sourceforge": "\f8d6",
+ "substack": "\f8d7",
+ "threads-fill": "\f8d8",
+ "threads": "\f8d9",
+ "transparency": "\f8da",
+ "twitter-x": "\f8db",
+ "type-h4": "\f8dc",
+ "type-h5": "\f8dd",
+ "type-h6": "\f8de",
+ "backpack-fill": "\f8df",
+ "backpack": "\f8e0",
+ "backpack2-fill": "\f8e1",
+ "backpack2": "\f8e2",
+ "backpack3-fill": "\f8e3",
+ "backpack3": "\f8e4",
+ "backpack4-fill": "\f8e5",
+ "backpack4": "\f8e6",
+ "brilliance": "\f8e7",
+ "cake-fill": "\f8e8",
+ "cake2-fill": "\f8e9",
+ "duffle-fill": "\f8ea",
+ "duffle": "\f8eb",
+ "exposure": "\f8ec",
+ "gender-neuter": "\f8ed",
+ "highlights": "\f8ee",
+ "luggage-fill": "\f8ef",
+ "luggage": "\f8f0",
+ "mailbox-flag": "\f8f1",
+ "mailbox2-flag": "\f8f2",
+ "noise-reduction": "\f8f3",
+ "passport-fill": "\f8f4",
+ "passport": "\f8f5",
+ "person-arms-up": "\f8f6",
+ "person-raised-hand": "\f8f7",
+ "person-standing-dress": "\f8f8",
+ "person-standing": "\f8f9",
+ "person-walking": "\f8fa",
+ "person-wheelchair": "\f8fb",
+ "shadows": "\f8fc",
+ "suitcase-fill": "\f8fd",
+ "suitcase-lg-fill": "\f8fe",
+ "suitcase-lg": "\f8ff",
+ "suitcase": "\f900",
+ "suitcase2-fill": "\f901",
+ "suitcase2": "\f902",
+ "vignette": "\f903",
+);
+
+@each $icon, $codepoint in $bootstrap-icons-map {
+ .bi-#{$icon}::before { content: $codepoint; }
+}
diff --git a/resources/assets/vendor/fonts/bootstrap-icons/bootstrap-icons.woff b/resources/assets/vendor/fonts/bootstrap-icons/bootstrap-icons.woff
new file mode 100644
index 0000000000000000000000000000000000000000..51204d27de92c7bb0f8bed6165b9dc888f38ff38
GIT binary patch
literal 176032
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