first commit
This commit is contained in:
		
							
								
								
									
										18
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
							
								
								
									
										38
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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 | ||||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | /node_modules | ||||||
|  | /vendor | ||||||
|  | /.vscode | ||||||
|  | /.nova | ||||||
|  | /.fleet | ||||||
|  | /.phpactor.json | ||||||
|  | /.phpunit.cache | ||||||
|  | /.phpunit.result.cache | ||||||
|  | /.zed | ||||||
|  | /.idea | ||||||
							
								
								
									
										16
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							| @ -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/ | ||||||
							
								
								
									
										29
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								Actions/Fortify/CreateNewUser.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Actions/Fortify/CreateNewUser.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Actions\Fortify; | ||||||
|  |  | ||||||
|  | use Laravel\Fortify\Contracts\CreatesNewUsers; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
|  | use Illuminate\Support\Facades\Validator; | ||||||
|  | use Illuminate\Validation\Rule; | ||||||
|  |  | ||||||
|  | class CreateNewUser implements CreatesNewUsers | ||||||
|  | { | ||||||
|  |     use PasswordValidationRules; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Validate and create a newly registered user. | ||||||
|  |      * | ||||||
|  |      * @param  array<string, string>  $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']), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								Actions/Fortify/PasswordValidationRules.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Actions/Fortify/PasswordValidationRules.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Actions\Fortify; | ||||||
|  |  | ||||||
|  | use Illuminate\Validation\Rules\Password; | ||||||
|  |  | ||||||
|  | trait PasswordValidationRules | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Get the validation rules used to validate passwords. | ||||||
|  |      * | ||||||
|  |      * @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string> | ||||||
|  |      */ | ||||||
|  |     protected function passwordRules(): array | ||||||
|  |     { | ||||||
|  |         return ['required', 'string', Password::default(), 'confirmed']; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								Actions/Fortify/ResetUserPassword.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Actions/Fortify/ResetUserPassword.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Actions\Fortify; | ||||||
|  |  | ||||||
|  | use Laravel\Fortify\Contracts\ResetsUserPasswords; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
|  | use Illuminate\Support\Facades\Validator; | ||||||
|  |  | ||||||
|  | class ResetUserPassword implements ResetsUserPasswords | ||||||
|  | { | ||||||
|  |     use PasswordValidationRules; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Validate and reset the user's forgotten password. | ||||||
|  |      * | ||||||
|  |      * @param  array<string, string>  $input | ||||||
|  |      */ | ||||||
|  |     public function reset(User $user, array $input): void | ||||||
|  |     { | ||||||
|  |         Validator::make($input, [ | ||||||
|  |             'password' => $this->passwordRules(), | ||||||
|  |         ])->validate(); | ||||||
|  |  | ||||||
|  |         $user->forceFill([ | ||||||
|  |             'password' => Hash::make($input['password']), | ||||||
|  |         ])->save(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								Actions/Fortify/UpdateUserPassword.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								Actions/Fortify/UpdateUserPassword.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Actions\Fortify; | ||||||
|  |  | ||||||
|  | use Laravel\Fortify\Contracts\UpdatesUserPasswords; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
|  | use Illuminate\Support\Facades\Validator; | ||||||
|  |  | ||||||
|  | class UpdateUserPassword implements UpdatesUserPasswords | ||||||
|  | { | ||||||
|  |     use PasswordValidationRules; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Validate and update the user's password. | ||||||
|  |      * | ||||||
|  |      * @param  array<string, string>  $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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								Actions/Fortify/UpdateUserProfileInformation.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								Actions/Fortify/UpdateUserProfileInformation.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Actions\Fortify; | ||||||
|  |  | ||||||
|  | use Laravel\Fortify\Contracts\UpdatesUserProfileInformation; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Illuminate\Contracts\Auth\MustVerifyEmail; | ||||||
|  | use Illuminate\Support\Facades\Validator; | ||||||
|  | use Illuminate\Validation\Rule; | ||||||
|  |  | ||||||
|  | class UpdateUserProfileInformation implements UpdatesUserProfileInformation | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Validate and update the given user's profile information. | ||||||
|  |      * | ||||||
|  |      * @param  array<string, string>  $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<string, string>  $input | ||||||
|  |      */ | ||||||
|  |     protected function updateVerifiedUser(User $user, array $input): void | ||||||
|  |     { | ||||||
|  |         $user->forceFill([ | ||||||
|  |             'name' => $input['name'], | ||||||
|  |             'email' => $input['email'], | ||||||
|  |             'email_verified_at' => null, | ||||||
|  |         ])->save(); | ||||||
|  |  | ||||||
|  |         $user->sendEmailVerificationNotification(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @ -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**. | ||||||
							
								
								
									
										9
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							| @ -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**. | ||||||
							
								
								
									
										43
									
								
								Console/Commands/CleanInitialAvatars.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								Console/Commands/CleanInitialAvatars.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Console\Commands; | ||||||
|  |  | ||||||
|  | use Illuminate\Console\Command; | ||||||
|  | use Illuminate\Support\Facades\Storage; | ||||||
|  |  | ||||||
|  | class CleanInitialAvatars extends Command | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * The name and signature of the console command. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     protected $signature = 'avatars:clean-initial'; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The console command description. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     protected $description = 'Elimina avatares generados automáticamente en el directorio initial-avatars'; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Execute the console command. | ||||||
|  |      */ | ||||||
|  |     public function handle() | ||||||
|  |     { | ||||||
|  |         $directory = 'initial-avatars'; | ||||||
|  |         $files = Storage::disk('public')->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.'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								Console/Commands/SyncRBAC.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Console/Commands/SyncRBAC.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Console\Commands; | ||||||
|  |  | ||||||
|  | use Illuminate\Console\Command; | ||||||
|  | use Koneko\VuexyAdmin\Services\RBACService; | ||||||
|  |  | ||||||
|  | class SyncRBAC extends Command | ||||||
|  | { | ||||||
|  |     protected $signature = 'rbac:sync {action}'; | ||||||
|  |     protected $description = 'Sincroniza roles y permisos con archivos JSON'; | ||||||
|  |  | ||||||
|  |     public function handle() | ||||||
|  |     { | ||||||
|  |         $action = $this->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".'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								Helpers/CatalogHelper.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Helpers/CatalogHelper.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Helpers; | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Eloquent\Builder; | ||||||
|  | use Illuminate\Http\JsonResponse; | ||||||
|  |  | ||||||
|  | class CatalogHelper | ||||||
|  | { | ||||||
|  |     public static function ajaxFlexibleResponse(Builder $query, array $options = []): JsonResponse | ||||||
|  |     { | ||||||
|  |         $id = $options['id'] ?? null; | ||||||
|  |         $searchTerm = $options['searchTerm'] ?? null; | ||||||
|  |         $limit = $options['limit'] ?? 20; | ||||||
|  |         $keyField = $options['keyField'] ?? 'id'; | ||||||
|  |         $valueField = $options['valueField'] ?? 'name'; | ||||||
|  |         $responseType = $options['responseType'] ?? 'select2'; | ||||||
|  |         $customFilters = $options['filters'] ?? []; | ||||||
|  |  | ||||||
|  |         // Si se pasa un ID, devolver un registro completo | ||||||
|  |         if ($id) { | ||||||
|  |             $data = $query->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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										209
									
								
								Helpers/VuexyHelper.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								Helpers/VuexyHelper.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,209 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Helpers; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Config; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  |  | ||||||
|  | class VuexyHelper | ||||||
|  | { | ||||||
|  |     public static function appClasses() | ||||||
|  |     { | ||||||
|  |         $data = config('vuexy.custom'); | ||||||
|  |  | ||||||
|  |         // default data array | ||||||
|  |         $DefaultData = [ | ||||||
|  |             'myLayout' => '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); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								Http/Controllers/AdminController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Http/Controllers/AdminController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Koneko\VuexyAdmin\Models\Setting; | ||||||
|  | use Koneko\VuexyAdmin\Services\VuexyAdminService; | ||||||
|  |  | ||||||
|  | class AdminController extends Controller | ||||||
|  | { | ||||||
|  |     public function searchNavbar() | ||||||
|  |     { | ||||||
|  |         abort_if(!request()->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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										144
									
								
								Http/Controllers/AuthController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								Http/Controllers/AuthController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,144 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Laravel\Fortify\Features; | ||||||
|  |  | ||||||
|  | class AuthController extends Controller | ||||||
|  | { | ||||||
|  |     /* | ||||||
|  |     public function loginView() | ||||||
|  |     { | ||||||
|  |         dd($viewMode); | ||||||
|  |         $viewMode = config('vuexy.custom.authViewMode'); | ||||||
|  |         $pageConfigs = ['myLayout' => '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]); | ||||||
|  |     } | ||||||
|  |     */ | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								Http/Controllers/CacheController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								Http/Controllers/CacheController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  | use Illuminate\Support\Facades\Artisan; | ||||||
|  | use Koneko\VuexyAdmin\Services\CacheConfigService; | ||||||
|  |  | ||||||
|  | class CacheController extends Controller | ||||||
|  | { | ||||||
|  |     public function generateConfigCache() | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             // Lógica para generar cache | ||||||
|  |             Artisan::call('config:cache'); | ||||||
|  |  | ||||||
|  |             return response()->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')); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								Http/Controllers/HomeController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								Http/Controllers/HomeController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  |  | ||||||
|  | class HomeController extends Controller | ||||||
|  | { | ||||||
|  |     public function index() | ||||||
|  |     { | ||||||
|  |         return view('vuexy-admin::pages.home'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function about() | ||||||
|  |     { | ||||||
|  |         return view('vuexy-admin::pages.about'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function comingsoon() | ||||||
|  |     { | ||||||
|  |         $pageConfigs = ['myLayout' => 'blank']; | ||||||
|  |  | ||||||
|  |         return view('vuexy-admin::pages.comingsoon', compact('pageConfigs')); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function underMaintenance() | ||||||
|  |     { | ||||||
|  |         $pageConfigs = ['myLayout' => 'blank']; | ||||||
|  |  | ||||||
|  |         return view('vuexy-admin::pages.under-maintenance', compact('pageConfigs')); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								Http/Controllers/LanguageController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Http/Controllers/LanguageController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\Support\Facades\App; | ||||||
|  |  | ||||||
|  | class LanguageController extends Controller | ||||||
|  | { | ||||||
|  |   public function swap(Request $request, $locale) | ||||||
|  |   { | ||||||
|  |     if (!in_array($locale, ['es', 'en', 'fr', 'ar', 'de'])) { | ||||||
|  |       abort(400); | ||||||
|  |     } else { | ||||||
|  |       $request->session()->put('locale', $locale); | ||||||
|  |     } | ||||||
|  |     App::setLocale($locale); | ||||||
|  |     return redirect()->back(); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								Http/Controllers/PermissionController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								Http/Controllers/PermissionController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\Support\Arr; | ||||||
|  | use Spatie\Permission\Models\Permission; | ||||||
|  | use Yajra\DataTables\Facades\DataTables; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  |  | ||||||
|  | class PermissionController extends Controller | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Display a listing of the resource. | ||||||
|  |      * | ||||||
|  |      * @return \Illuminate\Http\Response | ||||||
|  |      */ | ||||||
|  |     public function index(Request $request) | ||||||
|  |     { | ||||||
|  |         if ($request->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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								Http/Controllers/RoleController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								Http/Controllers/RoleController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Spatie\Permission\Models\Role; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  |  | ||||||
|  | class RoleController extends Controller | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Display a listing of the resource. | ||||||
|  |      * | ||||||
|  |      * @return \Illuminate\Http\Response | ||||||
|  |      */ | ||||||
|  |     public function index(Request $request) | ||||||
|  |     { | ||||||
|  |         return view('vuexy-admin::roles.index'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function checkUniqueRoleName(Request $request) | ||||||
|  |     { | ||||||
|  |         $id   = $request->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]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								Http/Controllers/RolePermissionController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Http/Controllers/RolePermissionController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Spatie\Permission\Models\Role; | ||||||
|  | use Spatie\Permission\Models\Permission; | ||||||
|  |  | ||||||
|  | class RolePermissionController extends Controller | ||||||
|  | { | ||||||
|  |     public function index() | ||||||
|  |     { | ||||||
|  |         return response()->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']); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										188
									
								
								Http/Controllers/UserController copy.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								Http/Controllers/UserController copy.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,188 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\Support\Arr; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Illuminate\Support\Facades\Validator; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Yajra\DataTables\Facades\DataTables; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Koneko\VuexyAdmin\Services\AvatarImageService; | ||||||
|  |  | ||||||
|  | class UserController extends Controller | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Display a listing of the resource. | ||||||
|  |      * | ||||||
|  |      * @return \Illuminate\Http\Response | ||||||
|  |      */ | ||||||
|  |     public function index(Request $request) | ||||||
|  |     { | ||||||
|  |         if ($request->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' | ||||||
|  |             ]); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										234
									
								
								Http/Controllers/UserController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								Http/Controllers/UserController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,234 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Illuminate\Support\Facades\{Auth,DB,Validator}; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Koneko\VuexyAdmin\Services\AvatarImageService; | ||||||
|  | use Koneko\VuexyAdmin\Queries\GenericQueryBuilder; | ||||||
|  |  | ||||||
|  | class UserController extends Controller | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Display a listing of the resource. | ||||||
|  |      * | ||||||
|  |      * @return \Illuminate\Http\Response | ||||||
|  |      */ | ||||||
|  |     public function index(Request $request) | ||||||
|  |     { | ||||||
|  |         if ($request->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')); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								Http/Controllers/UserProfileController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								Http/Controllers/UserProfileController.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Controllers; | ||||||
|  |  | ||||||
|  | use App\Http\Controllers\Controller; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Koneko\VuexyAdmin\Services\AvatarInitialsService; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  |  | ||||||
|  | class UserProfileController extends Controller | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Display a listing of the resource. | ||||||
|  |      * | ||||||
|  |      * @return \Illuminate\Http\Response | ||||||
|  |      */ | ||||||
|  |     public function index(Request $request) | ||||||
|  |     { | ||||||
|  |         return view('vuexy-admin::profile.index'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     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); | ||||||
|  |  | ||||||
|  |         $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' | ||||||
|  |             ]); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										37
									
								
								Http/Middleware/AdminTemplateMiddleware.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								Http/Middleware/AdminTemplateMiddleware.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Http\Middleware; | ||||||
|  |  | ||||||
|  | use Closure; | ||||||
|  | use Koneko\VuexyAdmin\Services\AdminTemplateService; | ||||||
|  | use Illuminate\Support\Facades\View; | ||||||
|  | use Koneko\VuexyAdmin\Services\VuexyAdminService; | ||||||
|  |  | ||||||
|  | class AdminTemplateMiddleware | ||||||
|  | { | ||||||
|  |     public function __construct() | ||||||
|  |     { | ||||||
|  |         // | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function handle($request, Closure $next) | ||||||
|  |     { | ||||||
|  |         // Aplicar configuración de layout antes de que la vista se cargue | ||||||
|  |         if (str_contains($request->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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								Listeners/ClearUserCache.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Listeners/ClearUserCache.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Listeners; | ||||||
|  |  | ||||||
|  | use Illuminate\Auth\Events\Logout; | ||||||
|  | use Illuminate\Support\Facades\Log; | ||||||
|  | use Koneko\VuexyAdmin\Services\VuexyAdminService; | ||||||
|  |  | ||||||
|  | class ClearUserCache | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Handle the event. | ||||||
|  |      * | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public function handle(Logout $event) | ||||||
|  |     { | ||||||
|  |         if ($event->user) { | ||||||
|  |             VuexyAdminService::clearUserMenuCache(); | ||||||
|  |             VuexyAdminService::clearSearchMenuCache(); | ||||||
|  |             VuexyAdminService::clearQuickLinksCache(); | ||||||
|  |             VuexyAdminService::clearNotificationsCache(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								Listeners/HandleUserLogin.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Listeners/HandleUserLogin.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Listeners; | ||||||
|  |  | ||||||
|  | use Illuminate\Auth\Events\Login; | ||||||
|  | use Illuminate\Support\Facades\Mail; | ||||||
|  | use Koneko\VuexyAdmin\Models\UserLogin; | ||||||
|  |  | ||||||
|  | class HandleUserLogin | ||||||
|  | { | ||||||
|  |     public function handle(Login $event) | ||||||
|  |     { | ||||||
|  |         // Guardar log en base de datos | ||||||
|  |         UserLogin::create([ | ||||||
|  |             'user_id' => $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)); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										83
									
								
								Livewire/AdminSettings/ApplicationSettings.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								Livewire/AdminSettings/ApplicationSettings.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\AdminSettings; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Livewire\WithFileUploads; | ||||||
|  | use Koneko\VuexyAdmin\Services\AdminSettingsService; | ||||||
|  | use Koneko\VuexyAdmin\Services\AdminTemplateService; | ||||||
|  |  | ||||||
|  | class ApplicationSettings extends Component | ||||||
|  | { | ||||||
|  |     use WithFileUploads; | ||||||
|  |  | ||||||
|  |     private $targetNotify = "#application-settings-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $admin_app_name, | ||||||
|  |         $admin_image_logo, | ||||||
|  |         $admin_image_logo_dark; | ||||||
|  |  | ||||||
|  |     public $upload_image_logo, | ||||||
|  |         $upload_image_logo_dark; | ||||||
|  |  | ||||||
|  |     public function mount() | ||||||
|  |     { | ||||||
|  |         $this->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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										84
									
								
								Livewire/AdminSettings/GeneralSettings.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								Livewire/AdminSettings/GeneralSettings.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\AdminSettings; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Livewire\WithFileUploads; | ||||||
|  | use Koneko\VuexyAdmin\Services\AdminSettingsService; | ||||||
|  | use Koneko\VuexyAdmin\Services\AdminTemplateService; | ||||||
|  |  | ||||||
|  | class GeneralSettings extends Component | ||||||
|  | { | ||||||
|  |     use WithFileUploads; | ||||||
|  |  | ||||||
|  |     private $targetNotify = "#general-settings-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $admin_title; | ||||||
|  |     public $admin_favicon_16x16, | ||||||
|  |         $admin_favicon_76x76, | ||||||
|  |         $admin_favicon_120x120, | ||||||
|  |         $admin_favicon_152x152, | ||||||
|  |         $admin_favicon_180x180, | ||||||
|  |         $admin_favicon_192x192; | ||||||
|  |  | ||||||
|  |     public $upload_image_favicon; | ||||||
|  |  | ||||||
|  |     public function mount() | ||||||
|  |     { | ||||||
|  |         $this->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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										118
									
								
								Livewire/AdminSettings/InterfaceSettings.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								Livewire/AdminSettings/InterfaceSettings.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\AdminSettings; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Koneko\VuexyAdmin\Services\AdminTemplateService; | ||||||
|  | use Koneko\VuexyAdmin\Services\GlobalSettingsService; | ||||||
|  |  | ||||||
|  | class InterfaceSettings extends Component | ||||||
|  | { | ||||||
|  |     private $targetNotify = "#interface-settings-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $vuexy_myLayout, | ||||||
|  |         $vuexy_myTheme, | ||||||
|  |         $vuexy_myStyle, | ||||||
|  |         $vuexy_hasCustomizer, | ||||||
|  |         $vuexy_displayCustomizer, | ||||||
|  |         $vuexy_contentLayout, | ||||||
|  |         $vuexy_navbarType, | ||||||
|  |         $vuexy_footerFixed, | ||||||
|  |         $vuexy_menuFixed, | ||||||
|  |         $vuexy_menuCollapsed, | ||||||
|  |         $vuexy_headerType, | ||||||
|  |         $vuexy_showDropdownOnHover, | ||||||
|  |         $vuexy_authViewMode, | ||||||
|  |         $vuexy_maxQuickLinks; | ||||||
|  |  | ||||||
|  |     public function mount() | ||||||
|  |     { | ||||||
|  |         $this->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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										106
									
								
								Livewire/AdminSettings/MailSenderResponseSettings.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								Livewire/AdminSettings/MailSenderResponseSettings.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,106 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\AdminSettings; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Koneko\VuexyAdmin\Services\GlobalSettingsService; | ||||||
|  |  | ||||||
|  | class MailSenderResponseSettings extends Component | ||||||
|  | { | ||||||
|  |     private $targetNotify = "#mail-sender-response-settings-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $from_address, | ||||||
|  |         $from_name, | ||||||
|  |         $reply_to_method, | ||||||
|  |         $reply_to_email, | ||||||
|  |         $reply_to_name; | ||||||
|  |  | ||||||
|  |     protected $listeners = ['saveMailSenderResponseSettings' => '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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										175
									
								
								Livewire/AdminSettings/MailSmtpSettings.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								Livewire/AdminSettings/MailSmtpSettings.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,175 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\AdminSettings; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Illuminate\Support\Facades\Config; | ||||||
|  | use Illuminate\Support\Facades\Crypt; | ||||||
|  | use Symfony\Component\Mailer\Transport; | ||||||
|  | use Symfony\Component\Mailer\Mailer; | ||||||
|  | use Symfony\Component\Mime\Email; | ||||||
|  | use Koneko\VuexyAdmin\Services\GlobalSettingsService; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MailSmtpSettings extends Component | ||||||
|  | { | ||||||
|  |     private $targetNotify = "#mail-smtp-settings-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $change_smtp_settings, | ||||||
|  |         $host, | ||||||
|  |         $port, | ||||||
|  |         $encryption, | ||||||
|  |         $username, | ||||||
|  |         $password; | ||||||
|  |  | ||||||
|  |     public $save_button_disabled; | ||||||
|  |  | ||||||
|  |     protected $listeners = [ | ||||||
|  |         'loadSettings', | ||||||
|  |         'testSmtpConnection', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     // the list of smtp_encryption values that can be stored in table | ||||||
|  |     const SMTP_ENCRYPTION_SSL  = 'SSL'; | ||||||
|  |     const SMTP_ENCRYPTION_TLS  = 'TLS'; | ||||||
|  |     const SMTP_ENCRYPTION_NONE = 'none'; | ||||||
|  |  | ||||||
|  |     public $encryption_options = [ | ||||||
|  |         self::SMTP_ENCRYPTION_SSL  => '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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										212
									
								
								Livewire/Cache/CacheFunctions.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								Livewire/Cache/CacheFunctions.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,212 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Cache; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Illuminate\Support\Facades\Artisan; | ||||||
|  | use Illuminate\Support\Facades\Cache; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Support\Facades\Redis; | ||||||
|  |  | ||||||
|  | class CacheFunctions extends Component | ||||||
|  | { | ||||||
|  |     private $targetNotify = "#cache-functions-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $cacheCounts = [ | ||||||
|  |         'general' => 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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								Livewire/Cache/CacheStats.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								Livewire/Cache/CacheStats.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Cache; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Koneko\VuexyAdmin\Services\CacheConfigService; | ||||||
|  | use Koneko\VuexyAdmin\Services\CacheManagerService; | ||||||
|  |  | ||||||
|  | class CacheStats extends Component | ||||||
|  | { | ||||||
|  |     private $targetNotify = "#cache-stats-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $cacheConfig = []; | ||||||
|  |     public $cacheStats = []; | ||||||
|  |  | ||||||
|  |     protected $listeners = ['reloadCacheStatsEvent' => '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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								Livewire/Cache/MemcachedStats.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								Livewire/Cache/MemcachedStats.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Cache; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Koneko\VuexyAdmin\Services\CacheManagerService; | ||||||
|  |  | ||||||
|  | class MemcachedStats extends Component | ||||||
|  | { | ||||||
|  |     private $driver       = 'memcached'; | ||||||
|  |     private $targetNotify = "#memcached-stats-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $memcachedStats = []; | ||||||
|  |  | ||||||
|  |     protected $listeners = ['reloadMemcachedStatsEvent' => '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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										64
									
								
								Livewire/Cache/RedisStats.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								Livewire/Cache/RedisStats.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Cache; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Koneko\VuexyAdmin\Services\CacheManagerService; | ||||||
|  |  | ||||||
|  | class RedisStats extends Component | ||||||
|  | { | ||||||
|  |     private $driver       = 'redis'; | ||||||
|  |     private $targetNotify = "#redis-stats-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $redisStats = []; | ||||||
|  |  | ||||||
|  |     protected $listeners = ['reloadRedisStatsEvent' => '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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								Livewire/Cache/SessionStats.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Livewire/Cache/SessionStats.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Cache; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Koneko\VuexyAdmin\Services\CacheConfigService; | ||||||
|  | use Koneko\VuexyAdmin\Services\SessionManagerService; | ||||||
|  |  | ||||||
|  | class SessionStats extends Component | ||||||
|  | { | ||||||
|  |     private $targetNotify = "#session-stats-card .notification-container"; | ||||||
|  |  | ||||||
|  |     public $cacheConfig = []; | ||||||
|  |     public $sessionStats = []; | ||||||
|  |  | ||||||
|  |     protected $listeners = ['reloadSessionStatsEvent' => '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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										515
									
								
								Livewire/Form/AbstractFormComponent.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										515
									
								
								Livewire/Form/AbstractFormComponent.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,515 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Form; | ||||||
|  |  | ||||||
|  | use Exception; | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  | use Illuminate\Database\QueryException; | ||||||
|  | use Illuminate\Database\Eloquent\ModelNotFoundException; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Illuminate\Validation\ValidationException; | ||||||
|  | use Illuminate\View\View; | ||||||
|  | use Livewire\Component; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class AbstractFormComponent | ||||||
|  |  * | ||||||
|  |  * Clase base y abstracta para la creación de formularios con Livewire. | ||||||
|  |  * Proporciona métodos y un flujo general para manejar operaciones CRUD | ||||||
|  |  * (creación, edición y eliminación), validaciones, notificaciones y | ||||||
|  |  * administración de errores en un entorno transaccional. | ||||||
|  |  * | ||||||
|  |  * @package Koneko\VuexyAdmin\Livewire\Form | ||||||
|  |  */ | ||||||
|  | abstract class AbstractFormComponent extends Component | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Identificador único del formulario, útil para distinguir múltiples instancias. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $uniqueId; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Modo actual del formulario: puede ser 'create', 'edit' o 'delete'. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $mode; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Texto que se mostrará en el botón de envío. Se adapta | ||||||
|  |      * automáticamente en función del modo actual (crear, editar o eliminar). | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $btnSubmitText; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * ID del registro que se está editando o eliminando. | ||||||
|  |      * Si el formulario está en modo 'create', puede ser null. | ||||||
|  |      * | ||||||
|  |      * @var int|null | ||||||
|  |      */ | ||||||
|  |     public $id; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Nombre de la etiqueta para generar Componentes | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $tagName; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Nombre de la columna que contiene el nombre del registro. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $columnNameLabel; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Nombre singular del modelo | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $singularName; | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |      * Nombre del identificador del Canvas | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $offcanvasId; | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |      * Nombre del identificador del Form | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $formId; | ||||||
|  |  | ||||||
|  |     // ====================================================================== | ||||||
|  |     //                            MÉTODOS ABSTRACTOS | ||||||
|  |     // ====================================================================== | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Retorna la clase (namespace) del modelo Eloquent asociado al formulario. | ||||||
|  |      * | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     abstract protected function model(): string; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Retorna las reglas de validación de forma dinámica, dependiendo del modo del formulario. | ||||||
|  |      * | ||||||
|  |      * @param string $mode El modo actual del formulario (por ejemplo, 'create', 'edit' o 'delete'). | ||||||
|  |      * @return array Reglas de validación (similares a las usadas en un Request de Laravel). | ||||||
|  |      */ | ||||||
|  |     abstract protected function dynamicRules(string $mode): array; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Inicializa los datos del formulario con base en el registro (si existe) | ||||||
|  |      * y en el modo actual. Útil para prellenar campos en modo 'edit'. | ||||||
|  |      * | ||||||
|  |      * @param mixed  $record El registro encontrado, o null si se crea uno nuevo. | ||||||
|  |      * @param string $mode   El modo actual del formulario. | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     abstract protected function initializeFormData(mixed $record, string $mode): void; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Prepara los datos ya validados para ser guardados en base de datos. | ||||||
|  |      * Permite, por ejemplo, castear valores o limpiar ciertos campos. | ||||||
|  |      * | ||||||
|  |      * @param array $validatedData Datos que ya pasaron la validación. | ||||||
|  |      * @return array Datos listos para el almacenamiento (por ejemplo, en create o update). | ||||||
|  |      */ | ||||||
|  |     abstract protected function prepareData(array $validatedData): array; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Define los contenedores de destino para las notificaciones. | ||||||
|  |      * | ||||||
|  |      * Retorna un array con keys como 'form', 'index', etc., y sus | ||||||
|  |      * valores deben ser selectores o identificadores en la vista, donde | ||||||
|  |      * se inyectarán las notificaciones. | ||||||
|  |      * | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     abstract protected function targetNotifies(): array; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Retorna la ruta de la vista Blade correspondiente a este formulario. | ||||||
|  |      * | ||||||
|  |      * Por ejemplo: 'package::livewire.some-form'. | ||||||
|  |      * | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     abstract protected function viewPath(): string; | ||||||
|  |  | ||||||
|  |     // ====================================================================== | ||||||
|  |     //                         MÉTODOS DE VALIDACIÓN | ||||||
|  |     // ====================================================================== | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Retorna un array que define nombres de atributos personalizados para los mensajes de validación. | ||||||
|  |      * | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     protected function attributes(): array | ||||||
|  |     { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Retorna un array con mensajes de validación personalizados. | ||||||
|  |      * | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     protected function messages(): array | ||||||
|  |     { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // ====================================================================== | ||||||
|  |     //                   INICIALIZACIÓN Y CICLO DE VIDA | ||||||
|  |     // ====================================================================== | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Método que se ejecuta al montar (instanciar) el componente Livewire. | ||||||
|  |      * Inicializa propiedades clave como el $mode, $id, $uniqueId, el texto | ||||||
|  |      * del botón de envío, y carga datos del registro si no es un 'create'. | ||||||
|  |      * | ||||||
|  |      * @param string    $mode Modo del formulario: 'create', 'edit' o 'delete'. | ||||||
|  |      * @param int|null  $id   ID del registro a editar/eliminar (o null para crear). | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public function mount(string $mode = 'create', mixed $id = null): void | ||||||
|  |     { | ||||||
|  |         $this->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()); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										667
									
								
								Livewire/Form/AbstractFormOffCanvasComponent.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										667
									
								
								Livewire/Form/AbstractFormOffCanvasComponent.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,667 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Form; | ||||||
|  |  | ||||||
|  | use Exception; | ||||||
|  | use Illuminate\Database\Eloquent\ModelNotFoundException; | ||||||
|  | use Illuminate\Database\QueryException; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Illuminate\Validation\ValidationException; | ||||||
|  | use Illuminate\View\View; | ||||||
|  | use Livewire\Component; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Clase base abstracta para manejar formularios de tipo Off-Canvas con Livewire. | ||||||
|  |  * | ||||||
|  |  * Esta clase proporciona métodos reutilizables para operaciones CRUD, validaciones dinámicas, | ||||||
|  |  * manejo de transacciones en base de datos y eventos de Livewire. | ||||||
|  |  * | ||||||
|  |  * @package Koneko\VuexyAdmin\Livewire\Form | ||||||
|  |  */ | ||||||
|  | abstract class AbstractFormOffCanvasComponent extends Component | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Identificador único del formulario, usado para evitar conflictos en instancias múltiples. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $uniqueId; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Modo actual del formulario: puede ser 'create', 'edit' o 'delete'. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $mode; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * ID del registro que se está editando o eliminando. | ||||||
|  |      * | ||||||
|  |      * @var int|null | ||||||
|  |      */ | ||||||
|  |     public $id; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Valores por defecto para los campos del formulario, | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     public $defaultValues; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Nombre de la etiqueta para generar Componentes | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $tagName; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Nombre de la columna que contiene el nombre del registro. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $columnNameLabel; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Nombre singular del modelo | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $singularName; | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |      * Nombre del identificador del Canvas | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $offcanvasId; | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |      * Nombre del identificador del Form | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $formId; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Campo que se debe enfocar cuando se abra el formulario. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $focusOnOpen; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Indica si se desea confirmar la eliminación del registro. | ||||||
|  |      * | ||||||
|  |      * @var bool | ||||||
|  |      */ | ||||||
|  |     public $confirmDeletion = false; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Indica si se ha producido un error de validación. | ||||||
|  |      * | ||||||
|  |      * @var bool | ||||||
|  |      */ | ||||||
|  |     public $validationError = false; | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |      * Indica si se ha procesado correctamente el formulario. | ||||||
|  |      * | ||||||
|  |      * @var bool | ||||||
|  |      */ | ||||||
|  |     public $successProcess = false; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Campos que deben ser casteados a tipos específicos. | ||||||
|  |      * | ||||||
|  |      * @var array<string, string> | ||||||
|  |      */ | ||||||
|  |     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<string, mixed> | ||||||
|  |      */ | ||||||
|  |     abstract protected function fields(): array; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Retorna los valores por defecto para los campos del formulario. | ||||||
|  |      * | ||||||
|  |      * @return array<string, mixed> 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<string, mixed> 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<string, mixed> 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<string, mixed> 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()); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										28
									
								
								Livewire/Permissions/PermissionIndex.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Livewire/Permissions/PermissionIndex.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Permissions; | ||||||
|  |  | ||||||
|  | use Spatie\Permission\Models\Role; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  |  | ||||||
|  | class PermissionIndex extends Component | ||||||
|  | { | ||||||
|  |     public $roles_html_select; | ||||||
|  |     public $rows_roles; | ||||||
|  |  | ||||||
|  |     public function render() | ||||||
|  |     { | ||||||
|  |         // Generamos Select y estilos HTML de roles | ||||||
|  |         $this->roles_html_select = "<select id=\"UserRole\" class=\"form-select text-capitalize\"><option value=\"\"> Selecciona un rol </option>"; | ||||||
|  |  | ||||||
|  |         foreach (Role::all() as $role) { | ||||||
|  |             $this->rows_roles[$role->name] = "<span class=\"badge bg-label-{$role->style} m-1\">{$role->name}</span>"; | ||||||
|  |             $this->roles_html_select      .= "<option value=\"{$role->name}\" class=\"text-capitalize\">{$role->name}</option>"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $this->roles_html_select .= "</select>"; | ||||||
|  |  | ||||||
|  |         return view('vuexy-admin::livewire.permissions.index'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								Livewire/Permissions/Permissions.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Livewire/Permissions/Permissions.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Permissions; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Spatie\Permission\Models\Permission; | ||||||
|  |  | ||||||
|  | class Permissions extends Component | ||||||
|  | { | ||||||
|  |     public $permissionName; | ||||||
|  |  | ||||||
|  |     public function createPermission() | ||||||
|  |     { | ||||||
|  |         $this->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() | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										182
									
								
								Livewire/Roles/RoleCards.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								Livewire/Roles/RoleCards.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,182 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Roles; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Spatie\Permission\Models\Permission; | ||||||
|  | use Spatie\Permission\Models\Role; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  |  | ||||||
|  | class RoleCards extends Component | ||||||
|  | { | ||||||
|  |     public $roles = []; | ||||||
|  |     public $permissions = []; | ||||||
|  |     public $roleId; | ||||||
|  |     public $name; | ||||||
|  |     public $style; | ||||||
|  |     public $title; | ||||||
|  |     public $btn_submit_text; | ||||||
|  |     public $permissionsInputs = []; | ||||||
|  |     public $destroyRoleId; | ||||||
|  |  | ||||||
|  |     protected $listeners = ['saveRole', 'deleteRole']; | ||||||
|  |  | ||||||
|  |     public function mount() | ||||||
|  |     { | ||||||
|  |         $this->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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								Livewire/Roles/RoleIndex.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								Livewire/Roles/RoleIndex.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Roles; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  | use Livewire\WithPagination; | ||||||
|  | use Spatie\Permission\Models\Role; | ||||||
|  | use Spatie\Permission\Models\Permission; | ||||||
|  |  | ||||||
|  | class RoleIndex extends Component | ||||||
|  | { | ||||||
|  |     use WithPagination; | ||||||
|  |  | ||||||
|  |     public $roleName; | ||||||
|  |     public $selectedRole; | ||||||
|  |     public $permissions = []; | ||||||
|  |     public $availablePermissions; | ||||||
|  |  | ||||||
|  |     public function mount() | ||||||
|  |     { | ||||||
|  |         $this->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) | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										174
									
								
								Livewire/Table/AbstractIndexComponent.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								Livewire/Table/AbstractIndexComponent.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Table; | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Livewire\Component; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Clase base abstracta para la creación de componentes tipo "Index" con Livewire. | ||||||
|  |  * | ||||||
|  |  * Provee una estructura general para: | ||||||
|  |  *  - Configurar y renderizar tablas con Bootstrap Table. | ||||||
|  |  *  - Definir columnas y formatos de manera estándar. | ||||||
|  |  *  - Manejar búsquedas, filtros, o catálogos necesarios. | ||||||
|  |  *  - Centralizar la lógica de montaje (mount). | ||||||
|  |  * | ||||||
|  |  * @package Koneko\VuexyAdmin\Livewire\Table | ||||||
|  |  */ | ||||||
|  | abstract class AbstractIndexComponent extends Component | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Configuración principal para la tabla con Bootstrap Table. | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     public $bt_datatable = []; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tag identificador del componente, derivado del modelo. | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $tagName; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Nombre singular del modelo (para mensajes, etiquetado, etc.). | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $singularName; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Identificador único del formulario (vinculado al Offcanvas o Modal). | ||||||
|  |      * | ||||||
|  |      * @var string | ||||||
|  |      */ | ||||||
|  |     public $formId; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Método para obtener la instancia del modelo asociado. | ||||||
|  |      * | ||||||
|  |      * Debe retornarse una instancia (o la clase) del modelo Eloquent que maneja este Index. | ||||||
|  |      * | ||||||
|  |      * @return Model|string | ||||||
|  |      */ | ||||||
|  |     abstract protected function model(): string; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Define las columnas (header) de la tabla. Este array se fusionará | ||||||
|  |      * o se inyectará en la configuración principal $bt_datatable. | ||||||
|  |      * | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     abstract protected function columns(): array; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Define el formato (formatter) de las columnas. | ||||||
|  |      * | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     abstract protected function format(): array; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Retorna la ruta de la vista Blade que renderizará el componente. | ||||||
|  |      * | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     abstract protected function viewPath(): string; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Método que define la configuración base del DataTable. | ||||||
|  |      * Aquí puedes poner ajustes comunes (exportFileName, paginación, etc.). | ||||||
|  |      * | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     protected function bootstraptableConfig(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'sortName'            => '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; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								Livewire/Users/UserCount.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								Livewire/Users/UserCount.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Users; | ||||||
|  |  | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  |  | ||||||
|  | class UserCount extends Component | ||||||
|  | { | ||||||
|  |     public $total, $enabled, $disabled; | ||||||
|  |  | ||||||
|  |     protected $listeners = ['refreshUserCount' => '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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										306
									
								
								Livewire/Users/UserForm.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								Livewire/Users/UserForm.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,306 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Users; | ||||||
|  |  | ||||||
|  | use Illuminate\Validation\Rule; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Koneko\VuexyAdmin\Livewire\Form\AbstractFormComponent; | ||||||
|  | use Koneko\SatCatalogs\Models\{Colonia, Estado, Localidad, Municipio, Pais, RegimenFiscal}; | ||||||
|  | use Koneko\VuexyAdmin\Models\Store; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class UserForm | ||||||
|  |  * | ||||||
|  |  * Componente Livewire para manejar el formulario CRUD de sucursales en el sistema ERP. | ||||||
|  |  * Implementa la creación, edición y eliminación de sucursales con validaciones dinámicas. | ||||||
|  |  */ | ||||||
|  | class UserForm extends AbstractFormComponent | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Campos específicos del formulario. | ||||||
|  |      */ | ||||||
|  |     public $code, $name, $description, $manager_id, $rfc, $nombre_fiscal, $c_regimen_fiscal, | ||||||
|  |         $domicilio_fiscal, $serie_ingresos, $serie_egresos, $serie_pagos, $c_codigo_postal, | ||||||
|  |         $c_pais, $c_estado, $c_localidad, $c_municipio, $c_colonia, $direccion, $num_ext, | ||||||
|  |         $num_int, $email, $tel, $tel2, $lat, $lng, $show_on_website, $enable_ecommerce, $status; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         public $confirmDeletion = false; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Listas de opciones para selects en el formulario. | ||||||
|  |      */ | ||||||
|  |     public $manager_id_options = [], | ||||||
|  |         $c_regimen_fiscal_options = [], | ||||||
|  |         $c_pais_options = [], | ||||||
|  |         $c_estado_options = [], | ||||||
|  |         $c_localidad_options = [], | ||||||
|  |         $c_municipio_options = [], | ||||||
|  |         $c_colonia_options = []; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Montar el formulario e inicializar datos específicos. | ||||||
|  |      * | ||||||
|  |      * @param string $mode Modo del formulario: create, edit, delete. | ||||||
|  |      * @param Store|null $store El modelo Store si está en modo edición o eliminación. | ||||||
|  |      */ | ||||||
|  |     public function mount(string $mode = 'create', mixed $store = null): void | ||||||
|  |     { | ||||||
|  |         parent::mount($mode, $store->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<string, string> | ||||||
|  |      */ | ||||||
|  |     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<string, string> | ||||||
|  |      */ | ||||||
|  |     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'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										115
									
								
								Livewire/Users/UserIndex.copy.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								Livewire/Users/UserIndex.copy.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Users; | ||||||
|  |  | ||||||
|  | use Spatie\Permission\Models\Role; | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Storage; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  |  | ||||||
|  | use Livewire\Component; | ||||||
|  |  | ||||||
|  | class UserIndex extends Component | ||||||
|  | { | ||||||
|  |     public $statuses; | ||||||
|  |     public $status_options; | ||||||
|  |  | ||||||
|  |     public $rows_roles = []; | ||||||
|  |     public $roles_options = []; | ||||||
|  |     public $roles_html_select; | ||||||
|  |  | ||||||
|  |     public $total, $enabled, $disabled; | ||||||
|  |  | ||||||
|  |     public $indexAlert; | ||||||
|  |  | ||||||
|  |     public $userId, $name, $email, $password, $roles, $status, $photo, $src_photo; | ||||||
|  |     public $modalTitle; | ||||||
|  |     public $btnSubmitTxt; | ||||||
|  |  | ||||||
|  |     public function mount() | ||||||
|  |     { | ||||||
|  |         $this->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 = "<select id=\"UserRole\" class=\"form-select text-capitalize\"><option value=\"\"> Selecciona un rol </option>"; | ||||||
|  |  | ||||||
|  |         foreach ($roles as $role) { | ||||||
|  |             $this->rows_roles[$role->name] = "<span class=\"badge bg-label-" . $role->style . " mx-1\">" . $role->name . "</span>"; | ||||||
|  |  | ||||||
|  |             if (Auth::user()->hasRole('SuperAdmin') || $role->name != 'SuperAdmin') { | ||||||
|  |                 $this->roles_html_select .= "<option value=\"" . $role->name . "\" class=\"text-capitalize\">" . $role->name . "</option>"; | ||||||
|  |                 $this->roles_options[$role->name] = $role->name; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $this->roles_html_select .= "</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 = '<div class="alert alert-warning alert-dismissible" role="alert">Se eliminó correctamente el usuario.<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>'; | ||||||
|  |  | ||||||
|  |             $this->dispatch('refreshUserCount'); | ||||||
|  |             $this->dispatch('afterDelete'); | ||||||
|  |         } else { | ||||||
|  |             $this->indexAlert = '<div class="alert alert-danger alert-dismissible" role="alert">Usuario no encontrado.<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>'; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public function render() | ||||||
|  |     { | ||||||
|  |         return view('vuexy-admin::livewire.users.index', [ | ||||||
|  |             'users' => User::paginate(10), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										299
									
								
								Livewire/Users/UserIndex.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								Livewire/Users/UserIndex.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,299 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Users; | ||||||
|  |  | ||||||
|  | use Livewire\WithFileUploads; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent; | ||||||
|  |  | ||||||
|  | class UserIndex extends AbstractIndexComponent | ||||||
|  | { | ||||||
|  |     use WithFileUploads; | ||||||
|  |  | ||||||
|  |     public $doc_file; | ||||||
|  |     public $dropzoneVisible = true; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Almacena rutas útiles para la funcionalidad de edición o eliminación. | ||||||
|  |      */ | ||||||
|  |     public $routes = []; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Método que define la clase o instancia del modelo a usar en este Index. | ||||||
|  |      */ | ||||||
|  |     protected function model(): string | ||||||
|  |     { | ||||||
|  |         return User::class; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Retorna las columnas (header) de la tabla. | ||||||
|  |      */ | ||||||
|  |     protected function columns(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'action'          => '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'; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										295
									
								
								Livewire/Users/UserOffCanvasForm.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								Livewire/Users/UserOffCanvasForm.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,295 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Users; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Validation\Rule; | ||||||
|  | use Illuminate\Http\UploadedFile; | ||||||
|  | use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Koneko\VuexyContacts\Services\{ContactCatalogService,ConstanciaFiscalService,FacturaXmlService}; | ||||||
|  | use Koneko\VuexyStoreManager\Services\StoreCatalogService; | ||||||
|  | use Livewire\WithFileUploads; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class UserOffCanvasForm | ||||||
|  |  * | ||||||
|  |  * Componente Livewire para gestionar almacenes. | ||||||
|  |  * Extiende la clase AbstractFormOffCanvasComponent e implementa validaciones dinámicas, | ||||||
|  |  * manejo de formularios, eventos y actualizaciones en tiempo real. | ||||||
|  |  * | ||||||
|  |  * @package Koneko\VuexyAdmin\Livewire\Users | ||||||
|  |  */ | ||||||
|  | class UserOffCanvasForm extends AbstractFormOffCanvasComponent | ||||||
|  | { | ||||||
|  |     use WithFileUploads; | ||||||
|  |  | ||||||
|  |     public $doc_file; | ||||||
|  |     public $dropzoneVisible = true; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Propiedades del formulario relacionadas con el usuario. | ||||||
|  |      */ | ||||||
|  |     public $code, | ||||||
|  |         $parent_id, | ||||||
|  |         $name, | ||||||
|  |         $last_name, | ||||||
|  |         $email, | ||||||
|  |         $company, | ||||||
|  |         $rfc, | ||||||
|  |         $nombre_fiscal, | ||||||
|  |         $tipo_persona, | ||||||
|  |         $c_regimen_fiscal, | ||||||
|  |         $domicilio_fiscal, | ||||||
|  |         $is_partner, | ||||||
|  |         $is_employee, | ||||||
|  |         $is_prospect, | ||||||
|  |         $is_customer, | ||||||
|  |         $is_provider, | ||||||
|  |         $status; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Listas de opciones para selects en el formulario. | ||||||
|  |      */ | ||||||
|  |     public $store_options = [], | ||||||
|  |         $work_center_options = [], | ||||||
|  |         $manager_options = []; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Eventos de escucha de Livewire. | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     protected $listeners = [ | ||||||
|  |         'editUsers' => '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<string, mixed> | ||||||
|  |      */ | ||||||
|  |     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<string, string> | ||||||
|  |      */ | ||||||
|  |     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<string, string> | ||||||
|  |      */ | ||||||
|  |     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'; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										283
									
								
								Livewire/Users/UserShow.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								Livewire/Users/UserShow.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,283 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Livewire\Users; | ||||||
|  |  | ||||||
|  | use App\Models\User; | ||||||
|  | use App\Models\Catalog\DropdownList; | ||||||
|  | use Koneko\SatCatalogs\Models\UsoCfdi; | ||||||
|  | use Koneko\SatCatalogs\Models\RegimenFiscal; | ||||||
|  | use Intervention\Image\ImageManager; | ||||||
|  | use Livewire\WithFileUploads; | ||||||
|  | use Livewire\Component; | ||||||
|  |  | ||||||
|  | class UserShow extends Component | ||||||
|  | { | ||||||
|  |     use WithFileUploads; | ||||||
|  |  | ||||||
|  |     public $image; | ||||||
|  |  | ||||||
|  |     public User $user; | ||||||
|  |     public $status_options, $pricelists_options; | ||||||
|  |     public $regimen_fiscal_options, $uso_cfdi_options; | ||||||
|  |     public $userId, | ||||||
|  |         $name, | ||||||
|  |         $cargo, | ||||||
|  |         $profile_photo, | ||||||
|  |         $profile_photo_path, | ||||||
|  |         $email, | ||||||
|  |         $password, | ||||||
|  |         $password_confirmation, | ||||||
|  |         $tipo_persona, | ||||||
|  |         $rfc, | ||||||
|  |         $nombre_fiscal, | ||||||
|  |         $c_regimen_fiscal, | ||||||
|  |         $domicilio_fiscal, | ||||||
|  |         $c_uso_cfdi, | ||||||
|  |         $pricelist_id, | ||||||
|  |         $enable_credit, | ||||||
|  |         $credit_days, | ||||||
|  |         $credit_limit, | ||||||
|  |         $is_prospect, | ||||||
|  |         $is_customer, | ||||||
|  |         $is_provider, | ||||||
|  |         $is_user, | ||||||
|  |         $status; | ||||||
|  |     public $deleteUserImage; | ||||||
|  |     public $cuentaUsuarioAlert, | ||||||
|  |         $accesosAlert, | ||||||
|  |         $facturacionElectronicaAlert; | ||||||
|  |  | ||||||
|  |     // Reglas de validación para la cuenta de usuario | ||||||
|  |     protected $rulesUser = [ | ||||||
|  |         'tipo_persona' => '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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								Models/MediaItem.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Models/MediaItem.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Models; | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  |  | ||||||
|  | class MediaItem extends Model | ||||||
|  | { | ||||||
|  |     use HasFactory; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The attributes that are mass assignable. | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'url', | ||||||
|  |         'imageable_type', | ||||||
|  |         'imageable_id', | ||||||
|  |         'type', | ||||||
|  |         'sub_type', | ||||||
|  |         'url', | ||||||
|  |         'path', | ||||||
|  |         'title', | ||||||
|  |         'description', | ||||||
|  |         'order', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     // the list of types values that can be stored in table | ||||||
|  |     const TYPE_CARD        = 1; | ||||||
|  |     const TYPE_BANNER      = 2; | ||||||
|  |     const TYPE_COVER       = 3; | ||||||
|  |     const TYPE_GALLERY     = 4; | ||||||
|  |     const TYPE_BANNER_HOME = 5; | ||||||
|  |     const TYPE_CARD2       = 6; | ||||||
|  |     const TYPE_BANNER2     = 7; | ||||||
|  |     const TYPE_COVER2      = 8; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * List of names for each types. | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     public static $typesList = [ | ||||||
|  |         self::TYPE_CARD        => '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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								Models/Setting.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								Models/Setting.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Models; | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  |  | ||||||
|  | class Setting extends Model | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * The attributes that are mass assignable. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     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'); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										377
									
								
								Models/User copy.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										377
									
								
								Models/User copy.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,377 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Models; | ||||||
|  |  | ||||||
|  | use Illuminate\Contracts\Auth\MustVerifyEmail; | ||||||
|  | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
|  | use Illuminate\Foundation\Auth\User as Authenticatable; | ||||||
|  | use Illuminate\Http\UploadedFile; | ||||||
|  | use Illuminate\Notifications\Notifiable; | ||||||
|  | use Illuminate\Support\Facades\Storage; | ||||||
|  | use Intervention\Image\ImageManager; | ||||||
|  | use Intervention\Image\Typography\FontFactory; | ||||||
|  | use Laravel\Fortify\TwoFactorAuthenticatable; | ||||||
|  | use Laravel\Sanctum\HasApiTokens; | ||||||
|  | use OwenIt\Auditing\Contracts\Auditable as AuditableContract; | ||||||
|  | use OwenIt\Auditing\Auditable; | ||||||
|  | use Spatie\Permission\Traits\HasRoles; | ||||||
|  | use Koneko\VuexyAdmin\Notifications\CustomResetPasswordNotification; | ||||||
|  |  | ||||||
|  | if (trait_exists(\Koneko\VuexyContacts\Traits\HasContactsAttributes::class)) { | ||||||
|  |     trait DynamicContactsAttributes { | ||||||
|  |         use \Koneko\VuexyContacts\Traits\HasContactsAttributes; | ||||||
|  |     } | ||||||
|  | } else { | ||||||
|  |     trait DynamicContactsAttributes {} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class User extends Authenticatable implements MustVerifyEmail, AuditableContract | ||||||
|  | { | ||||||
|  |     use HasRoles, | ||||||
|  |         HasApiTokens, | ||||||
|  |         HasFactory, | ||||||
|  |         Notifiable, | ||||||
|  |         TwoFactorAuthenticatable, | ||||||
|  |         Auditable, | ||||||
|  |         DynamicContactsAttributes; | ||||||
|  |  | ||||||
|  |     // the list of status values that can be stored in table | ||||||
|  |     const STATUS_ENABLED  = 10; | ||||||
|  |     const STATUS_DISABLED = 1; | ||||||
|  |     const STATUS_REMOVED  = 0; | ||||||
|  |  | ||||||
|  |     const AVATAR_DISK        = 'public'; | ||||||
|  |     const PROFILE_PHOTO_DIR  = 'profile-photos'; | ||||||
|  |     const INITIAL_AVATAR_DIR = 'initial-avatars'; | ||||||
|  |     const INITIAL_MAX_LENGTH = 4; | ||||||
|  |  | ||||||
|  |     const AVATAR_WIDTH  = 512; | ||||||
|  |     const AVATAR_HEIGHT = 512; | ||||||
|  |     const AVATAR_BACKGROUND = '#EBF4FF'; // Fondo por defecto | ||||||
|  |     const AVATAR_COLORS = [ | ||||||
|  |         '#7367f0', | ||||||
|  |         '#808390', | ||||||
|  |         '#28c76f', | ||||||
|  |         '#ff4c51', | ||||||
|  |         '#ff9f43', | ||||||
|  |         '#00bad1', | ||||||
|  |         '#4b4b4b', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * List of names for each status. | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     public static $statusList = [ | ||||||
|  |         self::STATUS_ENABLED  => '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<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'name', | ||||||
|  |         'last_name', | ||||||
|  |         'email', | ||||||
|  |         'password', | ||||||
|  |         'profile_photo_path', | ||||||
|  |         'status', | ||||||
|  |         'created_by', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The attributes that should be hidden for serialization. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $hidden = [ | ||||||
|  |         'password', | ||||||
|  |         'remember_token', | ||||||
|  |         'two_factor_recovery_codes', | ||||||
|  |         'two_factor_secret', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The accessors to append to the model's array form. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $appends = [ | ||||||
|  |         'profile_photo_url', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Get the attributes that should be cast. | ||||||
|  |      * | ||||||
|  |      * @return array<string, string> | ||||||
|  |      */ | ||||||
|  |     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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										237
									
								
								Models/User.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								Models/User.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,237 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Models; | ||||||
|  |  | ||||||
|  | use Illuminate\Contracts\Auth\MustVerifyEmail; | ||||||
|  | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
|  | use Illuminate\Foundation\Auth\User as Authenticatable; | ||||||
|  | use Illuminate\Notifications\Notifiable; | ||||||
|  | use Laravel\Fortify\TwoFactorAuthenticatable; | ||||||
|  | use Laravel\Sanctum\HasApiTokens; | ||||||
|  | use OwenIt\Auditing\Contracts\Auditable as AuditableContract; | ||||||
|  | use OwenIt\Auditing\Auditable; | ||||||
|  | use Spatie\Permission\Traits\HasRoles; | ||||||
|  | use Koneko\VuexyAdmin\Notifications\CustomResetPasswordNotification; | ||||||
|  |  | ||||||
|  | class User extends Authenticatable implements MustVerifyEmail, AuditableContract | ||||||
|  | { | ||||||
|  |     use HasRoles, HasApiTokens, HasFactory, Notifiable, | ||||||
|  |         TwoFactorAuthenticatable, Auditable; | ||||||
|  |  | ||||||
|  |     // the list of status values that can be stored in table | ||||||
|  |     const STATUS_ENABLED  = 10; | ||||||
|  |     const STATUS_DISABLED = 1; | ||||||
|  |     const STATUS_REMOVED  = 0; | ||||||
|  |  | ||||||
|  |     const AVATAR_DISK        = 'public'; | ||||||
|  |     const PROFILE_PHOTO_DIR  = 'profile-photos'; | ||||||
|  |     const INITIAL_AVATAR_DIR = 'initial-avatars'; | ||||||
|  |     const INITIAL_MAX_LENGTH = 4; | ||||||
|  |  | ||||||
|  |     const AVATAR_WIDTH  = 512; | ||||||
|  |     const AVATAR_HEIGHT = 512; | ||||||
|  |     const AVATAR_BACKGROUND = '#EBF4FF'; // Fondo por defecto | ||||||
|  |     const AVATAR_COLORS = [ | ||||||
|  |         '#7367f0', | ||||||
|  |         '#808390', | ||||||
|  |         '#28c76f', | ||||||
|  |         '#ff4c51', | ||||||
|  |         '#ff9f43', | ||||||
|  |         '#00bad1', | ||||||
|  |         '#4b4b4b', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * List of names for each status. | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     public static $statusList = [ | ||||||
|  |         self::STATUS_ENABLED  => '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<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'name', | ||||||
|  |         'last_name', | ||||||
|  |         'email', | ||||||
|  |         'password', | ||||||
|  |         'profile_photo_path', | ||||||
|  |         'status', | ||||||
|  |         'created_by', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The attributes that should be hidden for serialization. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     protected $hidden = [ | ||||||
|  |         'password', | ||||||
|  |         'remember_token', | ||||||
|  |         'two_factor_recovery_codes', | ||||||
|  |         'two_factor_secret', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The accessors to append to the model's array form. | ||||||
|  |      * | ||||||
|  |      * @var array<int, string> | ||||||
|  |      */ | ||||||
|  |     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<string, string> | ||||||
|  |      */ | ||||||
|  |     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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								Models/UserLogin.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Models/UserLogin.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Models; | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  |  | ||||||
|  | class UserLogin extends Model | ||||||
|  | { | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'user_id', | ||||||
|  |         'ip_address', | ||||||
|  |         'user_agent' | ||||||
|  |     ]; | ||||||
|  | } | ||||||
							
								
								
									
										117
									
								
								Notifications/CustomResetPasswordNotification.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								Notifications/CustomResetPasswordNotification.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,117 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Notifications; | ||||||
|  |  | ||||||
|  | use Exception; | ||||||
|  | use Illuminate\Bus\Queueable; | ||||||
|  | use Illuminate\Notifications\Notification; | ||||||
|  | use Illuminate\Notifications\Messages\MailMessage; | ||||||
|  | use Illuminate\Support\Facades\Config; | ||||||
|  | use Illuminate\Support\Facades\Crypt; | ||||||
|  | use Illuminate\Support\Facades\Log; | ||||||
|  | use Koneko\VuexyAdmin\Models\Setting; | ||||||
|  |  | ||||||
|  | class CustomResetPasswordNotification extends Notification | ||||||
|  | { | ||||||
|  |     use Queueable; | ||||||
|  |  | ||||||
|  |     public $token; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Crea una nueva instancia de notificación. | ||||||
|  |      */ | ||||||
|  |     public function __construct($token) | ||||||
|  |     { | ||||||
|  |         $this->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.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								Providers/ConfigServiceProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								Providers/ConfigServiceProvider.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Providers; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\ServiceProvider; | ||||||
|  | use Koneko\VuexyAdmin\Services\GlobalSettingsService; | ||||||
|  |  | ||||||
|  | class ConfigServiceProvider extends ServiceProvider | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Register services. | ||||||
|  |      */ | ||||||
|  |     public function register() | ||||||
|  |     { | ||||||
|  |         // Cargar configuración del sistema | ||||||
|  |         $this->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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										124
									
								
								Providers/FortifyServiceProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								Providers/FortifyServiceProvider.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Providers; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\ServiceProvider; | ||||||
|  | use Illuminate\Cache\RateLimiting\Limit; | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\Support\Facades\Config; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
|  | use Illuminate\Support\Facades\RateLimiter; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Laravel\Fortify\Fortify; | ||||||
|  | use Koneko\VuexyAdmin\Actions\Fortify\CreateNewUser; | ||||||
|  | use Koneko\VuexyAdmin\Actions\Fortify\ResetUserPassword; | ||||||
|  | use Koneko\VuexyAdmin\Actions\Fortify\UpdateUserPassword; | ||||||
|  | use Koneko\VuexyAdmin\Actions\Fortify\UpdateUserProfileInformation; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Koneko\VuexyAdmin\Services\AdminTemplateService; | ||||||
|  |  | ||||||
|  | class FortifyServiceProvider extends ServiceProvider | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Register any application services. | ||||||
|  |      */ | ||||||
|  |     public function register(): void | ||||||
|  |     { | ||||||
|  |         // | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Bootstrap any application services. | ||||||
|  |      */ | ||||||
|  |     public function boot(): void | ||||||
|  |     { | ||||||
|  |         Fortify::createUsersUsing(CreateNewUser::class); | ||||||
|  |         Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class); | ||||||
|  |         Fortify::updateUserPasswordsUsing(UpdateUserPassword::class); | ||||||
|  |         Fortify::resetUserPasswordsUsing(ResetUserPassword::class); | ||||||
|  |  | ||||||
|  |         RateLimiter::for('login', function (Request $request) { | ||||||
|  |             $throttleKey = Str::transliterate(Str::lower($request->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]); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								Providers/VuexyAdminServiceProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								Providers/VuexyAdminServiceProvider.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,132 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Providers; | ||||||
|  |  | ||||||
|  | use Koneko\VuexyAdmin\Http\Middleware\AdminTemplateMiddleware; | ||||||
|  | use Koneko\VuexyAdmin\Listeners\{ClearUserCache,HandleUserLogin}; | ||||||
|  | use Koneko\VuexyAdmin\Livewire\Users\{UserIndex,UserShow,UserForm,UserOffCanvasForm}; | ||||||
|  | use Koneko\VuexyAdmin\Livewire\Roles\RoleIndex; | ||||||
|  | use Koneko\VuexyAdmin\Livewire\Permissions\PermissionIndex; | ||||||
|  | use Koneko\VuexyAdmin\Livewire\Cache\{CacheFunctions,CacheStats,SessionStats,MemcachedStats,RedisStats}; | ||||||
|  | use Koneko\VuexyAdmin\Livewire\AdminSettings\{ApplicationSettings,GeneralSettings,InterfaceSettings,MailSmtpSettings,MailSenderResponseSettings}; | ||||||
|  | use Koneko\VuexyAdmin\Console\Commands\CleanInitialAvatars; | ||||||
|  | use Koneko\VuexyAdmin\Helpers\VuexyHelper; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Illuminate\Support\Facades\{URL,Event,Blade}; | ||||||
|  | use Illuminate\Support\ServiceProvider; | ||||||
|  | use Illuminate\Foundation\AliasLoader; | ||||||
|  | use Illuminate\Auth\Events\{Login,Logout}; | ||||||
|  | use Livewire\Livewire; | ||||||
|  | use OwenIt\Auditing\AuditableObserver; | ||||||
|  | use Spatie\Permission\PermissionServiceProvider; | ||||||
|  |  | ||||||
|  | class VuexyAdminServiceProvider extends ServiceProvider | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Register any application services. | ||||||
|  |      */ | ||||||
|  |     public function register(): void | ||||||
|  |     { | ||||||
|  |         // Cargar configuraciones personalizadas | ||||||
|  |         $this->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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										104
									
								
								Queries/BootstrapTableQueryBuilder.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								Queries/BootstrapTableQueryBuilder.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Queries; | ||||||
|  |  | ||||||
|  | use Illuminate\Http\Request; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  |  | ||||||
|  | abstract class BootstrapTableQueryBuilder | ||||||
|  | { | ||||||
|  |     protected $query; | ||||||
|  |     protected $request; | ||||||
|  |     protected $config; | ||||||
|  |  | ||||||
|  |     public function __construct(Request $request, array $config) | ||||||
|  |     { | ||||||
|  |         $this->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, | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								Queries/GenericQueryBuilder.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Queries/GenericQueryBuilder.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Queries; | ||||||
|  |  | ||||||
|  | class GenericQueryBuilder extends BootstrapTableQueryBuilder | ||||||
|  | { | ||||||
|  |     // Custom query builder | ||||||
|  | } | ||||||
							
								
								
									
										223
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										223
									
								
								README.md
									
									
									
									
									
								
							| @ -1,185 +1,130 @@ | |||||||
|  | # 🎨 Laravel Vuexy Admin | ||||||
|  |  | ||||||
| <p align="center"> | <p align="center"> | ||||||
|     <a href="https://koneko.mx" target="_blank"> |     <a href="https://koneko.mx" target="_blank"> <img src="https://git.koneko.mx/Koneko-ST/koneko-st/raw/branch/main/logo-images/horizontal-05.png" width="400" alt="Koneko Soluciones Tecnológicas Logo"> </a>  | ||||||
|         <img src="https://git.koneko.mx/Koneko-ST/koneko-st/raw/branch/main/logo-images/horizontal-05.png" width="400" alt="Koneko Soluciones Tecnológicas Logo"> |  | ||||||
|     </a> |  | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| <p align="center"> | <p align="center"> | ||||||
|  |     <a href="https://koneko.mx"><img src="https://img.shields.io/badge/Website-koneko.mx-blue" alt="Sitio Web"></a>  | ||||||
|     <a href="https://packagist.org/packages/koneko/laravel-vuexy-admin"><img src="https://img.shields.io/packagist/v/koneko/laravel-vuexy-admin" alt="Latest Stable Version"></a> |     <a href="https://packagist.org/packages/koneko/laravel-vuexy-admin"><img src="https://img.shields.io/packagist/v/koneko/laravel-vuexy-admin" alt="Latest Stable Version"></a> | ||||||
|     <a href="https://packagist.org/packages/koneko/laravel-vuexy-admin"><img src="https://img.shields.io/packagist/l/koneko/laravel-vuexy-admin" alt="License"></a> |     <a href="https://packagist.org/packages/koneko/laravel-vuexy-admin"><img src="https://img.shields.io/packagist/l/koneko/laravel-vuexy-admin" alt="License"></a> | ||||||
|     <a href="mailto:contacto@koneko.mx"><img src="https://img.shields.io/badge/contact-email-green" alt="Email"></a> |     <a href="https://git.koneko.mx/koneko"><img src="https://img.shields.io/badge/Git%20Server-Koneko%20Git-orange" alt="Servidor Git"></a>  | ||||||
|  |     <a href="https://github.com/koneko-mx/laravel-vuexy-admin/actions/workflows/tests.yml"><img src="https://github.com/koneko-mx/laravel-vuexy-admin/actions/workflows/tests.yml/badge.svg" alt="Build Status"></a>  | ||||||
|  |     <a href="https://github.com/koneko-mx/laravel-vuexy-admin/issues"><img src="https://img.shields.io/github/issues/koneko/laravel-vuexy-admin" alt="Issues"></a>  | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| # 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 | ### ✨ Características | ||||||
|  | - 🔹 Sistema de autenticación con Laravel Fortify. | ||||||
| - **Optimización para México**: | - 🔹 Gestión avanzada de usuarios con Livewire. | ||||||
|   - Uso de los catálogos oficiales del SAT (versión CFDI 4.0): | - 🔹 Control de roles y permisos con Spatie Permissions. | ||||||
|     - Banco (`sat_banco`) | - 🔹 Auditoría de acciones con Laravel Auditing. | ||||||
|     - Clave de Producto o Servicio (`sat_clave_prod_serv`) | - 🔹 Publicación de configuraciones y vistas. | ||||||
|     - Clave de Unidad (`sat_clave_unidad`) | - 🔹 Soporte para cache y optimización de rendimiento. | ||||||
|     - 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. |  | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| ## Instalación | ## 📦 Instalación | ||||||
|  |  | ||||||
| Este proyecto ofrece dos métodos de instalación: mediante Composer o manualmente. A continuación, te explicamos ambos procesos. | Instalar vía **Composer**: | ||||||
|  |  | ||||||
| ### Opción 1: Usar Composer (Recomendado) |  | ||||||
|  |  | ||||||
| Para instalar el proyecto rápidamente usando Composer, ejecuta el siguiente comando: |  | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| composer create-project koneko/laravel-vuexy-admin | composer require koneko/laravel-vuexy-admin | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Este comando realizará automáticamente los siguientes pasos: | Publicar archivos de configuración y migraciones: | ||||||
| 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: |  | ||||||
|  |  | ||||||
| ```bash | ```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 | php artisan migrate --seed | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Finalmente, compila los activos iniciales: | Para publicar imágenes del tema: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| npm install | php artisan vendor:publish --tag=vuexy-admin-images | ||||||
| npm run dev |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| Inicia el servidor local con: |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| php artisan serve |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| ### 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: | ### 🔄 Sincronización con GitHub | ||||||
|    ```bash | - **Repositorio Principal:** [git.koneko.mx](https://git.koneko.mx/koneko/laravel-vuexy-admin) | ||||||
|    git clone https://git.koneko.mx/Koneko-ST/laravel-vuexy-admin.git | - **Repositorio en GitHub:** [github.com/koneko-mx/laravel-vuexy-admin](https://github.com/koneko-mx/laravel-vuexy-admin) | ||||||
|    cd laravel-vuexy-admin | - **Los cambios pueden reflejarse primero en Tea antes de GitHub.** | ||||||
|    ``` |  | ||||||
|  |  | ||||||
| 2. Instala las dependencias de Composer: | ### 🤝 Contribuciones | ||||||
|    ```bash | Si deseas contribuir: | ||||||
|    composer install | 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: | ⚠️ **Nota:** Algunos cambios pueden tardar en reflejarse en GitHub, ya que este repositorio se actualiza automáticamente desde Tea. | ||||||
|    ```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 |  | ||||||
|    ``` |  | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| ## Notas importantes | ## 🏅 Licencia | ||||||
|  |  | ||||||
| - Asegúrate de tener instalado: | Este paquete es de código abierto bajo la licencia [MIT](LICENSE). | ||||||
|   - **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). |  | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| ## 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. |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| <p align="center"> | <p align="center"> | ||||||
|     Hecho con ❤️ por <a href="https://koneko.mx">Koneko Soluciones Tecnológicas</a> |     Hecho con ❤️ por <a href="https://koneko.mx">Koneko Soluciones Tecnológicas</a> | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								Rules/NotEmptyHtml.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Rules/NotEmptyHtml.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Rules; | ||||||
|  |  | ||||||
|  | use Closure; | ||||||
|  | use Illuminate\Contracts\Validation\ValidationRule; | ||||||
|  |  | ||||||
|  | class NotEmptyHtml implements ValidationRule | ||||||
|  | { | ||||||
|  |     public function validate(string $attribute, mixed $value, Closure $fail): void | ||||||
|  |     { | ||||||
|  |         // Eliminar etiquetas HTML y espacios en blanco | ||||||
|  |         $strippedContent = trim(strip_tags($value)); | ||||||
|  |  | ||||||
|  |         // Considerar vacío si no queda contenido significativo | ||||||
|  |         if (empty($strippedContent)) { | ||||||
|  |             $fail('El contenido no puede estar vacío.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										215
									
								
								Services/AdminSettingsService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								Services/AdminSettingsService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,215 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Storage; | ||||||
|  | use Intervention\Image\ImageManager; | ||||||
|  | use Koneko\VuexyAdmin\Models\Setting; | ||||||
|  |  | ||||||
|  | class AdminSettingsService | ||||||
|  | { | ||||||
|  |     private $driver; | ||||||
|  |     private $imageDisk           = 'public'; | ||||||
|  |     private $favicon_basePath    = 'favicon/'; | ||||||
|  |     private $image_logo_basePath = 'images/logo/'; | ||||||
|  |  | ||||||
|  |     private $faviconsSizes = [ | ||||||
|  |         '180x180' => [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(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										156
									
								
								Services/AdminTemplateService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								Services/AdminTemplateService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,156 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Cache; | ||||||
|  | use Illuminate\Support\Facades\Config; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  | use Koneko\VuexyAdmin\Models\Setting; | ||||||
|  |  | ||||||
|  | class AdminTemplateService | ||||||
|  | { | ||||||
|  |     protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos | ||||||
|  |  | ||||||
|  |     public function updateSetting(string $key, string $value): bool | ||||||
|  |     { | ||||||
|  |         $setting = Setting::updateOrCreate( | ||||||
|  |             ['key' => $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"); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								Services/AvatarImageService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								Services/AvatarImageService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Http\UploadedFile; | ||||||
|  | use Illuminate\Support\Facades\Storage; | ||||||
|  | use Intervention\Image\ImageManager; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  |  | ||||||
|  | class AvatarImageService | ||||||
|  | { | ||||||
|  |     protected $avatarDisk = 'public'; | ||||||
|  |     protected $profilePhotoDir = 'profile-photos'; | ||||||
|  |     protected $avatarWidth = 512; | ||||||
|  |     protected $avatarHeight = 512; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Actualiza la foto de perfil procesando la imagen subida. | ||||||
|  |      * | ||||||
|  |      * @param mixed $user Objeto usuario que se va a actualizar. | ||||||
|  |      * @param UploadedFile $image_avatar Archivo de imagen subido. | ||||||
|  |      * | ||||||
|  |      * @throws \Exception Si el archivo no existe o tiene un formato inválido. | ||||||
|  |      * | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public function updateProfilePhoto(User $user, UploadedFile $image_avatar) | ||||||
|  |     { | ||||||
|  |         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.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										124
									
								
								Services/AvatarInitialsService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								Services/AvatarInitialsService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Storage; | ||||||
|  | use Intervention\Image\ImageManager; | ||||||
|  | use Intervention\Image\Typography\FontFactory; | ||||||
|  |  | ||||||
|  | class AvatarInitialsService | ||||||
|  | { | ||||||
|  |     protected $avatarDisk = 'public'; | ||||||
|  |     protected $initialAvatarDir = 'initial-avatars'; | ||||||
|  |     protected $avatarWidth = 512; | ||||||
|  |     protected $avatarHeight = 512; | ||||||
|  |     protected const INITIAL_MAX_LENGTH = 3; | ||||||
|  |     protected const AVATAR_BACKGROUND = '#EBF4FF'; | ||||||
|  |     protected const AVATAR_COLORS = [ | ||||||
|  |         '#7367f0', | ||||||
|  |         '#808390', | ||||||
|  |         '#28c76f', | ||||||
|  |         '#ff4c51', | ||||||
|  |         '#ff9f43', | ||||||
|  |         '#00bad1', | ||||||
|  |         '#4b4b4b', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Genera o retorna el avatar basado en las iniciales. | ||||||
|  |      * | ||||||
|  |      * @param string $name Nombre completo del usuario. | ||||||
|  |      * | ||||||
|  |      * @return \Illuminate\Http\Response Respuesta con la imagen generada. | ||||||
|  |      */ | ||||||
|  |     public function getAvatarImage($name) | ||||||
|  |     { | ||||||
|  |         $color       = $this->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)]; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										235
									
								
								Services/CacheConfigService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								Services/CacheConfigService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,235 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Config; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Support\Facades\Redis; | ||||||
|  |  | ||||||
|  | class CacheConfigService | ||||||
|  | { | ||||||
|  |     public function getConfig(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'cache' => $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'), | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										389
									
								
								Services/CacheManagerService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										389
									
								
								Services/CacheManagerService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,389 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Cache; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Support\Facades\Redis; | ||||||
|  | use Illuminate\Support\Facades\File; | ||||||
|  |  | ||||||
|  | class CacheManagerService | ||||||
|  | { | ||||||
|  |     private string $driver; | ||||||
|  |  | ||||||
|  |     public function __construct(string $driver = null) | ||||||
|  |     { | ||||||
|  |         $this->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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										225
									
								
								Services/GlobalSettingsService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								Services/GlobalSettingsService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,225 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Arr; | ||||||
|  | use Illuminate\Support\Facades\Cache; | ||||||
|  | use Illuminate\Support\Facades\Config; | ||||||
|  | use Illuminate\Support\Facades\Crypt; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  | use Koneko\VuexyAdmin\Models\Setting; | ||||||
|  |  | ||||||
|  | class GlobalSettingsService | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Tiempo de vida del caché en minutos (30 días). | ||||||
|  |      */ | ||||||
|  |     private $cacheTTL = 60 * 24 * 30; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Actualiza o crea una configuración. | ||||||
|  |      */ | ||||||
|  |     public function updateSetting(string $key, string $value): bool | ||||||
|  |     { | ||||||
|  |         $setting = Setting::updateOrCreate( | ||||||
|  |             ['key' => $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'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										28
									
								
								Services/RBACService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Services/RBACService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Spatie\Permission\Models\Role; | ||||||
|  | use Spatie\Permission\Models\Permission; | ||||||
|  | use Illuminate\Support\Facades\File; | ||||||
|  |  | ||||||
|  | class RBACService | ||||||
|  | { | ||||||
|  |     public static function loadRolesAndPermissions() | ||||||
|  |     { | ||||||
|  |         $filePath = database_path('data/rbac-config.json'); | ||||||
|  |         if (!File::exists($filePath)) { | ||||||
|  |             throw new \Exception("Archivo de configuración RBAC no encontrado."); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $rbacData = json_decode(File::get($filePath), true); | ||||||
|  |         foreach ($rbacData['permissions'] as $perm) { | ||||||
|  |             Permission::updateOrCreate(['name' => $perm]); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         foreach ($rbacData['roles'] as $name => $role) { | ||||||
|  |             $roleInstance = Role::updateOrCreate(['name' => $name, 'style' => $role['style']]); | ||||||
|  |             $roleInstance->syncPermissions($role['permissions']); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										153
									
								
								Services/SessionManagerService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								Services/SessionManagerService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,153 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Cache; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Support\Facades\Redis; | ||||||
|  |  | ||||||
|  | class SessionManagerService | ||||||
|  | { | ||||||
|  |     private string $driver; | ||||||
|  |  | ||||||
|  |     public function __construct(string $driver = null) | ||||||
|  |     { | ||||||
|  |         $this->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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										623
									
								
								Services/VuexyAdminService.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										623
									
								
								Services/VuexyAdminService.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,623 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Services; | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Auth; | ||||||
|  | use Illuminate\Support\Facades\Cache; | ||||||
|  | use Illuminate\Support\Facades\Route; | ||||||
|  | use Illuminate\Support\Facades\Gate; | ||||||
|  | use Koneko\VuexyAdmin\Models\Setting; | ||||||
|  |  | ||||||
|  | class VuexyAdminService | ||||||
|  | { | ||||||
|  |     private $vuexySearch; | ||||||
|  |     private $quicklinksRouteNames = []; | ||||||
|  |  | ||||||
|  |     protected $cacheTTL = 60 * 24 * 30; // 30 días en minutos | ||||||
|  |  | ||||||
|  |     private $homeRoute = [ | ||||||
|  |         'name' => '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 "<li class='nav-item dropdown-notifications navbar-dropdown dropdown me-3 me-xl-2'> | ||||||
|  |                 <a class='nav-link btn btn-text-secondary btn-icon rounded-pill dropdown-toggle hide-arrow' href='javascript:void(0);' data-bs-toggle='dropdown' data-bs-auto-close='outside' aria-expanded='false'> | ||||||
|  |                     <span class='position-relative'> | ||||||
|  |                         <i class='ti ti-bell ti-md'></i> | ||||||
|  |                         <span class='badge rounded-pill bg-danger badge-dot badge-notifications border'></span> | ||||||
|  |                     </span> | ||||||
|  |                 </a> | ||||||
|  |                 <ul class='dropdown-menu dropdown-menu-end p-0'> | ||||||
|  |                     <li class='dropdown-menu-header border-bottom'> | ||||||
|  |                         <div class='dropdown-header d-flex align-items-center py-3'> | ||||||
|  |                             <h6 class='mb-0 me-auto'>Notification</h6> | ||||||
|  |                             <div class='d-flex align-items-center h6 mb-0'> | ||||||
|  |                                 <span class='badge bg-label-primary me-2'>8 New</span> | ||||||
|  |                                 <a href='javascript:void(0)' class='btn btn-text-secondary rounded-pill btn-icon dropdown-notifications-all' data-bs-toggle='tooltip' data-bs-placement='top' title='Mark all as read'><i class='ti ti-mail-opened text-heading'></i></a> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     </li> | ||||||
|  |                     <li class='dropdown-notifications-list scrollable-container'> | ||||||
|  |                         <ul class='list-group list-group-flush'> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <img src='' . asset('assets/admin/img/avatars/1.png') . '' alt class='rounded-circle'> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='small mb-1'>Congratulation Lettie 🎉</h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>Won the monthly best seller gold badge</small> | ||||||
|  |                                         <small class='text-muted'>1h ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <span class='avatar-initial rounded-circle bg-label-danger'>CF</span> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='mb-1 small'>Charles Franklin</h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>Accepted your connection</small> | ||||||
|  |                                         <small class='text-muted'>12hr ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <img src='' . asset('assets/admin/img/avatars/2.png') . '' alt class='rounded-circle'> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='mb-1 small'>New Message ✉️</h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>You have new message from Natalie</small> | ||||||
|  |                                         <small class='text-muted'>1h ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <span class='avatar-initial rounded-circle bg-label-success'><i class='ti ti-shopping-cart'></i></span> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='mb-1 small'>Whoo! You have new order 🛒 </h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>ACME Inc. made new order $1,154</small> | ||||||
|  |                                         <small class='text-muted'>1 day ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <img src='' . asset('assets/admin/img/avatars/9.png') . '' alt class='rounded-circle'> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='mb-1 small'>Application has been approved 🚀 </h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>Your ABC project application has been approved.</small> | ||||||
|  |                                         <small class='text-muted'>2 days ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <span class='avatar-initial rounded-circle bg-label-success'><i class='ti ti-chart-pie'></i></span> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='mb-1 small'>Monthly report is generated</h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>July monthly financial report is generated </small> | ||||||
|  |                                         <small class='text-muted'>3 days ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <img src='' . asset('assets/admin/img/avatars/5.png') . '' alt class='rounded-circle'> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='mb-1 small'>Send connection request</h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>Peter sent you connection request</small> | ||||||
|  |                                         <small class='text-muted'>4 days ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <img src='' . asset('assets/admin/img/avatars/6.png') . '' alt class='rounded-circle'> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='mb-1 small'>New message from Jane</h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>Your have new message from Jane</small> | ||||||
|  |                                         <small class='text-muted'>5 days ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                             <li class='list-group-item list-group-item-action dropdown-notifications-item marked-as-read'> | ||||||
|  |                                 <div class='d-flex'> | ||||||
|  |                                     <div class='flex-shrink-0 me-3'> | ||||||
|  |                                         <div class='avatar'> | ||||||
|  |                                             <span class='avatar-initial rounded-circle bg-label-warning'><i class='ti ti-alert-triangle'></i></span> | ||||||
|  |                                         </div> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-grow-1'> | ||||||
|  |                                         <h6 class='mb-1 small'>CPU is running high</h6> | ||||||
|  |                                         <small class='mb-1 d-block text-body'>CPU Utilization Percent is currently at 88.63%,</small> | ||||||
|  |                                         <small class='text-muted'>5 days ago</small> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class='flex-shrink-0 dropdown-notifications-actions'> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-read'><span class='badge badge-dot'></span></a> | ||||||
|  |                                         <a href='javascript:void(0)' class='dropdown-notifications-archive'><span class='ti ti-x'></span></a> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </li> | ||||||
|  |                         </ul> | ||||||
|  |                     </li> | ||||||
|  |                     <li class='border-top'> | ||||||
|  |                         <div class='d-grid p-4'> | ||||||
|  |                             <a class='btn btn-primary btn-sm d-flex' href='javascript:void(0);'> | ||||||
|  |                                 <small class='align-middle'>View all notifications</small> | ||||||
|  |                             </a> | ||||||
|  |                         </div> | ||||||
|  |                     </li> | ||||||
|  |                 </ul> | ||||||
|  |             </li>"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,40 +1,28 @@ | |||||||
| { | { | ||||||
|     "name": "koneko/laravel-vuexy-admin-module", |     "name": "koneko/laravel-vuexy-admin", | ||||||
|     "description": "Base modular para proyectos Laravel altamente personalizados.", |     "description": "Laravel Vuexy Admin, un modulo de administracion optimizado para México.", | ||||||
|  |     "keywords": ["laravel", "koneko", "framework", "vuexy", "admin", "mexico"], | ||||||
|     "type": "library", |     "type": "library", | ||||||
|     "license": "MIT", |     "license": "MIT", | ||||||
|     "require": { |     "require": { | ||||||
|         "php": "^8.2", |         "php": "^8.2", | ||||||
|         "intervention/image-laravel": "^1.3", |         "intervention/image-laravel": "^1.4", | ||||||
|  |         "laravel/framework": "^11.31", | ||||||
|         "laravel/fortify": "^1.25", |         "laravel/fortify": "^1.25", | ||||||
|         "laravel/framework": "^11.0", |  | ||||||
|         "laravel/sanctum": "^4.0", |         "laravel/sanctum": "^4.0", | ||||||
|         "laravel/tinker": "^2.9", |  | ||||||
|         "livewire/livewire": "^3.5", |         "livewire/livewire": "^3.5", | ||||||
|         "maatwebsite/excel": "^3.1", |  | ||||||
|         "owen-it/laravel-auditing": "^13.6", |         "owen-it/laravel-auditing": "^13.6", | ||||||
|         "spatie/laravel-permission": "^6.10", |         "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" |  | ||||||
|     }, |     }, | ||||||
|     "autoload": { |     "autoload": { | ||||||
|         "psr-4": { |         "psr-4": { | ||||||
|             "Koneko\\VuexyAdminModule\\": "src/" |             "Koneko\\VuexyAdmin\\": "" | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "extra": { |     "extra": { | ||||||
|         "laravel": { |         "laravel": { | ||||||
|             "providers": [ |             "providers": [ | ||||||
|                 "Koneko\\VuexyAdminModule\\BaseServiceProvider" |                 "Koneko\\VuexyAdmin\\Providers\\VuexyAdminServiceProvider" | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| @ -43,5 +31,11 @@ | |||||||
|             "name": "Arturo Corro Pacheco", |             "name": "Arturo Corro Pacheco", | ||||||
|             "email": "arturo@koneko.mx" |             "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 | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										159
									
								
								config/fortify.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								config/fortify.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,159 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Laravel\Fortify\Features; | ||||||
|  |  | ||||||
|  | return [ | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |     |-------------------------------------------------------------------------- | ||||||
|  |     | Fortify Guard | ||||||
|  |     |-------------------------------------------------------------------------- | ||||||
|  |     | | ||||||
|  |     | Here you may specify which authentication guard Fortify will use while | ||||||
|  |     | authenticating users. This value should correspond with one of your | ||||||
|  |     | guards that is already present in your "auth" configuration file. | ||||||
|  |     | | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |     'guard' => '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, | ||||||
|  |         ]), | ||||||
|  |     ], | ||||||
|  |  | ||||||
|  | ]; | ||||||
							
								
								
									
										42
									
								
								config/image.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								config/image.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | return [ | ||||||
|  |  | ||||||
|  |     /* | ||||||
|  |     |-------------------------------------------------------------------------- | ||||||
|  |     | Image Driver | ||||||
|  |     |-------------------------------------------------------------------------- | ||||||
|  |     | | ||||||
|  |     | Intervention Image supports “GD Library” and “Imagick” to process images | ||||||
|  |     | internally. Depending on your PHP setup, you can choose one of them. | ||||||
|  |     | | ||||||
|  |     | Included options: | ||||||
|  |     |   - \Intervention\Image\Drivers\Gd\Driver::class | ||||||
|  |     |   - \Intervention\Image\Drivers\Imagick\Driver::class | ||||||
|  |     | | ||||||
|  |     */ | ||||||
|  |  | ||||||
|  |     'driver' => \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', | ||||||
|  |     ] | ||||||
|  | ]; | ||||||
							
								
								
									
										14
									
								
								config/koneko.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								config/koneko.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | <?php | ||||||
|  | // Variables | ||||||
|  | return [ | ||||||
|  |     "appName" => "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", | ||||||
|  | ]; | ||||||
							
								
								
									
										36
									
								
								config/vuexy.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								config/vuexy.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | // Custom 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/). | ||||||
|  |  | ||||||
|  | return [ | ||||||
|  |     'custom' => [ | ||||||
|  |         '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 | ||||||
|  |     ], | ||||||
|  | ]; | ||||||
							
								
								
									
										848
									
								
								config/vuexy_menu.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										848
									
								
								config/vuexy_menu.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,848 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | return [ | ||||||
|  |     'Inicio' => [ | ||||||
|  |         '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', | ||||||
|  |             ], | ||||||
|  |         ] | ||||||
|  |     ], | ||||||
|  | ]; | ||||||
							
								
								
									
										510
									
								
								database/data/rbac-config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										510
									
								
								database/data/rbac-config.json
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||||
|  |     ] | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								database/data/users.csv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								database/data/users.csv
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
| 
 | 
							
								
								
									
										49
									
								
								database/factories/UserFactory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								database/factories/UserFactory.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Koneko\VuexyAdmin\Database\factories; | ||||||
|  |  | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Illuminate\Support\Facades\Hash; | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | use Illuminate\Database\Eloquent\Factories\Factory; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @extends \Illuminate\Database\Eloquent\Factories\Factory<\Koneko\VuexyAdmin\Models\User> | ||||||
|  |  */ | ||||||
|  | class UserFactory extends Factory | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * The current password being used by the factory. | ||||||
|  |      */ | ||||||
|  |     protected static ?string $password; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Define the model's default state. | ||||||
|  |      * | ||||||
|  |      * @return array<string, mixed> | ||||||
|  |      */ | ||||||
|  |     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, | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										44
									
								
								database/migrations/2024_12_14_030215_modify_users_table.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								database/migrations/2024_12_14_030215_modify_users_table.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  |  | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      */ | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         DB::statement('ALTER TABLE `users` MODIFY `id` BIGINT UNSIGNED NOT NULL;'); | ||||||
|  |         DB::statement('ALTER TABLE `users` DROP PRIMARY KEY;'); | ||||||
|  |         DB::statement('ALTER TABLE `users` MODIFY `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (`id`);'); | ||||||
|  |  | ||||||
|  |         Schema::table('users', function (Blueprint $table) { | ||||||
|  |             $table->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']); | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | }; | ||||||
| @ -0,0 +1,36 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  |  | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      */ | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         Schema::create('user_logins', function (Blueprint $table) { | ||||||
|  |             $table->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'); | ||||||
|  |     } | ||||||
|  | }; | ||||||
| @ -0,0 +1,33 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  |  | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      */ | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         Schema::create('personal_access_tokens', function (Blueprint $table) { | ||||||
|  |             $table->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'); | ||||||
|  |     } | ||||||
|  | }; | ||||||
| @ -0,0 +1,153 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  |  | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      */ | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         $teams = config('permission.teams'); | ||||||
|  |         $tableNames = config('permission.table_names'); | ||||||
|  |         $columnNames = config('permission.column_names'); | ||||||
|  |         $pivotRole = $columnNames['role_pivot_key'] ?? 'role_id'; | ||||||
|  |         $pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id'; | ||||||
|  |  | ||||||
|  |         if (empty($tableNames)) { | ||||||
|  |             throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if ($teams && empty($columnNames['team_foreign_key'] ?? null)) { | ||||||
|  |             throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Schema::create($tableNames['permissions'], function (Blueprint $table) { | ||||||
|  |             //$table->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']); | ||||||
|  |     } | ||||||
|  | }; | ||||||
| @ -0,0 +1,46 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  | use Laravel\Fortify\Fortify; | ||||||
|  |  | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      */ | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         Schema::table('users', function (Blueprint $table) { | ||||||
|  |             $table->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', | ||||||
|  |             ] : [])); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | }; | ||||||
| @ -0,0 +1,37 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  |  | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      */ | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         Schema::create('settings', function (Blueprint $table) { | ||||||
|  |             $table->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'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | }; | ||||||
| @ -0,0 +1,48 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  |  | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      */ | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         Schema::create('media_items', function (Blueprint $table) { | ||||||
|  |             $table->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'); | ||||||
|  |     } | ||||||
|  | }; | ||||||
| @ -0,0 +1,52 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  |  | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      * | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public function up() | ||||||
|  |     { | ||||||
|  |         $connection = config('audit.drivers.database.connection', config('database.default')); | ||||||
|  |         $table = config('audit.drivers.database.table', 'audits'); | ||||||
|  |  | ||||||
|  |         Schema::connection($connection)->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); | ||||||
|  |     } | ||||||
|  | }; | ||||||
							
								
								
									
										14
									
								
								database/seeders/PermissionSeeder.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								database/seeders/PermissionSeeder.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Database\Seeders; | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Seeder; | ||||||
|  | use Koneko\VuexyAdmin\Services\RBACService; | ||||||
|  |  | ||||||
|  | class PermissionSeeder extends Seeder | ||||||
|  | { | ||||||
|  |     public function run() | ||||||
|  |     { | ||||||
|  |         RBACService::loadRolesAndPermissions(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										108
									
								
								database/seeders/SettingSeeder.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								database/seeders/SettingSeeder.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Database\Seeders; | ||||||
|  |  | ||||||
|  | use Illuminate\Database\Seeder; | ||||||
|  | use Illuminate\Support\Facades\Crypt; | ||||||
|  | use Koneko\VuexyAdmin\Models\Setting; | ||||||
|  |  | ||||||
|  | class SettingSeeder extends Seeder | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the database seeds. | ||||||
|  |      * | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public function run() | ||||||
|  |     { | ||||||
|  |         $settings_array = [ | ||||||
|  |             /* | ||||||
|  |             'app_title' => '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, | ||||||
|  |             ]); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								database/seeders/UserSeeder.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								database/seeders/UserSeeder.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | namespace Database\Seeders; | ||||||
|  |  | ||||||
|  | use Koneko\VuexyAdmin\Models\User; | ||||||
|  | use Koneko\VuexyAdmin\Services\AvatarImageService; | ||||||
|  | use Illuminate\Http\UploadedFile; | ||||||
|  | use Illuminate\Support\Facades\Storage; | ||||||
|  | use Illuminate\Database\Seeder; | ||||||
|  |  | ||||||
|  | class UserSeeder extends Seeder | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the database seeds. | ||||||
|  |      * | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public function run() | ||||||
|  |     { | ||||||
|  |         // Define el disco y la carpeta | ||||||
|  |         $disk = 'public'; | ||||||
|  |         $directory = 'profile-photos'; | ||||||
|  |  | ||||||
|  |         // Verifica si la carpeta existe | ||||||
|  |         if (Storage::disk($disk)->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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										129
									
								
								resources/assets/css/demo.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								resources/assets/css/demo.css
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
|  | } | ||||||
							
								
								
									
										245
									
								
								resources/assets/js/bootstrap-table/bootstrapTableManager.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								resources/assets/js/bootstrap-table/bootstrapTableManager.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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; | ||||||
							
								
								
									
										132
									
								
								resources/assets/js/bootstrap-table/globalConfig.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								resources/assets/js/bootstrap-table/globalConfig.js
									
									
									
									
									
										Normal file
									
								
							| @ -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', | ||||||
|  | }; | ||||||
|  |  | ||||||
							
								
								
									
										193
									
								
								resources/assets/js/bootstrap-table/globalFormatters.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								resources/assets/js/bootstrap-table/globalFormatters.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 ` | ||||||
|  |         <div class="flex space-x-2"> | ||||||
|  |             <a href="${editUrl}" title="Editar" class="icon-button hover:text-slate-700"> | ||||||
|  |                 <i class="ti ti-edit"></i> | ||||||
|  |             </a> | ||||||
|  |             <a href="${deleteUrl}" title="Eliminar" class="icon-button hover:text-slate-700"> | ||||||
|  |                 <i class="ti ti-trash"></i> | ||||||
|  |             </a> | ||||||
|  |             <a href="${showUrl}" title="Ver" class="icon-button hover:text-slate-700"> | ||||||
|  |                 <i class="ti ti-eye"></i> | ||||||
|  |             </a> | ||||||
|  |         </div> | ||||||
|  |     `.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 ? '' : `<span class="${trueClass}">${trueIcon ? `<i class="${trueIcon} ${iconClass}"></i> ` : ''}${trueText}</span>`; | ||||||
|  |     const falseElement = !falseIcon && !falseText ? '' : `<span class="${falseClass}">${falseIcon ? `<i class="${falseIcon}"></i> ` : ''}${falseText}</span>`; | ||||||
|  |  | ||||||
|  |     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 `<span class="badge bg-${color} ${textColor} ${additionalClass}">${value}</span>`; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const statusIntBadgeBgFormatter = (value, row, index) => { | ||||||
|  |     return value | ||||||
|  |         ? `<span class="badge bg-label-${statusIntBadgeBgCatalogCss[value]}">${statusIntBadgeBgCatalog[value]}</span>` | ||||||
|  |         : ''; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const textNowrapFormatter = (value, row, index) => { | ||||||
|  |     if (!value) return ''; | ||||||
|  |         return `<span class="text-nowrap">${value}</span>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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 ` | ||||||
|  |         <div class="flex items-center space-x-3" style="min-width: 240px"> | ||||||
|  |             <a href="${profileUrl}" class="flex-shrink-0"> | ||||||
|  |                 <img src="${avatar}" alt="Avatar" class="w-10 h-10 rounded-full border border-gray-300 shadow-sm hover:scale-105 transition-transform"> | ||||||
|  |             </a> | ||||||
|  |             <div class="truncate"> | ||||||
|  |                 <a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.full_name}</a> | ||||||
|  |                 <small class="text-muted block truncate">${email}</small> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     `; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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 ` | ||||||
|  |         <div class="flex items-center space-x-3" style="min-width: 240px"> | ||||||
|  |             <a href="${profileUrl}" class="flex-shrink-0"> | ||||||
|  |                 <img src="${avatar}" alt="Avatar" class="w-10 h-10 rounded-full border border-gray-300 shadow-sm hover:scale-105 transition-transform"> | ||||||
|  |             </a> | ||||||
|  |             <div class="truncate"> | ||||||
|  |                 <a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.full_name}</a> | ||||||
|  |                 <small class="text-muted block truncate">${email}</small> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     `; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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 ` | ||||||
|  |         <div class="flex flex-col"> | ||||||
|  |             <a href="${showUrl}" class="font-medium text-slate-600 hover:underline block text-wrap">${row.creator}</a> | ||||||
|  |             <small class="text-muted">${email}</small> | ||||||
|  |         </div> | ||||||
|  |     `; | ||||||
|  | }; | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user