first commit
This commit is contained in:
commit
b21a11c2ee
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
|
24
.gitattributes
vendored
Normal file
24
.gitattributes
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Normaliza los saltos de línea en diferentes SO
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Reglas para archivos específicos
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
# Evitar que estos archivos se exporten con Composer create-project
|
||||||
|
/.github export-ignore
|
||||||
|
/.gitignore export-ignore
|
||||||
|
/.git export-ignore
|
||||||
|
.gitattributes export-ignore
|
||||||
|
.editorconfig export-ignore
|
||||||
|
.prettierrc.json export-ignore
|
||||||
|
.prettierignore export-ignore
|
||||||
|
.eslintrc.json export-ignore
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
CONTRIBUTING.md export-ignore
|
||||||
|
README.md export-ignore
|
||||||
|
composer.lock export-ignore
|
||||||
|
package-lock.json 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);
|
||||||
|
}
|
||||||
|
}
|
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 koneko
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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
|
||||||
|
}
|
130
README.md
Normal file
130
README.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# 🎨 Laravel Vuexy Admin
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<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>
|
||||||
|
</p>
|
||||||
|
<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/l/koneko/laravel-vuexy-admin" alt="License"></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>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 Descripción
|
||||||
|
|
||||||
|
**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
|
||||||
|
- 🔹 Sistema de autenticación con Laravel Fortify.
|
||||||
|
- 🔹 Gestión avanzada de usuarios con Livewire.
|
||||||
|
- 🔹 Control de roles y permisos con Spatie Permissions.
|
||||||
|
- 🔹 Auditoría de acciones con Laravel Auditing.
|
||||||
|
- 🔹 Publicación de configuraciones y vistas.
|
||||||
|
- 🔹 Soporte para cache y optimización de rendimiento.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Instalación
|
||||||
|
|
||||||
|
Instalar vía **Composer**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require koneko/laravel-vuexy-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Publicar archivos de configuración y migraciones:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=vuexy-admin-config
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Uso básico
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Koneko\VuexyAdmin\Models\User;
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Juan Pérez',
|
||||||
|
'email' => 'juan@example.com',
|
||||||
|
'password' => bcrypt('secret'),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Configuración adicional
|
||||||
|
|
||||||
|
Si necesitas personalizar la configuración del módulo, publica el archivo de configuración:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=vuexy-admin-config
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto generará `config/vuexy_menu.php`, donde puedes modificar valores predeterminados.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Dependencias
|
||||||
|
|
||||||
|
Este paquete requiere las siguientes dependencias:
|
||||||
|
- Laravel 11
|
||||||
|
- `laravel/fortify` (autenticación)
|
||||||
|
- `spatie/laravel-permission` (gestión de roles y permisos)
|
||||||
|
- `owen-it/laravel-auditing` (auditoría de usuarios)
|
||||||
|
- `livewire/livewire` (interfaz dinámica)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Publicación de Assets y Configuraciones
|
||||||
|
|
||||||
|
Para publicar configuraciones y seeders:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=vuexy-admin-config
|
||||||
|
php artisan vendor:publish --tag=vuexy-admin-seeders
|
||||||
|
php artisan migrate --seed
|
||||||
|
```
|
||||||
|
|
||||||
|
Para publicar imágenes del tema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=vuexy-admin-images
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 Repositorio Principal y Sincronización
|
||||||
|
|
||||||
|
Este repositorio es una **copia sincronizada** del repositorio principal alojado en **[Tea - Koneko Git](https://git.koneko.mx/koneko/laravel-vuexy-admin)**.
|
||||||
|
|
||||||
|
### 🔄 Sincronización con GitHub
|
||||||
|
- **Repositorio Principal:** [git.koneko.mx](https://git.koneko.mx/koneko/laravel-vuexy-admin)
|
||||||
|
- **Repositorio en GitHub:** [github.com/koneko-mx/laravel-vuexy-admin](https://github.com/koneko-mx/laravel-vuexy-admin)
|
||||||
|
- **Los cambios pueden reflejarse primero en Tea antes de GitHub.**
|
||||||
|
|
||||||
|
### 🤝 Contribuciones
|
||||||
|
Si deseas contribuir:
|
||||||
|
1. Puedes abrir un **Issue** en [GitHub Issues](https://github.com/koneko-mx/laravel-vuexy-admin/issues).
|
||||||
|
2. Para Pull Requests, **preferimos contribuciones en Tea**. Contacta a `admin@koneko.mx` para solicitar acceso.
|
||||||
|
|
||||||
|
⚠️ **Nota:** Algunos cambios pueden tardar en reflejarse en GitHub, ya que este repositorio se actualiza automáticamente desde Tea.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏅 Licencia
|
||||||
|
|
||||||
|
Este paquete es de código abierto bajo la licencia [MIT](LICENSE).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Hecho con ❤️ por <a href="https://koneko.mx">Koneko Soluciones Tecnológicas</a>
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
}
|
41
composer.json
Normal file
41
composer.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "koneko/laravel-vuexy-admin",
|
||||||
|
"description": "Laravel Vuexy Admin, un modulo de administracion optimizado para México.",
|
||||||
|
"keywords": ["laravel", "koneko", "framework", "vuexy", "admin", "mexico"],
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"intervention/image-laravel": "^1.4",
|
||||||
|
"laravel/framework": "^11.31",
|
||||||
|
"laravel/fortify": "^1.25",
|
||||||
|
"laravel/sanctum": "^4.0",
|
||||||
|
"livewire/livewire": "^3.5",
|
||||||
|
"owen-it/laravel-auditing": "^13.6",
|
||||||
|
"spatie/laravel-permission": "^6.10"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Koneko\\VuexyAdmin\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Koneko\\VuexyAdmin\\Providers\\VuexyAdminServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Arturo Corro Pacheco",
|
||||||
|
"email": "arturo@koneko.mx"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/koneko-mx/laravel-vuexy-admin",
|
||||||
|
"issues": "https://github.com/koneko-mx/laravel-vuexy-admin/issues"
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
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@koneko.test,Administrador Web,LAdmin123
|
||||||
|
Productos y servicios,productos@koneko.test,Productos y servicios,LAdmin123
|
||||||
|
Recursos humanos,rrhh@koneko.test,Recursos humanos,LAdmin123
|
||||||
|
Nómina,nomina@koneko.test,Nómina,LAdmin123
|
||||||
|
Activos fijos,activos@koneko.test,Activos fijos,LAdmin123
|
||||||
|
Compras y gastos,compras@koneko.test,Compras y gastos,LAdmin123
|
||||||
|
CRM,crm@koneko.test,CRM,LAdmin123
|
||||||
|
Vendedor,vendedor@koneko.test,Vendedor,LAdmin123
|
||||||
|
Gerente,gerente@koneko.test,Gerente,LAdmin123
|
||||||
|
Facturación,facturacion@koneko.test,Facturación,LAdmin123
|
||||||
|
Facturación avanzado,facturacion_avanzado@koneko.test,Facturación avanzado,LAdmin123
|
||||||
|
Finanzas,finanzas@koneko.test,Finanzas,LAdmin123
|
||||||
|
Almacenista,almacenista@koneko.test,Almacenista,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();
|
||||||
|
}
|
||||||
|
}
|
109
database/seeders/SettingSeeder.php
Normal file
109
database/seeders/SettingSeeder.php
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?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' => 'sadmin@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
|
||||||
|
$user = 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'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Auditor
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Auditor',
|
||||||
|
'email' => 'auditor@koneko.mx',
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => bcrypt('LAdmin123'),
|
||||||
|
'status' => User::STATUS_ENABLED,
|
||||||
|
])->assignRole('Auditor');
|
||||||
|
|
||||||
|
$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',
|
||||||
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user