first commit
This commit is contained in:
parent
f54ca8e341
commit
68ca619829
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
38
.gitattributes
vendored
Normal file
38
.gitattributes
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
# Ignorar archivos de configuración y herramientas de desarrollo
|
||||
.editorconfig export-ignore
|
||||
.prettierrc.json export-ignore
|
||||
.prettierignore export-ignore
|
||||
.eslintrc.json export-ignore
|
||||
|
||||
# Ignorar node_modules y dependencias locales
|
||||
node_modules/ export-ignore
|
||||
vendor/ export-ignore
|
||||
|
||||
# Ignorar archivos de build
|
||||
npm-debug.log export-ignore
|
||||
|
||||
# Ignorar carpetas de logs y caché
|
||||
storage/logs/ export-ignore
|
||||
storage/framework/ export-ignore
|
||||
|
||||
# Ignorar carpetas de compilación de frontend
|
||||
public/build/ export-ignore
|
||||
dist/ export-ignore
|
||||
|
||||
# Ignorar archivos de CI/CD
|
||||
.github/ export-ignore
|
||||
.gitlab-ci.yml export-ignore
|
||||
.vscode/ export-ignore
|
||||
.idea/ export-ignore
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/node_modules
|
||||
/vendor
|
||||
/.vscode
|
||||
/.nova
|
||||
/.fleet
|
||||
/.phpactor.json
|
||||
/.phpunit.cache
|
||||
/.phpunit.result.cache
|
||||
/.zed
|
||||
/.idea
|
16
.prettierignore
Normal file
16
.prettierignore
Normal file
@ -0,0 +1,16 @@
|
||||
# Dependencias de Composer y Node.js
|
||||
/vendor/
|
||||
/node_modules/
|
||||
|
||||
# Caché y logs
|
||||
/storage/
|
||||
*.log
|
||||
*.cache
|
||||
|
||||
# Archivos del sistema
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Configuración de editores
|
||||
.idea/
|
||||
.vscode/
|
29
.prettierrc.json
Normal file
29
.prettierrc.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf",
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"resources/assets/**/*.js"
|
||||
],
|
||||
"options": {
|
||||
"semi": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
40
Actions/Fortify/CreateNewUser.php
Normal file
40
Actions/Fortify/CreateNewUser.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique(User::class),
|
||||
],
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'password' => Hash::make($input['password']),
|
||||
]);
|
||||
}
|
||||
}
|
18
Actions/Fortify/PasswordValidationRules.php
Normal file
18
Actions/Fortify/PasswordValidationRules.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Actions\Fortify;
|
||||
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
}
|
29
Actions/Fortify/ResetUserPassword.php
Normal file
29
Actions/Fortify/ResetUserPassword.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
32
Actions/Fortify/UpdateUserPassword.php
Normal file
32
Actions/Fortify/UpdateUserPassword.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'current_password' => ['required', 'string', 'current_password:web'],
|
||||
'password' => $this->passwordRules(),
|
||||
], [
|
||||
'current_password.current_password' => __('The provided password does not match your current password.'),
|
||||
])->validateWithBag('updatePassword');
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
60
Actions/Fortify/UpdateUserProfileInformation.php
Normal file
60
Actions/Fortify/UpdateUserProfileInformation.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Actions\Fortify;
|
||||
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email',
|
||||
'max:255',
|
||||
Rule::unique('users')->ignore($user->id),
|
||||
],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if (
|
||||
$input['email'] !== $user->email &&
|
||||
$user instanceof MustVerifyEmail
|
||||
) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
} else {
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given verified user's profile information.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
protected function updateVerifiedUser(User $user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
41
CHANGELOG.md
Normal file
41
CHANGELOG.md
Normal file
@ -0,0 +1,41 @@
|
||||
# 📜 CHANGELOG - Laravel Vuexy Admin
|
||||
|
||||
Este documento sigue el formato [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [0.1.0] - ALPHA - 2025-03-05
|
||||
|
||||
### ✨ Added (Agregado)
|
||||
- 📌 **Integración con los catálogos SAT (CFDI 4.0)**:
|
||||
- `sat_banco`, `sat_clave_prod_serv`, `sat_clave_unidad`, `sat_codigo_postal`, `sat_colonia`, `sat_deduccion`, `sat_estado`, `sat_forma_pago`, `sat_localidad`, `sat_municipio`, `sat_moneda`, `sat_pais`, `sat_percepcion`, `sat_regimen_contratacion`, `sat_regimen_fiscal`, `sat_uso_cfdi`.
|
||||
- 🎨 **Interfaz basada en Vuexy Admin** con integración de Laravel Blade + Livewire.
|
||||
- 🔑 **Sistema de autenticación y RBAC** con Laravel Fortify y Spatie Permissions.
|
||||
- 🔄 **Módulo de tipos de cambio** con integración de la API de Banxico.
|
||||
- 📦 **Manejo de almacenamiento y gestión de activos**.
|
||||
- 🚀 **Publicación inicial del repositorio en Packagist y Git Tea**.
|
||||
|
||||
### 🛠 Changed (Modificado)
|
||||
- **Optimización del sistema de permisos y roles** para mayor flexibilidad.
|
||||
|
||||
### 🐛 Fixed (Correcciones)
|
||||
- Se corrigieron errores en migraciones de catálogos SAT.
|
||||
|
||||
---
|
||||
|
||||
## 📅 Próximos Cambios Planeados
|
||||
- 📊 **Módulo de Reportes** con Laravel Excel y Charts.
|
||||
- 🏪 **Módulo de Inventarios y Punto de Venta (POS)**.
|
||||
- 📍 **Mejor integración con APIs de geolocalización**.
|
||||
|
||||
---
|
||||
|
||||
**📌 Nota:** Esta es la primera versión **ALPHA**, aún en desarrollo.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Sincronización de Cambios
|
||||
Este `CHANGELOG.md` se actualiza primero en nuestro repositorio principal en **[Tea - Koneko Git](https://git.koneko.mx/koneko/laravel-vuexy-admin)** y luego se refleja en GitHub.
|
||||
Los cambios recientes pueden verse antes en **Tea** que en **GitHub** debido a la sincronización automática.
|
||||
|
||||
---
|
||||
|
||||
📅 Última actualización: **2024-03-05**.
|
9
CONTRIBUTING.md
Normal file
9
CONTRIBUTING.md
Normal file
@ -0,0 +1,9 @@
|
||||
## 🔐 Acceso al Repositorio Privado
|
||||
|
||||
Nuestro servidor Git en **Tea** tiene un registro cerrado. Para contribuir:
|
||||
|
||||
1. Abre un **Issue** en [GitHub](https://github.com/koneko-mx/laravel-vuexy-admin/issues) indicando tu interés en contribuir.
|
||||
2. Alternativamente, envía un correo a **contacto@koneko.mx** solicitando acceso.
|
||||
3. Una vez aprobado, recibirás una invitación para registrarte y clonar el repositorio.
|
||||
|
||||
Si solo necesitas acceso de lectura, puedes clonar la versión pública en **GitHub**.
|
43
Console/Commands/CleanInitialAvatars.php
Normal file
43
Console/Commands/CleanInitialAvatars.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CleanInitialAvatars extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'avatars:clean-initial';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Elimina avatares generados automáticamente en el directorio initial-avatars';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$directory = 'initial-avatars';
|
||||
$files = Storage::disk('public')->files($directory);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$lastModified = Storage::disk('public')->lastModified($file);
|
||||
|
||||
// Elimina archivos no accedidos en los últimos 30 días
|
||||
if (now()->timestamp - $lastModified > 30 * 24 * 60 * 60) {
|
||||
Storage::disk('public')->delete($file);
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Avatares iniciales antiguos eliminados.');
|
||||
}
|
||||
}
|
26
Console/Commands/SyncRBAC.php
Normal file
26
Console/Commands/SyncRBAC.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Koneko\VuexyAdmin\Services\RBACService;
|
||||
|
||||
class SyncRBAC extends Command
|
||||
{
|
||||
protected $signature = 'rbac:sync {action}';
|
||||
protected $description = 'Sincroniza roles y permisos con archivos JSON';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$action = $this->argument('action');
|
||||
if ($action === 'import') {
|
||||
RBACService::loadRolesAndPermissions();
|
||||
$this->info('Roles y permisos importados correctamente.');
|
||||
} elseif ($action === 'export') {
|
||||
// Implementación para exportar los roles a JSON
|
||||
$this->info('Exportación de roles y permisos completada.');
|
||||
} else {
|
||||
$this->error('Acción no válida. Usa "import" o "export".');
|
||||
}
|
||||
}
|
||||
}
|
72
Helpers/CatalogHelper.php
Normal file
72
Helpers/CatalogHelper.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Helpers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class CatalogHelper
|
||||
{
|
||||
public static function ajaxFlexibleResponse(Builder $query, array $options = []): JsonResponse
|
||||
{
|
||||
$id = $options['id'] ?? null;
|
||||
$searchTerm = $options['searchTerm'] ?? null;
|
||||
$limit = $options['limit'] ?? 20;
|
||||
$keyField = $options['keyField'] ?? 'id';
|
||||
$valueField = $options['valueField'] ?? 'name';
|
||||
$responseType = $options['responseType'] ?? 'select2';
|
||||
$customFilters = $options['filters'] ?? [];
|
||||
|
||||
// Si se pasa un ID, devolver un registro completo
|
||||
if ($id) {
|
||||
$data = $query->find($id);
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
// Aplicar filtros personalizados
|
||||
foreach ($customFilters as $field => $value) {
|
||||
if (!is_null($value)) {
|
||||
$query->where($field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar filtro de búsqueda si hay searchTerm
|
||||
if ($searchTerm) {
|
||||
$query->where($valueField, 'like', '%' . $searchTerm . '%');
|
||||
}
|
||||
|
||||
// Limitar resultados si el límite no es falso
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
$results = $query->get([$keyField, $valueField]);
|
||||
|
||||
// Devolver según el tipo de respuesta
|
||||
switch ($responseType) {
|
||||
case 'keyValue':
|
||||
$data = $results->pluck($valueField, $keyField)->toArray();
|
||||
break;
|
||||
|
||||
case 'select2':
|
||||
$data = $results->map(function ($item) use ($keyField, $valueField) {
|
||||
return [
|
||||
'id' => $item->{$keyField},
|
||||
'text' => $item->{$valueField},
|
||||
];
|
||||
})->toArray();
|
||||
break;
|
||||
|
||||
default:
|
||||
$data = $results->map(function ($item) use ($keyField, $valueField) {
|
||||
return [
|
||||
'id' => $item->{$keyField},
|
||||
'text' => $item->{$valueField},
|
||||
];
|
||||
})->toArray();
|
||||
break;
|
||||
}
|
||||
|
||||
return response()->json($data);
|
||||
}
|
||||
}
|
209
Helpers/VuexyHelper.php
Normal file
209
Helpers/VuexyHelper.php
Normal file
@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Helpers;
|
||||
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class VuexyHelper
|
||||
{
|
||||
public static function appClasses()
|
||||
{
|
||||
$data = config('vuexy.custom');
|
||||
|
||||
// default data array
|
||||
$DefaultData = [
|
||||
'myLayout' => 'vertical',
|
||||
'myTheme' => 'theme-default',
|
||||
'myStyle' => 'light',
|
||||
'myRTLSupport' => false,
|
||||
'myRTLMode' => true,
|
||||
'hasCustomizer' => true,
|
||||
'showDropdownOnHover' => true,
|
||||
'displayCustomizer' => true,
|
||||
'contentLayout' => 'compact',
|
||||
'headerType' => 'fixed',
|
||||
'navbarType' => 'fixed',
|
||||
'menuFixed' => true,
|
||||
'menuCollapsed' => false,
|
||||
'footerFixed' => false,
|
||||
'customizerControls' => [
|
||||
'rtl',
|
||||
'style',
|
||||
'headerType',
|
||||
'contentLayout',
|
||||
'layoutCollapsed',
|
||||
'showDropdownOnHover',
|
||||
'layoutNavbarOptions',
|
||||
'themes',
|
||||
],
|
||||
// 'defaultLanguage'=>'en',
|
||||
];
|
||||
|
||||
// if any key missing of array from custom.php file it will be merge and set a default value from dataDefault array and store in data variable
|
||||
$data = array_merge($DefaultData, $data);
|
||||
|
||||
// All options available in the template
|
||||
$allOptions = [
|
||||
'myLayout' => ['vertical', 'horizontal', 'blank', 'front'],
|
||||
'menuCollapsed' => [true, false],
|
||||
'hasCustomizer' => [true, false],
|
||||
'showDropdownOnHover' => [true, false],
|
||||
'displayCustomizer' => [true, false],
|
||||
'contentLayout' => ['compact', 'wide'],
|
||||
'headerType' => ['fixed', 'static'],
|
||||
'navbarType' => ['fixed', 'static', 'hidden'],
|
||||
'myStyle' => ['light', 'dark', 'system'],
|
||||
'myTheme' => ['theme-default', 'theme-bordered', 'theme-semi-dark'],
|
||||
'myRTLSupport' => [true, false],
|
||||
'myRTLMode' => [true, false],
|
||||
'menuFixed' => [true, false],
|
||||
'footerFixed' => [true, false],
|
||||
'customizerControls' => [],
|
||||
// 'defaultLanguage'=>array('en'=>'en','fr'=>'fr','de'=>'de','ar'=>'ar'),
|
||||
];
|
||||
|
||||
//if myLayout value empty or not match with default options in custom.php config file then set a default value
|
||||
foreach ($allOptions as $key => $value) {
|
||||
if (array_key_exists($key, $DefaultData)) {
|
||||
if (gettype($DefaultData[$key]) === gettype($data[$key])) {
|
||||
// data key should be string
|
||||
if (is_string($data[$key])) {
|
||||
// data key should not be empty
|
||||
if (isset($data[$key]) && $data[$key] !== null) {
|
||||
// data key should not be exist inside allOptions array's sub array
|
||||
if (!array_key_exists($data[$key], $value)) {
|
||||
// ensure that passed value should be match with any of allOptions array value
|
||||
$result = array_search($data[$key], $value, 'strict');
|
||||
if (empty($result) && $result !== 0) {
|
||||
$data[$key] = $DefaultData[$key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if data key not set or
|
||||
$data[$key] = $DefaultData[$key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$data[$key] = $DefaultData[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
$styleVal = $data['myStyle'] == "dark" ? "dark" : "light";
|
||||
$styleUpdatedVal = $data['myStyle'] == "dark" ? "dark" : $data['myStyle'];
|
||||
// Determine if the layout is admin or front based on cookies
|
||||
$layoutName = $data['myLayout'];
|
||||
$isAdmin = Str::contains($layoutName, 'front') ? false : true;
|
||||
|
||||
$modeCookieName = $isAdmin ? 'admin-mode' : 'front-mode';
|
||||
$colorPrefCookieName = $isAdmin ? 'admin-colorPref' : 'front-colorPref';
|
||||
|
||||
// Determine style based on cookies, only if not 'blank-layout'
|
||||
if ($layoutName !== 'blank') {
|
||||
if (isset($_COOKIE[$modeCookieName])) {
|
||||
$styleVal = $_COOKIE[$modeCookieName];
|
||||
if ($styleVal === 'system') {
|
||||
$styleVal = isset($_COOKIE[$colorPrefCookieName]) ? $_COOKIE[$colorPrefCookieName] : 'light';
|
||||
}
|
||||
$styleUpdatedVal = $_COOKIE[$modeCookieName];
|
||||
}
|
||||
}
|
||||
|
||||
isset($_COOKIE['theme']) ? $themeVal = $_COOKIE['theme'] : $themeVal = $data['myTheme'];
|
||||
|
||||
$directionVal = isset($_COOKIE['direction']) ? ($_COOKIE['direction'] === "true" ? 'rtl' : 'ltr') : $data['myRTLMode'];
|
||||
|
||||
//layout classes
|
||||
$layoutClasses = [
|
||||
'layout' => $data['myLayout'],
|
||||
'theme' => $themeVal,
|
||||
'themeOpt' => $data['myTheme'],
|
||||
'style' => $styleVal,
|
||||
'styleOpt' => $data['myStyle'],
|
||||
'styleOptVal' => $styleUpdatedVal,
|
||||
'rtlSupport' => $data['myRTLSupport'],
|
||||
'rtlMode' => $data['myRTLMode'],
|
||||
'textDirection' => $directionVal, //$data['myRTLMode'],
|
||||
'menuCollapsed' => $data['menuCollapsed'],
|
||||
'hasCustomizer' => $data['hasCustomizer'],
|
||||
'showDropdownOnHover' => $data['showDropdownOnHover'],
|
||||
'displayCustomizer' => $data['displayCustomizer'],
|
||||
'contentLayout' => $data['contentLayout'],
|
||||
'headerType' => $data['headerType'],
|
||||
'navbarType' => $data['navbarType'],
|
||||
'menuFixed' => $data['menuFixed'],
|
||||
'footerFixed' => $data['footerFixed'],
|
||||
'customizerControls' => $data['customizerControls'],
|
||||
];
|
||||
|
||||
// sidebar Collapsed
|
||||
if ($layoutClasses['menuCollapsed'] == true) {
|
||||
$layoutClasses['menuCollapsed'] = 'layout-menu-collapsed';
|
||||
}
|
||||
|
||||
// Header Type
|
||||
if ($layoutClasses['headerType'] == 'fixed') {
|
||||
$layoutClasses['headerType'] = 'layout-menu-fixed';
|
||||
}
|
||||
// Navbar Type
|
||||
if ($layoutClasses['navbarType'] == 'fixed') {
|
||||
$layoutClasses['navbarType'] = 'layout-navbar-fixed';
|
||||
} elseif ($layoutClasses['navbarType'] == 'static') {
|
||||
$layoutClasses['navbarType'] = '';
|
||||
} else {
|
||||
$layoutClasses['navbarType'] = 'layout-navbar-hidden';
|
||||
}
|
||||
|
||||
// Menu Fixed
|
||||
if ($layoutClasses['menuFixed'] == true) {
|
||||
$layoutClasses['menuFixed'] = 'layout-menu-fixed';
|
||||
}
|
||||
|
||||
// Footer Fixed
|
||||
if ($layoutClasses['footerFixed'] == true) {
|
||||
$layoutClasses['footerFixed'] = 'layout-footer-fixed';
|
||||
}
|
||||
|
||||
// RTL Supported template
|
||||
if ($layoutClasses['rtlSupport'] == true) {
|
||||
$layoutClasses['rtlSupport'] = '/rtl';
|
||||
}
|
||||
|
||||
// RTL Layout/Mode
|
||||
if ($layoutClasses['rtlMode'] == true) {
|
||||
$layoutClasses['rtlMode'] = 'rtl';
|
||||
$layoutClasses['textDirection'] = isset($_COOKIE['direction']) ? ($_COOKIE['direction'] === "true" ? 'rtl' : 'ltr') : 'rtl';
|
||||
} else {
|
||||
$layoutClasses['rtlMode'] = 'ltr';
|
||||
$layoutClasses['textDirection'] = isset($_COOKIE['direction']) && $_COOKIE['direction'] === "true" ? 'rtl' : 'ltr';
|
||||
}
|
||||
|
||||
// Show DropdownOnHover for Horizontal Menu
|
||||
if ($layoutClasses['showDropdownOnHover'] == true) {
|
||||
$layoutClasses['showDropdownOnHover'] = true;
|
||||
} else {
|
||||
$layoutClasses['showDropdownOnHover'] = false;
|
||||
}
|
||||
|
||||
// To hide/show display customizer UI, not js
|
||||
if ($layoutClasses['displayCustomizer'] == true) {
|
||||
$layoutClasses['displayCustomizer'] = true;
|
||||
} else {
|
||||
$layoutClasses['displayCustomizer'] = false;
|
||||
}
|
||||
|
||||
return $layoutClasses;
|
||||
}
|
||||
|
||||
public static function updatePageConfig($pageConfigs)
|
||||
{
|
||||
$demo = 'custom';
|
||||
if (isset($pageConfigs)) {
|
||||
if (count($pageConfigs) > 0) {
|
||||
foreach ($pageConfigs as $config => $val) {
|
||||
Config::set('vuexy.' . $demo . '.' . $config, $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
Http/Controllers/AdminController.php
Normal file
62
Http/Controllers/AdminController.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
use Koneko\VuexyAdmin\Services\VuexyAdminService;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
public function searchNavbar()
|
||||
{
|
||||
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
|
||||
|
||||
$VuexyAdminService = app(VuexyAdminService::class);
|
||||
|
||||
return response()->json($VuexyAdminService->getVuexySearchData());
|
||||
}
|
||||
|
||||
public function quickLinksUpdate(Request $request)
|
||||
{
|
||||
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
|
||||
|
||||
$validated = $request->validate([
|
||||
'action' => 'required|in:update,remove',
|
||||
'route' => 'required|string',
|
||||
]);
|
||||
|
||||
$quickLinks = Setting::where('user_id', Auth::user()->id)
|
||||
->where('key', 'quicklinks')
|
||||
->first();
|
||||
|
||||
$quickLinks = $quickLinks ? json_decode($quickLinks->value, true) : [];
|
||||
|
||||
if ($validated['action'] === 'update') {
|
||||
// Verificar si ya existe
|
||||
if (!in_array($validated['route'], $quickLinks))
|
||||
$quickLinks[] = $validated['route'];
|
||||
} elseif ($validated['action'] === 'remove') {
|
||||
// Eliminar la ruta si existe
|
||||
$quickLinks = array_filter($quickLinks, function ($route) use ($validated) {
|
||||
return $route !== $validated['route'];
|
||||
});
|
||||
}
|
||||
|
||||
Setting::updateOrCreate(['user_id' => Auth::user()->id, 'key' => 'quicklinks'], ['value' => json_encode($quickLinks)]);
|
||||
|
||||
VuexyAdminService::clearQuickLinksCache();
|
||||
}
|
||||
|
||||
public function generalSettings()
|
||||
{
|
||||
return view('vuexy-admin::admin-settings.webapp-general-settings');
|
||||
}
|
||||
|
||||
public function smtpSettings()
|
||||
{
|
||||
return view('vuexy-admin::admin-settings.smtp-settings');
|
||||
}
|
||||
}
|
144
Http/Controllers/AuthController.php
Normal file
144
Http/Controllers/AuthController.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/*
|
||||
public function loginView()
|
||||
{
|
||||
dd($viewMode);
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.login-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function registerView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function confirmPasswordView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.confirm-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function resetPasswordView()
|
||||
{
|
||||
if (!Features::enabled(Features::resetPasswords()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function requestPasswordResetLinkView(Request $request)
|
||||
{
|
||||
if (!Features::enabled(Features::resetPasswords()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs, 'request' => $request]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public function twoFactorChallengeView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.two-factor-challenge-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function twoFactorRecoveryCodesView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function twoFactorAuthenticationView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public function verifyEmailView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.verify-email-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function showEmailVerificationForm()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
|
||||
public function userProfileView()
|
||||
{
|
||||
if (!Features::enabled(Features::registration()))
|
||||
abort(403, 'El registro está deshabilitado.');
|
||||
|
||||
$viewMode = config('vuexy.custom.authViewMode');
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
}
|
||||
*/
|
||||
}
|
41
Http/Controllers/CacheController.php
Normal file
41
Http/Controllers/CacheController.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Koneko\VuexyAdmin\Services\CacheConfigService;
|
||||
|
||||
class CacheController extends Controller
|
||||
{
|
||||
public function generateConfigCache()
|
||||
{
|
||||
try {
|
||||
// Lógica para generar cache
|
||||
Artisan::call('config:cache');
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Cache generado correctamente.']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => 'Error al generar el cache.', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function generateRouteCache()
|
||||
{
|
||||
try {
|
||||
// Lógica para generar cache de rutas
|
||||
Artisan::call('route:cache');
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Cache de rutas generado correctamente.']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => 'Error al generar el cache de rutas.', 'error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function cacheManager(CacheConfigService $cacheConfigService)
|
||||
{
|
||||
$configCache = $cacheConfigService->getConfig();
|
||||
|
||||
return view('vuexy-admin::cache-manager.index', compact('configCache'));
|
||||
}
|
||||
}
|
32
Http/Controllers/HomeController.php
Normal file
32
Http/Controllers/HomeController.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('vuexy-admin::pages.home');
|
||||
}
|
||||
|
||||
public function about()
|
||||
{
|
||||
return view('vuexy-admin::pages.about');
|
||||
}
|
||||
|
||||
public function comingsoon()
|
||||
{
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view('vuexy-admin::pages.comingsoon', compact('pageConfigs'));
|
||||
}
|
||||
|
||||
public function underMaintenance()
|
||||
{
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
return view('vuexy-admin::pages.under-maintenance', compact('pageConfigs'));
|
||||
}
|
||||
}
|
21
Http/Controllers/LanguageController.php
Normal file
21
Http/Controllers/LanguageController.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class LanguageController extends Controller
|
||||
{
|
||||
public function swap(Request $request, $locale)
|
||||
{
|
||||
if (!in_array($locale, ['es', 'en', 'fr', 'ar', 'de'])) {
|
||||
abort(400);
|
||||
} else {
|
||||
$request->session()->put('locale', $locale);
|
||||
}
|
||||
App::setLocale($locale);
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
37
Http/Controllers/PermissionController.php
Normal file
37
Http/Controllers/PermissionController.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$permissions = Permission::latest()->get();
|
||||
|
||||
return DataTables::of($permissions)
|
||||
->addIndexColumn()
|
||||
->addColumn('assigned_to', function ($row) {
|
||||
return (Arr::pluck($row->roles, ['name']));
|
||||
})
|
||||
->editColumn('created_at', function ($request) {
|
||||
return $request->created_at->format('Y-m-d h:i:s a');
|
||||
})
|
||||
->make(true);
|
||||
}
|
||||
|
||||
return view('vuexy-admin::permissions.index');
|
||||
}
|
||||
}
|
38
Http/Controllers/RoleController.php
Normal file
38
Http/Controllers/RoleController.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class RoleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('vuexy-admin::roles.index');
|
||||
}
|
||||
|
||||
public function checkUniqueRoleName(Request $request)
|
||||
{
|
||||
$id = $request->input('id');
|
||||
$name = $request->input('name');
|
||||
|
||||
// Verificar si el nombre ya existe en la base de datos
|
||||
$existingRole = Role::where('name', $name)
|
||||
->whereNot('id', $id)
|
||||
->first();
|
||||
|
||||
if ($existingRole) {
|
||||
return response()->json(['valid' => false]);
|
||||
}
|
||||
|
||||
return response()->json(['valid' => true]);
|
||||
}
|
||||
}
|
76
Http/Controllers/RolePermissionController.php
Normal file
76
Http/Controllers/RolePermissionController.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class RolePermissionController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return response()->json([
|
||||
'roles' => Role::with('permissions')->get(),
|
||||
'permissions' => Permission::all()
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeRole(Request $request)
|
||||
{
|
||||
$request->validate(['name' => 'required|string|unique:roles,name']);
|
||||
$role = Role::create(['name' => $request->name]);
|
||||
return response()->json(['message' => 'Rol creado con éxito', 'role' => $role]);
|
||||
}
|
||||
|
||||
public function storePermission(Request $request)
|
||||
{
|
||||
$request->validate(['name' => 'required|string|unique:permissions,name']);
|
||||
$permission = Permission::create(['name' => $request->name]);
|
||||
return response()->json(['message' => 'Permiso creado con éxito', 'permission' => $permission]);
|
||||
}
|
||||
|
||||
public function assignPermissionToRole(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'role_id' => 'required|exists:roles,id',
|
||||
'permission_id' => 'required|exists:permissions,id'
|
||||
]);
|
||||
|
||||
$role = Role::findById($request->role_id);
|
||||
$permission = Permission::findById($request->permission_id);
|
||||
|
||||
$role->givePermissionTo($permission->name);
|
||||
|
||||
return response()->json(['message' => 'Permiso asignado con éxito']);
|
||||
}
|
||||
|
||||
public function removePermissionFromRole(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'role_id' => 'required|exists:roles,id',
|
||||
'permission_id' => 'required|exists:permissions,id'
|
||||
]);
|
||||
|
||||
$role = Role::findById($request->role_id);
|
||||
$permission = Permission::findById($request->permission_id);
|
||||
|
||||
$role->revokePermissionTo($permission->name);
|
||||
|
||||
return response()->json(['message' => 'Permiso eliminado con éxito']);
|
||||
}
|
||||
|
||||
public function deleteRole($id)
|
||||
{
|
||||
$role = Role::findOrFail($id);
|
||||
$role->delete();
|
||||
return response()->json(['message' => 'Rol eliminado con éxito']);
|
||||
}
|
||||
|
||||
public function deletePermission($id)
|
||||
{
|
||||
$permission = Permission::findOrFail($id);
|
||||
$permission->delete();
|
||||
return response()->json(['message' => 'Permiso eliminado con éxito']);
|
||||
}
|
||||
}
|
188
Http/Controllers/UserController copy.php
Normal file
188
Http/Controllers/UserController copy.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyAdmin\Services\AvatarImageService;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$users = User::when(!Auth::user()->hasRole('SuperAdmin'), function ($query) {
|
||||
$query->where('id', '>', 1);
|
||||
})
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
return DataTables::of($users)
|
||||
->only(['id', 'name', 'email', 'avatar', 'roles', 'status', 'created_at'])
|
||||
->addIndexColumn()
|
||||
->addColumn('avatar', function ($user) {
|
||||
return $user->profile_photo_url;
|
||||
})
|
||||
->addColumn('roles', function ($user) {
|
||||
return (Arr::pluck($user->roles, ['name']));
|
||||
})
|
||||
/*
|
||||
->addColumn('stores', function ($user) {
|
||||
return (Arr::pluck($user->stores, ['nombre']));
|
||||
})
|
||||
y*/
|
||||
->editColumn('created_at', function ($user) {
|
||||
return $user->created_at->format('Y-m-d');
|
||||
})
|
||||
->make(true);
|
||||
}
|
||||
|
||||
return view('vuexy-admin::users.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|max:255|unique:users',
|
||||
'photo' => 'nullable|mimes:jpg,jpeg,png|max:1024',
|
||||
'password' => 'required',
|
||||
]);
|
||||
|
||||
if ($validator->fails())
|
||||
return response()->json(['errors' => $validator->errors()->all()]);
|
||||
|
||||
// Preparamos los datos
|
||||
$user_request = array_merge_recursive($request->all(), [
|
||||
'remember_token' => Str::random(10),
|
||||
'created_by' => Auth::user()->id,
|
||||
]);
|
||||
|
||||
$user_request['password'] = bcrypt($request->password);
|
||||
|
||||
// Guardamos el nuevo usuario
|
||||
$user = User::create($user_request);
|
||||
|
||||
// Asignmos los permisos
|
||||
$user->assignRole($request->roles);
|
||||
|
||||
// Asignamos Sucursals
|
||||
//$user->stores()->attach($request->stores);
|
||||
|
||||
if ($request->file('photo')){
|
||||
$avatarImageService = new AvatarImageService();
|
||||
|
||||
$avatarImageService->updateProfilePhoto($user, $request->file('photo'));
|
||||
}
|
||||
|
||||
return response()->json(['success' => 'Se agrego correctamente el usuario']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int User $user
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show(User $user)
|
||||
{
|
||||
return view('vuexy-admin::users.show', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int User $user
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function updateAjax(Request $request, User $user)
|
||||
{
|
||||
// Validamos los datos
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|max:191',
|
||||
'email' => "required|max:191|unique:users,email," . $user->id,
|
||||
'photo' => 'nullable|mimes:jpg,jpeg,png|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails())
|
||||
return response()->json(['errors' => $validator->errors()->all()]);
|
||||
|
||||
// Preparamos los datos
|
||||
$user_request = $request->all();
|
||||
|
||||
if ($request->password) {
|
||||
$user_request['password'] = bcrypt($request->password);
|
||||
} else {
|
||||
unset($user_request['password']);
|
||||
}
|
||||
|
||||
// Guardamos los cambios
|
||||
$user->update($user_request);
|
||||
|
||||
// Sincronizamos Roles
|
||||
$user->syncRoles($request->roles);
|
||||
|
||||
// Sincronizamos Sucursals
|
||||
//$user->stores()->sync($request->stores);
|
||||
|
||||
// Actualizamos foto de perfil
|
||||
if ($request->file('photo'))
|
||||
$avatarImageService = new AvatarImageService();
|
||||
|
||||
$avatarImageService->updateProfilePhoto($user, $request->file('photo'));
|
||||
|
||||
return response()->json(['success' => 'Se guardo correctamente los cambios.']);
|
||||
}
|
||||
|
||||
|
||||
public function userSettings(User $user)
|
||||
{
|
||||
return view('vuexy-admin::users.user-settings', compact('user'));
|
||||
}
|
||||
|
||||
public function generateAvatar(Request $request)
|
||||
{
|
||||
// Validación de entrada
|
||||
$request->validate([
|
||||
'name' => 'nullable|string',
|
||||
'color' => 'nullable|string|size:6',
|
||||
'background' => 'nullable|string|size:6',
|
||||
'size' => 'nullable|integer|min:20|max:1024'
|
||||
]);
|
||||
|
||||
$name = $request->get('name', 'NA');
|
||||
$color = $request->get('color', '7F9CF5');
|
||||
$background = $request->get('background', 'EBF4FF');
|
||||
$size = $request->get('size', 100);
|
||||
|
||||
return User::getAvatarImage($name, $color, $background, $size);
|
||||
|
||||
try {
|
||||
} catch (\Exception $e) {
|
||||
// String base64 de una imagen PNG transparente de 1x1 píxel
|
||||
$transparentBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==';
|
||||
|
||||
return response()->make(base64_decode($transparentBase64), 200, [
|
||||
'Content-Type' => 'image/png'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
234
Http/Controllers/UserController.php
Normal file
234
Http/Controllers/UserController.php
Normal file
@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\{Auth,DB,Validator};
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyAdmin\Services\AvatarImageService;
|
||||
use Koneko\VuexyAdmin\Queries\GenericQueryBuilder;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$bootstrapTableIndexConfig = [
|
||||
'table' => 'users',
|
||||
'columns' => [
|
||||
'users.id',
|
||||
'users.code',
|
||||
DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS full_name"),
|
||||
'users.email',
|
||||
'users.birth_date',
|
||||
'users.hire_date',
|
||||
'users.curp',
|
||||
'users.nss',
|
||||
'users.job_title',
|
||||
'users.profile_photo_path',
|
||||
DB::raw("(SELECT GROUP_CONCAT(roles.name SEPARATOR ';') as roles FROM model_has_roles INNER JOIN roles ON (model_has_roles.role_id = roles.id) WHERE model_has_roles.model_id = 1) as roles"),
|
||||
'users.is_partner',
|
||||
'users.is_employee',
|
||||
'users.is_prospect',
|
||||
'users.is_customer',
|
||||
'users.is_provider',
|
||||
'users.is_user',
|
||||
'users.status',
|
||||
DB::raw("CONCAT_WS(' ', created.name, created.last_name) AS creator"),
|
||||
'created.email AS creator_email',
|
||||
'users.created_at',
|
||||
'users.updated_at',
|
||||
],
|
||||
'joins' => [
|
||||
[
|
||||
'table' => 'users as parent',
|
||||
'first' => 'users.parent_id',
|
||||
'second' => 'parent.id',
|
||||
'type' => 'leftJoin',
|
||||
],
|
||||
[
|
||||
'table' => 'users as agent',
|
||||
'first' => 'users.agent_id',
|
||||
'second' => 'agent.id',
|
||||
'type' => 'leftJoin',
|
||||
],
|
||||
[
|
||||
'table' => 'users as created',
|
||||
'first' => 'users.created_by',
|
||||
'second' => 'created.id',
|
||||
'type' => 'leftJoin',
|
||||
],
|
||||
[
|
||||
'table' => 'sat_codigo_postal',
|
||||
'first' => 'users.domicilio_fiscal',
|
||||
'second' => 'sat_codigo_postal.c_codigo_postal',
|
||||
'type' => 'leftJoin',
|
||||
],
|
||||
[
|
||||
'table' => 'sat_estado',
|
||||
'first' => 'sat_codigo_postal.c_estado',
|
||||
'second' => 'sat_estado.c_estado',
|
||||
'type' => 'leftJoin',
|
||||
'and' => [
|
||||
'sat_estado.c_pais = "MEX"',
|
||||
],
|
||||
],
|
||||
[
|
||||
'table' => 'sat_localidad',
|
||||
'first' => 'sat_codigo_postal.c_localidad',
|
||||
'second' => 'sat_localidad.c_localidad',
|
||||
'type' => 'leftJoin',
|
||||
'and' => [
|
||||
'sat_codigo_postal.c_estado = sat_localidad.c_estado',
|
||||
],
|
||||
],
|
||||
[
|
||||
'table' => 'sat_municipio',
|
||||
'first' => 'sat_codigo_postal.c_municipio',
|
||||
'second' => 'sat_municipio.c_municipio',
|
||||
'type' => 'leftJoin',
|
||||
'and' => [
|
||||
'sat_codigo_postal.c_estado = sat_municipio.c_estado',
|
||||
],
|
||||
],
|
||||
[
|
||||
'table' => 'sat_regimen_fiscal',
|
||||
'first' => 'users.c_regimen_fiscal',
|
||||
'second' => 'sat_regimen_fiscal.c_regimen_fiscal',
|
||||
'type' => 'leftJoin',
|
||||
],
|
||||
[
|
||||
'table' => 'sat_uso_cfdi',
|
||||
'first' => 'users.c_uso_cfdi',
|
||||
'second' => 'sat_uso_cfdi.c_uso_cfdi',
|
||||
'type' => 'leftJoin',
|
||||
],
|
||||
],
|
||||
'filters' => [
|
||||
'search' => ['users.name', 'users.email', 'users.code', 'parent.name', 'created.name'],
|
||||
],
|
||||
'sort_column' => 'users.name',
|
||||
'default_sort_order' => 'asc',
|
||||
];
|
||||
|
||||
return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson();
|
||||
}
|
||||
|
||||
return view('vuexy-admin::users.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|max:255',
|
||||
'email' => 'required|max:255|unique:users',
|
||||
'photo' => 'nullable|mimes:jpg,jpeg,png|max:1024',
|
||||
'password' => 'required',
|
||||
]);
|
||||
|
||||
if ($validator->fails())
|
||||
return response()->json(['errors' => $validator->errors()->all()]);
|
||||
|
||||
// Preparamos los datos
|
||||
$user_request = array_merge_recursive($request->all(), [
|
||||
'remember_token' => Str::random(10),
|
||||
'created_by' => Auth::user()->id,
|
||||
]);
|
||||
|
||||
$user_request['password'] = bcrypt($request->password);
|
||||
|
||||
// Guardamos el nuevo usuario
|
||||
$user = User::create($user_request);
|
||||
|
||||
// Asignmos los permisos
|
||||
$user->assignRole($request->roles);
|
||||
|
||||
// Asignamos Sucursals
|
||||
//$user->stores()->attach($request->stores);
|
||||
|
||||
if ($request->file('photo')){
|
||||
$avatarImageService = new AvatarImageService();
|
||||
|
||||
$avatarImageService->updateProfilePhoto($user, $request->file('photo'));
|
||||
}
|
||||
|
||||
return response()->json(['success' => 'Se agrego correctamente el usuario']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param int User $user
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show(User $user)
|
||||
{
|
||||
return view('vuexy-admin::users.show', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param int User $user
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function updateAjax(Request $request, User $user)
|
||||
{
|
||||
// Validamos los datos
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|max:191',
|
||||
'email' => "required|max:191|unique:users,email," . $user->id,
|
||||
'photo' => 'nullable|mimes:jpg,jpeg,png|max:2048'
|
||||
]);
|
||||
|
||||
if ($validator->fails())
|
||||
return response()->json(['errors' => $validator->errors()->all()]);
|
||||
|
||||
// Preparamos los datos
|
||||
$user_request = $request->all();
|
||||
|
||||
if ($request->password) {
|
||||
$user_request['password'] = bcrypt($request->password);
|
||||
} else {
|
||||
unset($user_request['password']);
|
||||
}
|
||||
|
||||
// Guardamos los cambios
|
||||
$user->update($user_request);
|
||||
|
||||
// Sincronizamos Roles
|
||||
$user->syncRoles($request->roles);
|
||||
|
||||
// Sincronizamos Sucursals
|
||||
//$user->stores()->sync($request->stores);
|
||||
|
||||
// Actualizamos foto de perfil
|
||||
if ($request->file('photo'))
|
||||
$avatarImageService = new AvatarImageService();
|
||||
|
||||
$avatarImageService->updateProfilePhoto($user, $request->file('photo'));
|
||||
|
||||
return response()->json(['success' => 'Se guardo correctamente los cambios.']);
|
||||
}
|
||||
|
||||
|
||||
public function userSettings(User $user)
|
||||
{
|
||||
return view('vuexy-admin::users.user-settings', compact('user'));
|
||||
}
|
||||
|
||||
}
|
54
Http/Controllers/UserProfileController.php
Normal file
54
Http/Controllers/UserProfileController.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Koneko\VuexyAdmin\Services\AvatarInitialsService;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return view('vuexy-admin::profile.index');
|
||||
}
|
||||
|
||||
|
||||
public function generateAvatar(Request $request)
|
||||
{
|
||||
// Validación de entrada
|
||||
$request->validate([
|
||||
'name' => 'nullable|string',
|
||||
'color' => 'nullable|string|size:6',
|
||||
'background' => 'nullable|string|size:6',
|
||||
'size' => 'nullable|integer|min:20|max:1024'
|
||||
]);
|
||||
|
||||
$name = $request->get('name', 'NA');
|
||||
$color = $request->get('color', '7F9CF5');
|
||||
$background = $request->get('background', 'EBF4FF');
|
||||
$size = $request->get('size', 100);
|
||||
|
||||
$avatarService = new AvatarInitialsService();
|
||||
|
||||
try {
|
||||
return $avatarService->getAvatarImage($name, $color, $background, $size);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// String base64 de una imagen PNG transparente de 1x1 píxel
|
||||
$transparentBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==';
|
||||
|
||||
return response()->make(base64_decode($transparentBase64), 200, [
|
||||
'Content-Type' => 'image/png'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
37
Http/Middleware/AdminTemplateMiddleware.php
Normal file
37
Http/Middleware/AdminTemplateMiddleware.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Koneko\VuexyAdmin\Services\AdminTemplateService;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Koneko\VuexyAdmin\Services\VuexyAdminService;
|
||||
|
||||
class AdminTemplateMiddleware
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// Aplicar configuración de layout antes de que la vista se cargue
|
||||
if (str_contains($request->header('Accept'), 'text/html')) {
|
||||
$adminVars = app(AdminTemplateService::class)->getAdminVars();
|
||||
$vuexyAdminService = app(VuexyAdminService::class);
|
||||
|
||||
View::share([
|
||||
'_admin' => $adminVars,
|
||||
'vuexyMenu' => $vuexyAdminService->getMenu(),
|
||||
'vuexySearch' => $vuexyAdminService->getSearch(),
|
||||
'vuexyQuickLinks' => $vuexyAdminService->getQuickLinks(),
|
||||
'vuexyNotifications' => $vuexyAdminService->getNotifications(),
|
||||
'vuexyBreadcrumbs' => $vuexyAdminService->getBreadcrumbs(),
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
25
Listeners/ClearUserCache.php
Normal file
25
Listeners/ClearUserCache.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Listeners;
|
||||
|
||||
use Illuminate\Auth\Events\Logout;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Koneko\VuexyAdmin\Services\VuexyAdminService;
|
||||
|
||||
class ClearUserCache
|
||||
{
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Logout $event)
|
||||
{
|
||||
if ($event->user) {
|
||||
VuexyAdminService::clearUserMenuCache();
|
||||
VuexyAdminService::clearSearchMenuCache();
|
||||
VuexyAdminService::clearQuickLinksCache();
|
||||
VuexyAdminService::clearNotificationsCache();
|
||||
}
|
||||
}
|
||||
}
|
26
Listeners/HandleUserLogin.php
Normal file
26
Listeners/HandleUserLogin.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Listeners;
|
||||
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Koneko\VuexyAdmin\Models\UserLogin;
|
||||
|
||||
class HandleUserLogin
|
||||
{
|
||||
public function handle(Login $event)
|
||||
{
|
||||
// Guardar log en base de datos
|
||||
UserLogin::create([
|
||||
'user_id' => $event->user->id,
|
||||
'ip_address' => request()->ip(),
|
||||
'user_agent' => request()->header('User-Agent'),
|
||||
]);
|
||||
|
||||
// Actualizar el último login
|
||||
$event->user->update(['last_login_at' => now(), 'last_login_ip' => request()->ip()]);
|
||||
|
||||
// Enviar notificación de inicio de sesión
|
||||
//Mail::to($event->user->email)->send(new LoginNotification($event->user));
|
||||
}
|
||||
}
|
83
Livewire/AdminSettings/ApplicationSettings.php
Normal file
83
Livewire/AdminSettings/ApplicationSettings.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Koneko\VuexyAdmin\Services\AdminSettingsService;
|
||||
use Koneko\VuexyAdmin\Services\AdminTemplateService;
|
||||
|
||||
class ApplicationSettings extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
private $targetNotify = "#application-settings-card .notification-container";
|
||||
|
||||
public $admin_app_name,
|
||||
$admin_image_logo,
|
||||
$admin_image_logo_dark;
|
||||
|
||||
public $upload_image_logo,
|
||||
$upload_image_logo_dark;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadSettings();
|
||||
}
|
||||
|
||||
public function loadSettings($clearcache = false)
|
||||
{
|
||||
$this->upload_image_logo = null;
|
||||
$this->upload_image_logo_dark = null;
|
||||
|
||||
$adminTemplateService = app(AdminTemplateService::class);
|
||||
|
||||
if ($clearcache) {
|
||||
$adminTemplateService->clearAdminVarsCache();
|
||||
}
|
||||
|
||||
// Obtener los valores de las configuraciones de la base de datos
|
||||
$settings = $adminTemplateService->getAdminVars();
|
||||
|
||||
$this->admin_app_name = $settings['app_name'];
|
||||
$this->admin_image_logo = $settings['image_logo']['large'];
|
||||
$this->admin_image_logo_dark = $settings['image_logo']['large_dark'];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate([
|
||||
'admin_app_name' => 'required|string|max:255',
|
||||
'upload_image_logo' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
|
||||
'upload_image_logo_dark' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
|
||||
]);
|
||||
|
||||
$adminSettingsService = app(AdminSettingsService::class);
|
||||
|
||||
// Guardar título del App en configuraciones
|
||||
$adminSettingsService->updateSetting('admin_app_name', $this->admin_app_name);
|
||||
|
||||
// Procesar favicon si se ha cargado una imagen
|
||||
if ($this->upload_image_logo) {
|
||||
$adminSettingsService->processAndSaveImageLogo($this->upload_image_logo);
|
||||
}
|
||||
|
||||
if ($this->upload_image_logo_dark) {
|
||||
$adminSettingsService->processAndSaveImageLogo($this->upload_image_logo_dark, 'dark');
|
||||
}
|
||||
|
||||
$this->loadSettings(true);
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'success',
|
||||
message: 'Se han guardado los cambios en las configuraciones.'
|
||||
);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.admin-settings.application-settings');
|
||||
}
|
||||
}
|
84
Livewire/AdminSettings/GeneralSettings.php
Normal file
84
Livewire/AdminSettings/GeneralSettings.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Koneko\VuexyAdmin\Services\AdminSettingsService;
|
||||
use Koneko\VuexyAdmin\Services\AdminTemplateService;
|
||||
|
||||
class GeneralSettings extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
private $targetNotify = "#general-settings-card .notification-container";
|
||||
|
||||
public $admin_title;
|
||||
public $admin_favicon_16x16,
|
||||
$admin_favicon_76x76,
|
||||
$admin_favicon_120x120,
|
||||
$admin_favicon_152x152,
|
||||
$admin_favicon_180x180,
|
||||
$admin_favicon_192x192;
|
||||
|
||||
public $upload_image_favicon;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadSettings();
|
||||
}
|
||||
|
||||
public function loadSettings($clearcache = false)
|
||||
{
|
||||
$this->upload_image_favicon = null;
|
||||
|
||||
$adminTemplateService = app(AdminTemplateService::class);
|
||||
|
||||
if ($clearcache) {
|
||||
$adminTemplateService->clearAdminVarsCache();
|
||||
}
|
||||
|
||||
// Obtener los valores de las configuraciones de la base de datos
|
||||
$settings = $adminTemplateService->getAdminVars();
|
||||
|
||||
$this->admin_title = $settings['title'];
|
||||
$this->admin_favicon_16x16 = $settings['favicon']['16x16'];
|
||||
$this->admin_favicon_76x76 = $settings['favicon']['76x76'];
|
||||
$this->admin_favicon_120x120 = $settings['favicon']['120x120'];
|
||||
$this->admin_favicon_152x152 = $settings['favicon']['152x152'];
|
||||
$this->admin_favicon_180x180 = $settings['favicon']['180x180'];
|
||||
$this->admin_favicon_192x192 = $settings['favicon']['192x192'];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate([
|
||||
'admin_title' => 'required|string|max:255',
|
||||
'upload_image_favicon' => 'nullable|image|mimes:jpeg,png,jpg,svg,webp|max:20480',
|
||||
]);
|
||||
|
||||
$adminSettingsService = app(AdminSettingsService::class);
|
||||
|
||||
// Guardar título del sitio en configuraciones
|
||||
$adminSettingsService->updateSetting('admin_title', $this->admin_title);
|
||||
|
||||
// Procesar favicon si se ha cargado una imagen
|
||||
if ($this->upload_image_favicon) {
|
||||
$adminSettingsService->processAndSaveFavicon($this->upload_image_favicon);
|
||||
}
|
||||
|
||||
$this->loadSettings(true);
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'success',
|
||||
message: 'Se han guardado los cambios en las configuraciones.'
|
||||
);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.admin-settings.general-settings');
|
||||
}
|
||||
}
|
118
Livewire/AdminSettings/InterfaceSettings.php
Normal file
118
Livewire/AdminSettings/InterfaceSettings.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
|
||||
|
||||
use Livewire\Component;
|
||||
use Koneko\VuexyAdmin\Services\AdminTemplateService;
|
||||
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
|
||||
|
||||
class InterfaceSettings extends Component
|
||||
{
|
||||
private $targetNotify = "#interface-settings-card .notification-container";
|
||||
|
||||
public $vuexy_myLayout,
|
||||
$vuexy_myTheme,
|
||||
$vuexy_myStyle,
|
||||
$vuexy_hasCustomizer,
|
||||
$vuexy_displayCustomizer,
|
||||
$vuexy_contentLayout,
|
||||
$vuexy_navbarType,
|
||||
$vuexy_footerFixed,
|
||||
$vuexy_menuFixed,
|
||||
$vuexy_menuCollapsed,
|
||||
$vuexy_headerType,
|
||||
$vuexy_showDropdownOnHover,
|
||||
$vuexy_authViewMode,
|
||||
$vuexy_maxQuickLinks;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadSettings();
|
||||
}
|
||||
|
||||
|
||||
public function loadSettings()
|
||||
{
|
||||
$adminTemplateService = app(AdminTemplateService::class);
|
||||
|
||||
// Obtener los valores de las configuraciones de la base de datos
|
||||
$settings = $adminTemplateService->getVuexyCustomizerVars();
|
||||
|
||||
$this->vuexy_myLayout = $settings['myLayout'];
|
||||
$this->vuexy_myTheme = $settings['myTheme'];
|
||||
$this->vuexy_myStyle = $settings['myStyle'];
|
||||
$this->vuexy_hasCustomizer = $settings['hasCustomizer'];
|
||||
$this->vuexy_displayCustomizer = $settings['displayCustomizer'];
|
||||
$this->vuexy_contentLayout = $settings['contentLayout'];
|
||||
$this->vuexy_navbarType = $settings['navbarType'];
|
||||
$this->vuexy_footerFixed = $settings['footerFixed'];
|
||||
$this->vuexy_menuFixed = $settings['menuFixed'];
|
||||
$this->vuexy_menuCollapsed = $settings['menuCollapsed'];
|
||||
$this->vuexy_headerType = $settings['headerType'];
|
||||
$this->vuexy_showDropdownOnHover = $settings['showDropdownOnHover'];
|
||||
$this->vuexy_authViewMode = $settings['authViewMode'];
|
||||
$this->vuexy_maxQuickLinks = $settings['maxQuickLinks'];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate([
|
||||
'vuexy_maxQuickLinks' => 'required|integer|min:2|max:20',
|
||||
]);
|
||||
|
||||
$globalSettingsService = app(GlobalSettingsService::class);
|
||||
|
||||
// Guardar configuraciones
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.myLayout', $this->vuexy_myLayout);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.myTheme', $this->vuexy_myTheme);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.myStyle', $this->vuexy_myStyle);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.hasCustomizer', $this->vuexy_hasCustomizer);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.displayCustomizer', $this->vuexy_displayCustomizer);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.contentLayout', $this->vuexy_contentLayout);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.navbarType', $this->vuexy_navbarType);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.footerFixed', $this->vuexy_footerFixed);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.menuFixed', $this->vuexy_menuFixed);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.menuCollapsed', $this->vuexy_menuCollapsed);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.headerType', $this->vuexy_headerType);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.showDropdownOnHover', $this->vuexy_showDropdownOnHover);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.authViewMode', $this->vuexy_authViewMode);
|
||||
$globalSettingsService->updateSetting('config.vuexy.custom.maxQuickLinks', $this->vuexy_maxQuickLinks);
|
||||
|
||||
$globalSettingsService->clearSystemConfigCache();
|
||||
|
||||
// Refrescar el componente actual
|
||||
$this->dispatch('clearLocalStoregeTemplateCustomizer');
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'success',
|
||||
message: 'Se han guardado los cambios en las configuraciones.',
|
||||
deferReload: true
|
||||
);
|
||||
}
|
||||
|
||||
public function clearCustomConfig()
|
||||
{
|
||||
$globalSettingsService = app(GlobalSettingsService::class);
|
||||
|
||||
$globalSettingsService->clearVuexyConfig();
|
||||
|
||||
// Refrescar el componente actual
|
||||
$this->dispatch('clearLocalStoregeTemplateCustomizer');
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'success',
|
||||
message: 'Se han guardado los cambios en las configuraciones.',
|
||||
deferReload: true
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.admin-settings.interface-settings');
|
||||
}
|
||||
}
|
106
Livewire/AdminSettings/MailSenderResponseSettings.php
Normal file
106
Livewire/AdminSettings/MailSenderResponseSettings.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
|
||||
|
||||
use Livewire\Component;
|
||||
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
|
||||
|
||||
class MailSenderResponseSettings extends Component
|
||||
{
|
||||
private $targetNotify = "#mail-sender-response-settings-card .notification-container";
|
||||
|
||||
public $from_address,
|
||||
$from_name,
|
||||
$reply_to_method,
|
||||
$reply_to_email,
|
||||
$reply_to_name;
|
||||
|
||||
protected $listeners = ['saveMailSenderResponseSettings' => 'save'];
|
||||
|
||||
const REPLY_EMAIL_CREATOR = 1;
|
||||
const REPLY_EMAIL_SENDER = 2;
|
||||
const REPLY_EMAIL_CUSTOM = 3;
|
||||
|
||||
public $reply_email_options = [
|
||||
self::REPLY_EMAIL_CREATOR => 'Responder al creador del documento',
|
||||
self::REPLY_EMAIL_SENDER => 'Responder a quien envía el documento',
|
||||
self::REPLY_EMAIL_CUSTOM => 'Definir dirección de correo electrónico',
|
||||
];
|
||||
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadSettings();
|
||||
}
|
||||
|
||||
|
||||
public function loadSettings()
|
||||
{
|
||||
$globalSettingsService = app(GlobalSettingsService::class);
|
||||
|
||||
// Obtener los valores de las configuraciones de la base de datos
|
||||
$settings = $globalSettingsService->getMailSystemConfig();
|
||||
|
||||
$this->from_address = $settings['from']['address'];
|
||||
$this->from_name = $settings['from']['name'];
|
||||
$this->reply_to_method = $settings['reply_to']['method'];
|
||||
$this->reply_to_email = $settings['reply_to']['email'];
|
||||
$this->reply_to_name = $settings['reply_to']['name'];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate([
|
||||
'from_address' => 'required|email',
|
||||
'from_name' => 'required|string|max:255',
|
||||
'reply_to_method' => 'required|string|max:255',
|
||||
], [
|
||||
'from_address.required' => 'El campo de correo electrónico es obligatorio.',
|
||||
'from_address.email' => 'El formato del correo electrónico no es válido.',
|
||||
'from_name.required' => 'El nombre es obligatorio.',
|
||||
'from_name.string' => 'El nombre debe ser una cadena de texto.',
|
||||
'from_name.max' => 'El nombre no puede tener más de 255 caracteres.',
|
||||
'reply_to_method.required' => 'El método de respuesta es obligatorio.',
|
||||
'reply_to_method.string' => 'El método de respuesta debe ser una cadena de texto.',
|
||||
'reply_to_method.max' => 'El método de respuesta no puede tener más de 255 caracteres.',
|
||||
]);
|
||||
|
||||
if ($this->reply_to_method == self::REPLY_EMAIL_CUSTOM) {
|
||||
$this->validate([
|
||||
'reply_to_email' => ['required', 'email'],
|
||||
'reply_to_name' => ['required', 'string', 'max:255'],
|
||||
], [
|
||||
'reply_to_email.required' => 'El correo de respuesta es obligatorio.',
|
||||
'reply_to_email.email' => 'El formato del correo de respuesta no es válido.',
|
||||
'reply_to_name.required' => 'El nombre de respuesta es obligatorio.',
|
||||
'reply_to_name.string' => 'El nombre de respuesta debe ser una cadena de texto.',
|
||||
'reply_to_name.max' => 'El nombre de respuesta no puede tener más de 255 caracteres.',
|
||||
]);
|
||||
}
|
||||
|
||||
$globalSettingsService = app(GlobalSettingsService::class);
|
||||
|
||||
// Guardar título del App en configuraciones
|
||||
$globalSettingsService->updateSetting('mail.from.address', $this->from_address);
|
||||
$globalSettingsService->updateSetting('mail.from.name', $this->from_name);
|
||||
$globalSettingsService->updateSetting('mail.reply_to.method', $this->reply_to_method);
|
||||
$globalSettingsService->updateSetting('mail.reply_to.email', $this->reply_to_method == self::REPLY_EMAIL_CUSTOM ? $this->reply_to_email : '');
|
||||
$globalSettingsService->updateSetting('mail.reply_to.name', $this->reply_to_method == self::REPLY_EMAIL_CUSTOM ? $this->reply_to_name : '');
|
||||
|
||||
$globalSettingsService->clearMailSystemConfigCache();
|
||||
|
||||
$this->loadSettings();
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'success',
|
||||
message: 'Se han guardado los cambios en las configuraciones.',
|
||||
);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.admin-settings.mail-sender-response-settings');
|
||||
}
|
||||
}
|
175
Livewire/AdminSettings/MailSmtpSettings.php
Normal file
175
Livewire/AdminSettings/MailSmtpSettings.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\AdminSettings;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
|
||||
|
||||
|
||||
class MailSmtpSettings extends Component
|
||||
{
|
||||
private $targetNotify = "#mail-smtp-settings-card .notification-container";
|
||||
|
||||
public $change_smtp_settings,
|
||||
$host,
|
||||
$port,
|
||||
$encryption,
|
||||
$username,
|
||||
$password;
|
||||
|
||||
public $save_button_disabled;
|
||||
|
||||
protected $listeners = [
|
||||
'loadSettings',
|
||||
'testSmtpConnection',
|
||||
];
|
||||
|
||||
// the list of smtp_encryption values that can be stored in table
|
||||
const SMTP_ENCRYPTION_SSL = 'SSL';
|
||||
const SMTP_ENCRYPTION_TLS = 'TLS';
|
||||
const SMTP_ENCRYPTION_NONE = 'none';
|
||||
|
||||
public $encryption_options = [
|
||||
self::SMTP_ENCRYPTION_SSL => 'SSL (Secure Sockets Layer)',
|
||||
self::SMTP_ENCRYPTION_TLS => 'TLS (Transport Layer Security)',
|
||||
self::SMTP_ENCRYPTION_NONE => 'Sin encriptación (No recomendado)',
|
||||
];
|
||||
|
||||
public $rules = [
|
||||
[
|
||||
'host' => 'nullable|string|max:255',
|
||||
'port' => 'nullable|integer',
|
||||
'encryption' => 'nullable|string',
|
||||
'username' => 'nullable|string|max:255',
|
||||
'password' => 'nullable|string|max:255',
|
||||
],
|
||||
[
|
||||
'host.string' => 'El servidor SMTP debe ser una cadena de texto.',
|
||||
'host.max' => 'El servidor SMTP no puede exceder los 255 caracteres.',
|
||||
'port.integer' => 'El puerto SMTP debe ser un número entero.',
|
||||
'encryption.string' => 'El tipo de encriptación SMTP debe ser una cadena de texto.',
|
||||
'username.string' => 'El nombre de usuario SMTP debe ser una cadena de texto.',
|
||||
'username.max' => 'El nombre de usuario SMTP no puede exceder los 255 caracteres.',
|
||||
'password.string' => 'La contraseña SMTP debe ser una cadena de texto.',
|
||||
'password.max' => 'La contraseña SMTP no puede exceder los 255 caracteres.',
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadSettings();
|
||||
}
|
||||
|
||||
public function loadSettings()
|
||||
{
|
||||
$globalSettingsService = app(GlobalSettingsService::class);
|
||||
|
||||
// Obtener los valores de las configuraciones de la base de datos
|
||||
$settings = $globalSettingsService->getMailSystemConfig();
|
||||
|
||||
$this->change_smtp_settings = false;
|
||||
$this->save_button_disabled = true;
|
||||
|
||||
$this->host = $settings['mailers']['smtp']['host'];
|
||||
$this->port = $settings['mailers']['smtp']['port'];
|
||||
$this->encryption = $settings['mailers']['smtp']['encryption'];
|
||||
$this->username = $settings['mailers']['smtp']['username'];
|
||||
$this->password = null;
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate($this->rules[0]);
|
||||
|
||||
$globalSettingsService = app(GlobalSettingsService::class);
|
||||
|
||||
// Guardar título del App en configuraciones
|
||||
$globalSettingsService->updateSetting('mail.mailers.smtp.host', $this->host);
|
||||
$globalSettingsService->updateSetting('mail.mailers.smtp.port', $this->port);
|
||||
$globalSettingsService->updateSetting('mail.mailers.smtp.encryption', $this->encryption);
|
||||
$globalSettingsService->updateSetting('mail.mailers.smtp.username', $this->username);
|
||||
$globalSettingsService->updateSetting('mail.mailers.smtp.password', Crypt::encryptString($this->password));
|
||||
|
||||
$globalSettingsService->clearMailSystemConfigCache();
|
||||
|
||||
$this->loadSettings();
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'success',
|
||||
message: 'Se han guardado los cambios en las configuraciones.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testSmtpConnection()
|
||||
{
|
||||
// Validar los datos del formulario
|
||||
$this->validate($this->rules[0]);
|
||||
|
||||
try {
|
||||
// Verificar la conexión SMTP
|
||||
if ($this->validateSMTPConnection()) {
|
||||
$this->save_button_disabled = false;
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'success',
|
||||
message: 'Conexión SMTP exitosa, se guardó los cambios exitosamente.',
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Captura y maneja errores de conexión SMTP
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'danger',
|
||||
message: 'Error en la conexión SMTP: ' . $e->getMessage(),
|
||||
delay: 15000 // Timeout personalizado
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateSMTPConnection()
|
||||
{
|
||||
$dsn = sprintf(
|
||||
'smtp://%s:%s@%s:%s?encryption=%s',
|
||||
urlencode($this->username), // Codificar nombre de usuario
|
||||
urlencode($this->password), // Codificar contraseña
|
||||
$this->host, // Host SMTP
|
||||
$this->port, // Puerto SMTP
|
||||
$this->encryption // Encriptación (tls o ssl)
|
||||
);
|
||||
|
||||
// Crear el transportador usando el DSN
|
||||
$transport = Transport::fromDsn($dsn);
|
||||
|
||||
// Crear el mailer con el transportador personalizado
|
||||
$mailer = new Mailer($transport);
|
||||
|
||||
// Enviar un correo de prueba
|
||||
$email = (new Email())
|
||||
->from($this->username) // Dirección de correo del remitente
|
||||
->to(env('MAIL_SANDBOX')) // Dirección de correo de destino
|
||||
->subject(Config::get('app.name') . ' - Correo de prueba')
|
||||
->text('Este es un correo de prueba para verificar la conexión SMTP.');
|
||||
|
||||
// Enviar el correo
|
||||
$mailer->send($email);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.admin-settings.mail-smtp-settings');
|
||||
}
|
||||
}
|
212
Livewire/Cache/CacheFunctions.php
Normal file
212
Livewire/Cache/CacheFunctions.php
Normal file
@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Cache;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CacheFunctions extends Component
|
||||
{
|
||||
private $targetNotify = "#cache-functions-card .notification-container";
|
||||
|
||||
public $cacheCounts = [
|
||||
'general' => 0,
|
||||
'config' => 0,
|
||||
'routes' => 0,
|
||||
'views' => 0,
|
||||
'events' => 0,
|
||||
];
|
||||
|
||||
protected $listeners = [
|
||||
'reloadCacheFunctionsStatsEvent' => 'reloadCacheStats',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->reloadCacheStats(false);
|
||||
}
|
||||
|
||||
public function reloadCacheStats($notify = true)
|
||||
{
|
||||
$cacheDriver = config('cache.default'); // Obtiene el driver configurado para caché
|
||||
|
||||
// Caché General
|
||||
switch ($cacheDriver) {
|
||||
case 'memcached':
|
||||
try {
|
||||
$cacheStore = Cache::getStore()->getMemcached();
|
||||
$stats = $cacheStore->getStats();
|
||||
|
||||
$this->cacheCounts['general'] = array_sum(array_column($stats, 'curr_items')); // Total de claves en Memcached
|
||||
} catch (\Exception $e) {
|
||||
$this->cacheCounts['general'] = 'Error obteniendo datos de Memcached';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'redis':
|
||||
try {
|
||||
$prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
|
||||
$keys = Redis::connection('cache')->keys($prefix . '*');
|
||||
|
||||
$this->cacheCounts['general'] = count($keys); // Total de claves en Redis
|
||||
} catch (\Exception $e) {
|
||||
$this->cacheCounts['general'] = 'Error obteniendo datos de Redis';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
try {
|
||||
$this->cacheCounts['general'] = DB::table('cache')->count(); // Total de registros en la tabla de caché
|
||||
} catch (\Exception $e) {
|
||||
$this->cacheCounts['general'] = 'Error obteniendo datos de la base de datos';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
try {
|
||||
$cachePath = config('cache.stores.file.path');
|
||||
$files = glob($cachePath . '/*');
|
||||
|
||||
$this->cacheCounts['general'] = count($files);
|
||||
} catch (\Exception $e) {
|
||||
$this->cacheCounts['general'] = 'Error obteniendo datos de archivos';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->cacheCounts['general'] = 'Driver de caché no soportado';
|
||||
}
|
||||
|
||||
// Configuración
|
||||
$this->cacheCounts['config'] = file_exists(base_path('bootstrap/cache/config.php')) ? 1 : 0;
|
||||
|
||||
// Rutas
|
||||
$this->cacheCounts['routes'] = count(glob(base_path('bootstrap/cache/routes-*.php'))) > 0 ? 1 : 0;
|
||||
|
||||
// Vistas
|
||||
$this->cacheCounts['views'] = count(glob(storage_path('framework/views/*')));
|
||||
|
||||
// Configuración
|
||||
$this->cacheCounts['events'] = file_exists(base_path('bootstrap/cache/events.php')) ? 1 : 0;
|
||||
|
||||
if ($notify) {
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: 'success',
|
||||
message: 'Se han recargado los estadísticos de caché.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function clearLaravelCache()
|
||||
{
|
||||
Artisan::call('cache:clear');
|
||||
|
||||
sleep(1);
|
||||
|
||||
$this->response('Se han limpiado las cachés de la aplicación.', 'warning');
|
||||
}
|
||||
|
||||
public function clearConfigCache()
|
||||
{
|
||||
Artisan::call('config:clear');
|
||||
|
||||
$this->response('Se ha limpiado la cache de la configuración de Laravel.', 'warning');
|
||||
}
|
||||
|
||||
public function configCache()
|
||||
{
|
||||
Artisan::call('config:cache');
|
||||
}
|
||||
|
||||
public function clearRouteCache()
|
||||
{
|
||||
Artisan::call('route:clear');
|
||||
|
||||
$this->response('Se han limpiado las rutas de Laravel.', 'warning');
|
||||
}
|
||||
|
||||
public function cacheRoutes()
|
||||
{
|
||||
Artisan::call('route:cache');
|
||||
}
|
||||
|
||||
public function clearViewCache()
|
||||
{
|
||||
Artisan::call('view:clear');
|
||||
|
||||
$this->response('Se han limpiado las vistas de Laravel.', 'warning');
|
||||
}
|
||||
|
||||
public function cacheViews()
|
||||
{
|
||||
Artisan::call('view:cache');
|
||||
|
||||
$this->response('Se han cacheado las vistas de Laravel.');
|
||||
}
|
||||
|
||||
public function clearEventCache()
|
||||
{
|
||||
Artisan::call('event:clear');
|
||||
|
||||
$this->response('Se han limpiado los eventos de Laravel.', 'warning');
|
||||
}
|
||||
|
||||
public function cacheEvents()
|
||||
{
|
||||
Artisan::call('event:cache');
|
||||
|
||||
$this->response('Se han cacheado los eventos de Laravel.');
|
||||
}
|
||||
|
||||
public function optimizeClear()
|
||||
{
|
||||
Artisan::call('optimize:clear');
|
||||
|
||||
$this->response('Se han optimizado todos los cachés de Laravel.');
|
||||
}
|
||||
|
||||
public function resetPermissionCache()
|
||||
{
|
||||
Artisan::call('permission:cache-reset');
|
||||
|
||||
$this->response('Se han limpiado los cachés de permisos.', 'warning');
|
||||
}
|
||||
|
||||
public function clearResetTokens()
|
||||
{
|
||||
Artisan::call('auth:clear-resets');
|
||||
|
||||
$this->response('Se han limpiado los tokens de reseteo de contraseña.', 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera una respuesta estandarizada.
|
||||
*/
|
||||
private function response(string $message, string $type = 'success'): void
|
||||
{
|
||||
$this->reloadCacheStats(false);
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $type,
|
||||
message: $message,
|
||||
);
|
||||
|
||||
$this->dispatch('reloadCacheStatsEvent', notify: false);
|
||||
$this->dispatch('reloadSessionStatsEvent', notify: false);
|
||||
$this->dispatch('reloadRedisStatsEvent', notify: false);
|
||||
$this->dispatch('reloadMemcachedStatsEvent', notify: false);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.cache.cache-functions');
|
||||
}
|
||||
}
|
65
Livewire/Cache/CacheStats.php
Normal file
65
Livewire/Cache/CacheStats.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Cache;
|
||||
|
||||
use Livewire\Component;
|
||||
use Koneko\VuexyAdmin\Services\CacheConfigService;
|
||||
use Koneko\VuexyAdmin\Services\CacheManagerService;
|
||||
|
||||
class CacheStats extends Component
|
||||
{
|
||||
private $targetNotify = "#cache-stats-card .notification-container";
|
||||
|
||||
public $cacheConfig = [];
|
||||
public $cacheStats = [];
|
||||
|
||||
protected $listeners = ['reloadCacheStatsEvent' => 'reloadCacheStats'];
|
||||
|
||||
public function mount(CacheConfigService $cacheConfigService)
|
||||
{
|
||||
$this->cacheConfig = $cacheConfigService->getConfig();
|
||||
|
||||
$this->reloadCacheStats(false);
|
||||
}
|
||||
|
||||
public function reloadCacheStats($notify = true)
|
||||
{
|
||||
$cacheManagerService = new CacheManagerService();
|
||||
|
||||
$this->cacheStats = $cacheManagerService->getCacheStats();
|
||||
|
||||
if ($notify) {
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $this->cacheStats['status'],
|
||||
message: $this->cacheStats['message']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearCache()
|
||||
{
|
||||
$cacheManagerService = new CacheManagerService();
|
||||
|
||||
$message = $cacheManagerService->clearCache();
|
||||
|
||||
$this->reloadCacheStats(false);
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $message['status'],
|
||||
message: $message['message'],
|
||||
);
|
||||
|
||||
$this->dispatch('reloadRedisStatsEvent', notify: false);
|
||||
$this->dispatch('reloadMemcachedStatsEvent', notify: false);
|
||||
$this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.cache.cache-stats');
|
||||
}
|
||||
}
|
64
Livewire/Cache/MemcachedStats.php
Normal file
64
Livewire/Cache/MemcachedStats.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Cache;
|
||||
|
||||
use Livewire\Component;
|
||||
use Koneko\VuexyAdmin\Services\CacheManagerService;
|
||||
|
||||
class MemcachedStats extends Component
|
||||
{
|
||||
private $driver = 'memcached';
|
||||
private $targetNotify = "#memcached-stats-card .notification-container";
|
||||
|
||||
public $memcachedStats = [];
|
||||
|
||||
protected $listeners = ['reloadMemcachedStatsEvent' => 'reloadCacheStats'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->reloadCacheStats(false);
|
||||
}
|
||||
|
||||
public function reloadCacheStats($notify = true)
|
||||
{
|
||||
$cacheManagerService = new CacheManagerService($this->driver);
|
||||
|
||||
$memcachedStats = $cacheManagerService->getMemcachedStats();
|
||||
|
||||
$this->memcachedStats = $memcachedStats['info'];
|
||||
|
||||
if ($notify) {
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $memcachedStats['status'],
|
||||
message: $memcachedStats['message']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearCache()
|
||||
{
|
||||
$cacheManagerService = new CacheManagerService($this->driver);
|
||||
|
||||
$message = $cacheManagerService->clearCache();
|
||||
|
||||
$this->reloadCacheStats(false);
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $message['status'],
|
||||
message: $message['message'],
|
||||
);
|
||||
|
||||
$this->dispatch('reloadCacheStatsEvent', notify: false);
|
||||
$this->dispatch('reloadSessionStatsEvent', notify: false);
|
||||
$this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.cache.memcached-stats');
|
||||
}
|
||||
}
|
64
Livewire/Cache/RedisStats.php
Normal file
64
Livewire/Cache/RedisStats.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Cache;
|
||||
|
||||
use Livewire\Component;
|
||||
use Koneko\VuexyAdmin\Services\CacheManagerService;
|
||||
|
||||
class RedisStats extends Component
|
||||
{
|
||||
private $driver = 'redis';
|
||||
private $targetNotify = "#redis-stats-card .notification-container";
|
||||
|
||||
public $redisStats = [];
|
||||
|
||||
protected $listeners = ['reloadRedisStatsEvent' => 'reloadCacheStats'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->reloadCacheStats(false);
|
||||
}
|
||||
|
||||
public function reloadCacheStats($notify = true)
|
||||
{
|
||||
$cacheManagerService = new CacheManagerService($this->driver);
|
||||
|
||||
$redisStats = $cacheManagerService->getRedisStats();
|
||||
|
||||
$this->redisStats = $redisStats['info'];
|
||||
|
||||
if ($notify) {
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $redisStats['status'],
|
||||
message: $redisStats['message']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearCache()
|
||||
{
|
||||
$cacheManagerService = new CacheManagerService($this->driver);
|
||||
|
||||
$message = $cacheManagerService->clearCache();
|
||||
|
||||
$this->reloadCacheStats(false);
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $message['status'],
|
||||
message: $message['message'],
|
||||
);
|
||||
|
||||
$this->dispatch('reloadCacheStatsEvent', notify: false);
|
||||
$this->dispatch('reloadSessionStatsEvent', notify: false);
|
||||
$this->dispatch('reloadCacheFunctionsStatsEvent', notify: false);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.cache.redis-stats');
|
||||
}
|
||||
}
|
63
Livewire/Cache/SessionStats.php
Normal file
63
Livewire/Cache/SessionStats.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Cache;
|
||||
|
||||
use Livewire\Component;
|
||||
use Koneko\VuexyAdmin\Services\CacheConfigService;
|
||||
use Koneko\VuexyAdmin\Services\SessionManagerService;
|
||||
|
||||
class SessionStats extends Component
|
||||
{
|
||||
private $targetNotify = "#session-stats-card .notification-container";
|
||||
|
||||
public $cacheConfig = [];
|
||||
public $sessionStats = [];
|
||||
|
||||
protected $listeners = ['reloadSessionStatsEvent' => 'reloadSessionStats'];
|
||||
|
||||
public function mount(CacheConfigService $cacheConfigService)
|
||||
{
|
||||
$this->cacheConfig = $cacheConfigService->getConfig();
|
||||
$this->reloadSessionStats(false);
|
||||
}
|
||||
|
||||
public function reloadSessionStats($notify = true)
|
||||
{
|
||||
$sessionManagerService = new SessionManagerService();
|
||||
|
||||
$this->sessionStats = $sessionManagerService->getSessionStats();
|
||||
|
||||
if ($notify) {
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $this->sessionStats['status'],
|
||||
message: $this->sessionStats['message']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearSessions()
|
||||
{
|
||||
$sessionManagerService = new SessionManagerService();
|
||||
|
||||
$message = $sessionManagerService->clearSessions();
|
||||
|
||||
$this->reloadSessionStats(false);
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $this->targetNotify,
|
||||
type: $message['status'],
|
||||
message: $message['message'],
|
||||
);
|
||||
|
||||
$this->dispatch('reloadRedisStatsEvent', notify: false);
|
||||
$this->dispatch('reloadMemcachedStatsEvent', notify: false);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.cache.session-stats');
|
||||
}
|
||||
}
|
515
Livewire/Form/AbstractFormComponent.php
Normal file
515
Livewire/Form/AbstractFormComponent.php
Normal file
@ -0,0 +1,515 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Form;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Class AbstractFormComponent
|
||||
*
|
||||
* Clase base y abstracta para la creación de formularios con Livewire.
|
||||
* Proporciona métodos y un flujo general para manejar operaciones CRUD
|
||||
* (creación, edición y eliminación), validaciones, notificaciones y
|
||||
* administración de errores en un entorno transaccional.
|
||||
*
|
||||
* @package Koneko\VuexyAdmin\Livewire\Form
|
||||
*/
|
||||
abstract class AbstractFormComponent extends Component
|
||||
{
|
||||
/**
|
||||
* Identificador único del formulario, útil para distinguir múltiples instancias.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $uniqueId;
|
||||
|
||||
/**
|
||||
* Modo actual del formulario: puede ser 'create', 'edit' o 'delete'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $mode;
|
||||
|
||||
/**
|
||||
* Texto que se mostrará en el botón de envío. Se adapta
|
||||
* automáticamente en función del modo actual (crear, editar o eliminar).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $btnSubmitText;
|
||||
|
||||
/**
|
||||
* ID del registro que se está editando o eliminando.
|
||||
* Si el formulario está en modo 'create', puede ser null.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Nombre de la etiqueta para generar Componentes
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $tagName;
|
||||
|
||||
/**
|
||||
* Nombre de la columna que contiene el nombre del registro.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $columnNameLabel;
|
||||
|
||||
/**
|
||||
* Nombre singular del modelo
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $singularName;
|
||||
|
||||
/*
|
||||
* Nombre del identificador del Canvas
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $offcanvasId;
|
||||
|
||||
/*
|
||||
* Nombre del identificador del Form
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $formId;
|
||||
|
||||
// ======================================================================
|
||||
// MÉTODOS ABSTRACTOS
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Retorna la clase (namespace) del modelo Eloquent asociado al formulario.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function model(): string;
|
||||
|
||||
/**
|
||||
* Retorna las reglas de validación de forma dinámica, dependiendo del modo del formulario.
|
||||
*
|
||||
* @param string $mode El modo actual del formulario (por ejemplo, 'create', 'edit' o 'delete').
|
||||
* @return array Reglas de validación (similares a las usadas en un Request de Laravel).
|
||||
*/
|
||||
abstract protected function dynamicRules(string $mode): array;
|
||||
|
||||
/**
|
||||
* Inicializa los datos del formulario con base en el registro (si existe)
|
||||
* y en el modo actual. Útil para prellenar campos en modo 'edit'.
|
||||
*
|
||||
* @param mixed $record El registro encontrado, o null si se crea uno nuevo.
|
||||
* @param string $mode El modo actual del formulario.
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function initializeFormData(mixed $record, string $mode): void;
|
||||
|
||||
/**
|
||||
* Prepara los datos ya validados para ser guardados en base de datos.
|
||||
* Permite, por ejemplo, castear valores o limpiar ciertos campos.
|
||||
*
|
||||
* @param array $validatedData Datos que ya pasaron la validación.
|
||||
* @return array Datos listos para el almacenamiento (por ejemplo, en create o update).
|
||||
*/
|
||||
abstract protected function prepareData(array $validatedData): array;
|
||||
|
||||
/**
|
||||
* Define los contenedores de destino para las notificaciones.
|
||||
*
|
||||
* Retorna un array con keys como 'form', 'index', etc., y sus
|
||||
* valores deben ser selectores o identificadores en la vista, donde
|
||||
* se inyectarán las notificaciones.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function targetNotifies(): array;
|
||||
|
||||
/**
|
||||
* Retorna la ruta de la vista Blade correspondiente a este formulario.
|
||||
*
|
||||
* Por ejemplo: 'package::livewire.some-form'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function viewPath(): string;
|
||||
|
||||
// ======================================================================
|
||||
// MÉTODOS DE VALIDACIÓN
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Retorna un array que define nombres de atributos personalizados para los mensajes de validación.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function attributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna un array con mensajes de validación personalizados.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function messages(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// INICIALIZACIÓN Y CICLO DE VIDA
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Método que se ejecuta al montar (instanciar) el componente Livewire.
|
||||
* Inicializa propiedades clave como el $mode, $id, $uniqueId, el texto
|
||||
* del botón de envío, y carga datos del registro si no es un 'create'.
|
||||
*
|
||||
* @param string $mode Modo del formulario: 'create', 'edit' o 'delete'.
|
||||
* @param int|null $id ID del registro a editar/eliminar (o null para crear).
|
||||
* @return void
|
||||
*/
|
||||
public function mount(string $mode = 'create', mixed $id = null): void
|
||||
{
|
||||
$this->uniqueId = uniqid();
|
||||
$this->mode = $mode;
|
||||
$this->id = $id;
|
||||
|
||||
$model = new ($this->model());
|
||||
|
||||
$this->tagName = $model->tagName;
|
||||
$this->columnNameLabel = $model->columnNameLabel;
|
||||
$this->singularName = $model->singularName;
|
||||
$this->formId = Str::camel($model->tagName) .'Form';
|
||||
|
||||
$this->setBtnSubmitText();
|
||||
|
||||
if ($this->mode !== 'create' && $this->id) {
|
||||
// Si no es modo 'create', cargamos el registro desde la BD
|
||||
$record = $this->model()::findOrFail($this->id);
|
||||
|
||||
$this->initializeFormData($record, $mode);
|
||||
|
||||
} else {
|
||||
// Modo 'create', o sin ID: iniciamos datos vacíos
|
||||
$this->initializeFormData(null, $mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configura el texto del botón principal de envío, basado en la propiedad $mode.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setBtnSubmitText(): void
|
||||
{
|
||||
$this->btnSubmitText = match ($this->mode) {
|
||||
'create' => 'Crear ' . $this->singularName(),
|
||||
'edit' => 'Guardar cambios',
|
||||
'delete' => 'Eliminar ' . $this->singularName(),
|
||||
default => 'Enviar'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna el "singularName" definido en el modelo asociado.
|
||||
* Permite también decidir si se devuelve con la primera letra en mayúscula
|
||||
* o en minúscula.
|
||||
*
|
||||
* @param string $type Puede ser 'uppercase' o 'lowercase'. Por defecto, 'lowercase'.
|
||||
* @return string Nombre en singular del modelo, formateado.
|
||||
*/
|
||||
private function singularName($type = 'lowercase'): string
|
||||
{
|
||||
/** @var Model $model */
|
||||
$model = new ($this->model());
|
||||
|
||||
return $type === 'uppercase'
|
||||
? ucfirst($model->singularName)
|
||||
: lcfirst($model->singularName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Método del ciclo de vida de Livewire que se llama en cada hidratación.
|
||||
* Puedes disparar eventos o manejar lógica que suceda en cada request
|
||||
* una vez que Livewire 'rehidrate' el componente en el servidor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function hydrate(): void
|
||||
{
|
||||
$this->dispatch($this->dispatches()['on-hydrate']);
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// OPERACIONES CRUD
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Método principal de envío del formulario (submit). Gestiona los flujos
|
||||
* de crear, editar o eliminar un registro dentro de una transacción de BD.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function onSubmit(): void
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
if ($this->mode === 'delete') {
|
||||
$this->delete();
|
||||
} else {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
DB::rollBack();
|
||||
$this->handleValidationException($e);
|
||||
|
||||
} catch (QueryException $e) {
|
||||
DB::rollBack();
|
||||
$this->handleDatabaseException($e);
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
DB::rollBack();
|
||||
$this->handleException('danger', 'Registro no encontrado.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack();
|
||||
$this->handleException('danger', 'Error al eliminar el registro: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea o actualiza un registro en la base de datos,
|
||||
* aplicando validaciones y llamadas a hooks antes y después de guardar.
|
||||
*
|
||||
* @return void
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function save(): void
|
||||
{
|
||||
// Validamos los datos, con posibles atributos y mensajes personalizados
|
||||
$validatedData = $this->validate(
|
||||
$this->dynamicRules($this->mode),
|
||||
$this->messages(),
|
||||
$this->attributes()
|
||||
);
|
||||
|
||||
// Hook previo (por referencia)
|
||||
$this->beforeSave($validatedData);
|
||||
|
||||
// Ajustamos/convertimos los datos finales
|
||||
$data = $this->prepareData($validatedData);
|
||||
$record = $this->model()::updateOrCreate(['id' => $this->id], $data);
|
||||
|
||||
// Hook posterior
|
||||
$this->afterSave($record);
|
||||
|
||||
// Notificamos éxito
|
||||
$this->handleSuccess('success', $this->singularName('uppercase') . " guardado correctamente.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina un registro de la base de datos (modo 'delete'),
|
||||
* aplicando validaciones y hooks antes y después de la eliminación.
|
||||
*
|
||||
* @return void
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function delete(): void
|
||||
{
|
||||
$this->validate($this->dynamicRules('delete', $this->messages(), $this->attributes()));
|
||||
|
||||
$record = $this->model()::findOrFail($this->id);
|
||||
|
||||
// Hook antes de la eliminación
|
||||
$this->beforeDelete($record);
|
||||
|
||||
$record->delete();
|
||||
|
||||
// Hook después de la eliminación
|
||||
$this->afterDelete($record);
|
||||
|
||||
$this->handleSuccess('warning', $this->singularName('uppercase') . " eliminado.");
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// HOOKS DE ACCIONES
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Hook que se ejecuta antes de guardar o actualizar un registro.
|
||||
* Puede usarse para ajustar o limpiar datos antes de la operación en base de datos.
|
||||
*
|
||||
* @param array $data Datos validados que se van a guardar.
|
||||
* Se pasa por referencia para permitir cambios.
|
||||
* @return void
|
||||
*/
|
||||
protected function beforeSave(array &$data): void {}
|
||||
|
||||
/**
|
||||
* Hook que se ejecuta después de guardar o actualizar un registro.
|
||||
* Puede usarse para acciones como disparar eventos, notificaciones a otros sistemas, etc.
|
||||
*
|
||||
* @param mixed $record Instancia del modelo recién creado o actualizado.
|
||||
* @return void
|
||||
*/
|
||||
protected function afterSave($record): void {}
|
||||
|
||||
/**
|
||||
* Hook que se ejecuta antes de eliminar un registro.
|
||||
* Puede emplearse para validaciones adicionales o limpieza de datos relacionados.
|
||||
*
|
||||
* @param mixed $record Instancia del modelo que se eliminará.
|
||||
* @return void
|
||||
*/
|
||||
protected function beforeDelete($record): void {}
|
||||
|
||||
/**
|
||||
* Hook que se ejecuta después de eliminar un registro.
|
||||
* Útil para operaciones finales, como remover archivos relacionados o
|
||||
* disparar un evento de "elemento eliminado".
|
||||
*
|
||||
* @param mixed $record Instancia del modelo que se acaba de eliminar.
|
||||
* @return void
|
||||
*/
|
||||
protected function afterDelete($record): void {}
|
||||
|
||||
// ======================================================================
|
||||
// MANEJO DE VALIDACIONES Y ERRORES
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Maneja las excepciones de validación (ValidationException).
|
||||
* Asigna los errores al error bag de Livewire y muestra notificaciones.
|
||||
*
|
||||
* @param ValidationException $e Excepción de validación.
|
||||
* @return void
|
||||
*/
|
||||
protected function handleValidationException(ValidationException $e): void
|
||||
{
|
||||
$this->setErrorBag($e->validator->errors());
|
||||
$this->handleException('danger', 'Error en la validación de los datos.');
|
||||
$this->dispatch($this->dispatches()['on-failed-validation']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja las excepciones de base de datos (QueryException).
|
||||
* Incluye casos especiales para claves foráneas y duplicadas.
|
||||
*
|
||||
* @param QueryException $e Excepción de consulta a la base de datos.
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDatabaseException(QueryException $e): void
|
||||
{
|
||||
$errorMessage = match ($e->errorInfo[1]) {
|
||||
1452 => "Una clave foránea no es válida.",
|
||||
1062 => $this->extractDuplicateField($e->getMessage()),
|
||||
1451 => "No se puede eliminar el registro porque está en uso.",
|
||||
default => env('APP_DEBUG') ? $e->getMessage() : "Error inesperado en la base de datos.",
|
||||
};
|
||||
|
||||
$this->handleException('danger', $errorMessage, 'form', 120000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja excepciones o errores generales, mostrando una notificación al usuario.
|
||||
*
|
||||
* @param string $type Tipo de notificación (por ejemplo, 'success', 'warning', 'danger').
|
||||
* @param string $message Mensaje que se mostrará en la notificación.
|
||||
* @param string $target Objetivo/área donde se mostrará la notificación ('form', 'index', etc.).
|
||||
* @param int $delay Tiempo en milisegundos que la notificación permanecerá visible.
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException($type, $message, $target = 'form', $delay = 9000): void
|
||||
{
|
||||
$this->dispatchNotification($type, $message, $target, $delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae el campo duplicado de un mensaje de error MySQL, para mostrar un mensaje amigable.
|
||||
*
|
||||
* @param string $errorMessage Mensaje de error completo de la base de datos.
|
||||
* @return string Mensaje simplificado indicando cuál campo está duplicado.
|
||||
*/
|
||||
private function extractDuplicateField($errorMessage): string
|
||||
{
|
||||
preg_match("/for key 'unique_(.*?)'/", $errorMessage, $matches);
|
||||
|
||||
return isset($matches[1])
|
||||
? "El valor ingresado para '" . str_replace('_', ' ', $matches[1]) . "' ya está en uso."
|
||||
: "Ya existe un registro con este valor.";
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// NOTIFICACIONES Y REDIRECCIONAMIENTOS
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Maneja el flujo de notificación y redirección cuando una operación
|
||||
* (guardar, eliminar) finaliza satisfactoriamente.
|
||||
*
|
||||
* @param string $type Tipo de notificación ('success', 'warning', etc.).
|
||||
* @param string $message Mensaje a mostrar.
|
||||
* @return void
|
||||
*/
|
||||
protected function handleSuccess($type, $message): void
|
||||
{
|
||||
$this->dispatchNotification($type, $message, 'index');
|
||||
$this->redirectRoute($this->getRedirectRoute());
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía una notificación al navegador (mediante eventos de Livewire)
|
||||
* indicando el tipo, el mensaje y el destino donde debe visualizarse.
|
||||
*
|
||||
* @param string $type Tipo de notificación (success, danger, etc.).
|
||||
* @param string $message Mensaje de la notificación.
|
||||
* @param string $target Destino para mostrarla ('form', 'index', etc.).
|
||||
* @param int $delay Duración de la notificación en milisegundos.
|
||||
* @return void
|
||||
*/
|
||||
protected function dispatchNotification($type, $message, $target = 'form', $delay = 9000): void
|
||||
{
|
||||
$this->dispatch(
|
||||
$target == 'index' ? 'store-notification' : 'notification',
|
||||
target: $target === 'index' ? $this->targetNotifies()['index'] : $this->targetNotifies()['form'],
|
||||
type: $type,
|
||||
message: $message,
|
||||
delay: $delay
|
||||
);
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// RENDERIZACIÓN
|
||||
// ======================================================================
|
||||
|
||||
/**
|
||||
* Renderiza la vista Blade asociada a este componente.
|
||||
* Retorna un objeto Illuminate\View\View.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view($this->viewPath());
|
||||
}
|
||||
}
|
667
Livewire/Form/AbstractFormOffCanvasComponent.php
Normal file
667
Livewire/Form/AbstractFormOffCanvasComponent.php
Normal file
@ -0,0 +1,667 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Form;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Clase base abstracta para manejar formularios de tipo Off-Canvas con Livewire.
|
||||
*
|
||||
* Esta clase proporciona métodos reutilizables para operaciones CRUD, validaciones dinámicas,
|
||||
* manejo de transacciones en base de datos y eventos de Livewire.
|
||||
*
|
||||
* @package Koneko\VuexyAdmin\Livewire\Form
|
||||
*/
|
||||
abstract class AbstractFormOffCanvasComponent extends Component
|
||||
{
|
||||
/**
|
||||
* Identificador único del formulario, usado para evitar conflictos en instancias múltiples.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $uniqueId;
|
||||
|
||||
/**
|
||||
* Modo actual del formulario: puede ser 'create', 'edit' o 'delete'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $mode;
|
||||
|
||||
/**
|
||||
* ID del registro que se está editando o eliminando.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Valores por defecto para los campos del formulario,
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $defaultValues;
|
||||
|
||||
/**
|
||||
* Nombre de la etiqueta para generar Componentes
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $tagName;
|
||||
|
||||
/**
|
||||
* Nombre de la columna que contiene el nombre del registro.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $columnNameLabel;
|
||||
|
||||
/**
|
||||
* Nombre singular del modelo
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $singularName;
|
||||
|
||||
/*
|
||||
* Nombre del identificador del Canvas
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $offcanvasId;
|
||||
|
||||
/*
|
||||
* Nombre del identificador del Form
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $formId;
|
||||
|
||||
/**
|
||||
* Campo que se debe enfocar cuando se abra el formulario.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $focusOnOpen;
|
||||
|
||||
/**
|
||||
* Indica si se desea confirmar la eliminación del registro.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $confirmDeletion = false;
|
||||
|
||||
/**
|
||||
* Indica si se ha producido un error de validación.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $validationError = false;
|
||||
|
||||
/*
|
||||
* Indica si se ha procesado correctamente el formulario.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $successProcess = false;
|
||||
|
||||
/**
|
||||
* Campos que deben ser casteados a tipos específicos.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [];
|
||||
|
||||
// ===================== MÉTODOS ABSTRACTOS =====================
|
||||
|
||||
/**
|
||||
* Define el modelo Eloquent asociado con el formulario.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function model(): string;
|
||||
|
||||
/**
|
||||
* Define los campos del formulario.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
abstract protected function fields(): array;
|
||||
|
||||
/**
|
||||
* Retorna los valores por defecto para los campos del formulario.
|
||||
*
|
||||
* @return array<string, mixed> Valores predeterminados.
|
||||
*/
|
||||
abstract protected function defaults(): array;
|
||||
|
||||
/**
|
||||
* Campo que se debe enfocar cuando se abra el formulario.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function focusOnOpen(): string;
|
||||
|
||||
/**
|
||||
* Define reglas de validación dinámicas según el modo del formulario.
|
||||
*
|
||||
* @param string $mode Modo actual del formulario ('create', 'edit', 'delete').
|
||||
* @return array<string, mixed> Reglas de validación.
|
||||
*/
|
||||
abstract protected function dynamicRules(string $mode): array;
|
||||
|
||||
/**
|
||||
* Devuelve las opciones que se mostrarán en los selectores del formulario.
|
||||
*
|
||||
* @return array<string, mixed> Opciones para los campos del formulario.
|
||||
*/
|
||||
abstract protected function options(): array;
|
||||
|
||||
/**
|
||||
* Retorna la ruta de la vista asociada al formulario.
|
||||
*
|
||||
* @return string Ruta de la vista Blade.
|
||||
*/
|
||||
abstract protected function viewPath(): string;
|
||||
|
||||
// ===================== VALIDACIONES =====================
|
||||
|
||||
protected function attributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function messages(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// ===================== INICIALIZACIÓN DEL COMPONENTE =====================
|
||||
|
||||
/**
|
||||
* Se ejecuta cuando el componente se monta por primera vez.
|
||||
*
|
||||
* Inicializa propiedades y carga datos iniciales.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->uniqueId = uniqid();
|
||||
|
||||
$model = new ($this->model());
|
||||
|
||||
$this->tagName = $model->tagName;
|
||||
$this->columnNameLabel = $model->columnNameLabel;
|
||||
$this->singularName = $model->singularName;
|
||||
$this->offcanvasId = 'offcanvas' . ucfirst(Str::camel($model->tagName));
|
||||
$this->formId = Str::camel($model->tagName) .'Form';
|
||||
$this->focusOnOpen = "{$this->focusOnOpen()}_{$this->uniqueId}";
|
||||
|
||||
$this->loadDefaults();
|
||||
$this->loadOptions();
|
||||
}
|
||||
|
||||
// ===================== INICIALIZACIÓN Y CONFIGURACIÓN =====================
|
||||
|
||||
/**
|
||||
* Devuelve los valores por defecto para los campos del formulario.
|
||||
*
|
||||
* @return array<string, mixed> Valores por defecto.
|
||||
*/
|
||||
private function loadDefaults(): void
|
||||
{
|
||||
$this->defaultValues = $this->defaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga las opciones necesarias para los campos del formulario.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function loadOptions(): void
|
||||
{
|
||||
foreach ($this->options() as $key => $value) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga los datos de un modelo específico en el formulario para su edición.
|
||||
*
|
||||
* @param int $id ID del registro a editar.
|
||||
* @return void
|
||||
*/
|
||||
public function loadFormModel(int $id): void
|
||||
{
|
||||
if ($this->loadData($id)) {
|
||||
$this->mode = 'edit';
|
||||
|
||||
$this->dispatch($this->getDispatche('refresh-offcanvas'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga el modelo para confirmar su eliminación.
|
||||
*
|
||||
* @param int $id ID del registro a eliminar.
|
||||
* @return void
|
||||
*/
|
||||
public function loadFormModelForDeletion(int $id): void
|
||||
{
|
||||
if ($this->loadData($id)) {
|
||||
$this->mode = 'delete';
|
||||
$this->confirmDeletion = false;
|
||||
|
||||
$this->dispatch($this->getDispatche('refresh-offcanvas'));
|
||||
}
|
||||
}
|
||||
|
||||
private function getDispatche(string $name): string
|
||||
{
|
||||
$model = new ($this->model());
|
||||
|
||||
$dispatches = [
|
||||
'refresh-offcanvas' => 'refresh-' . Str::kebab($model->tagName) . '-offcanvas',
|
||||
'reload-table' => 'reload-bt-' . Str::kebab($model->tagName) . 's',
|
||||
];
|
||||
|
||||
return $dispatches[$name] ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Carga los datos del modelo según el ID proporcionado.
|
||||
*
|
||||
* @param int $id ID del modelo.
|
||||
* @return bool True si los datos fueron cargados correctamente.
|
||||
*/
|
||||
protected function loadData(int $id): bool
|
||||
{
|
||||
$model = $this->model()::find($id);
|
||||
|
||||
if ($model) {
|
||||
$data = $model->only(['id', ...$this->fields()]);
|
||||
|
||||
$this->applyCasts($data);
|
||||
$this->fill($data);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===================== OPERACIONES CRUD =====================
|
||||
|
||||
/**
|
||||
* Método principal para enviar el formulario.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function onSubmit(): void
|
||||
{
|
||||
$this->successProcess = false;
|
||||
$this->validationError = false;
|
||||
|
||||
if(!$this->mode)
|
||||
$this->mode = 'create';
|
||||
|
||||
DB::beginTransaction(); // Iniciar transacción
|
||||
|
||||
try {
|
||||
if($this->mode === 'delete'){
|
||||
$this->delete();
|
||||
|
||||
}else{
|
||||
$this->save();
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
DB::rollBack();
|
||||
$this->handleValidationException($e);
|
||||
|
||||
} catch (QueryException $e) {
|
||||
DB::rollBack();
|
||||
$this->handleDatabaseException($e);
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
DB::rollBack();
|
||||
$this->handleException('danger', 'Registro no encontrado.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack(); // Revertir la transacción si ocurre un error
|
||||
$this->handleException('danger', 'Error al eliminar el registro: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda o actualiza un registro en la base de datos.
|
||||
*
|
||||
* @return void
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function save(): void
|
||||
{
|
||||
// Valida incluyendo atributos personalizados
|
||||
$validatedData = $this->validate(
|
||||
$this->dynamicRules($this->mode),
|
||||
$this->messages(),
|
||||
$this->attributes()
|
||||
);
|
||||
|
||||
$this->convertEmptyValuesToNull($validatedData);
|
||||
$this->applyCasts($validatedData);
|
||||
|
||||
$this->beforeSave($validatedData);
|
||||
$record = $this->model()::updateOrCreate(['id' => $this->id], $validatedData);
|
||||
$this->afterSave($record);
|
||||
|
||||
$this->handleSuccess('success', ucfirst($this->singularName) . " guardado correctamente.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina un registro en la base de datos.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function delete(): void
|
||||
{
|
||||
$this->validate($this->dynamicRules(
|
||||
'delete',
|
||||
$this->messages(),
|
||||
$this->attributes()
|
||||
));
|
||||
|
||||
$record = $this->model()::findOrFail($this->id);
|
||||
|
||||
$this->beforeDelete($record);
|
||||
$record->delete();
|
||||
$this->afterDelete($record);
|
||||
|
||||
$this->handleSuccess('warning', ucfirst($this->singularName) . " eliminado.");
|
||||
}
|
||||
|
||||
// ===================== HOOKS DE ACCIONES CRUD =====================
|
||||
|
||||
/**
|
||||
* Hook que se ejecuta antes de guardar datos en la base de datos.
|
||||
*
|
||||
* Este método permite realizar modificaciones o preparar los datos antes de ser validados
|
||||
* y almacenados. Es útil para formatear datos, agregar valores calculados o realizar
|
||||
* operaciones previas a la persistencia.
|
||||
*
|
||||
* @param array $data Datos validados que se almacenarán. Se pasan por referencia,
|
||||
* por lo que cualquier cambio aquí afectará directamente los datos guardados.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function beforeSave(array &$data): void {}
|
||||
|
||||
/**
|
||||
* Hook que se ejecuta después de guardar o actualizar un registro en la base de datos.
|
||||
*
|
||||
* Ideal para ejecutar tareas posteriores al guardado, como enviar notificaciones,
|
||||
* registrar auditorías o realizar acciones en otros modelos relacionados.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $record El modelo que fue guardado, conteniendo
|
||||
* los datos actualizados.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function afterSave($record): void {}
|
||||
|
||||
/**
|
||||
* Hook que se ejecuta antes de eliminar un registro de la base de datos.
|
||||
*
|
||||
* Permite validar si el registro puede ser eliminado o realizar tareas previas
|
||||
* como desasociar relaciones, eliminar archivos relacionados o verificar restricciones.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $record El modelo que está por ser eliminado.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function beforeDelete($record): void {}
|
||||
|
||||
/**
|
||||
* Hook que se ejecuta después de eliminar un registro de la base de datos.
|
||||
*
|
||||
* Útil para realizar acciones adicionales tras la eliminación, como limpiar datos relacionados,
|
||||
* eliminar archivos vinculados o registrar eventos de auditoría.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $record El modelo eliminado. Aunque ya no existe en la base de datos,
|
||||
* se conserva la información del registro en memoria.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function afterDelete($record): void {}
|
||||
|
||||
// ===================== MANEJO DE VALIDACIONES Y EXCEPCIONES =====================
|
||||
|
||||
/**
|
||||
* Maneja las excepciones de validación.
|
||||
*
|
||||
* Este método captura los errores de validación, los agrega al error bag de Livewire
|
||||
* y dispara un evento para manejar el fallo de validación, útil en formularios modales.
|
||||
*
|
||||
* @param ValidationException $e Excepción de validación capturada.
|
||||
* @return void
|
||||
*/
|
||||
protected function handleValidationException(ValidationException $e): void
|
||||
{
|
||||
$this->setErrorBag($e->validator->errors());
|
||||
|
||||
// Notifica al usuario que ocurrió un error de validación
|
||||
$this->handleException('danger', 'Error en la validación de los datos.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja las excepciones relacionadas con la base de datos.
|
||||
*
|
||||
* Analiza el código de error de la base de datos y genera un mensaje de error específico
|
||||
* para la situación. También se encarga de enviar una notificación de error.
|
||||
*
|
||||
* @param QueryException $e Excepción capturada durante la ejecución de una consulta.
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDatabaseException(QueryException $e): void
|
||||
{
|
||||
$errorMessage = match ($e->errorInfo[1]) {
|
||||
1452 => "Una clave foránea no es válida.",
|
||||
1062 => $this->extractDuplicateField($e->getMessage()),
|
||||
1451 => "No se puede eliminar el registro porque está en uso.",
|
||||
default => env('APP_DEBUG') ? $e->getMessage() : "Error inesperado en la base de datos.",
|
||||
};
|
||||
|
||||
$this->handleException('danger', $errorMessage, 'form', 120000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja cualquier tipo de excepción general y envía una notificación al usuario.
|
||||
*
|
||||
* @param string $type El tipo de notificación (success, danger, warning).
|
||||
* @param string $message El mensaje que se mostrará al usuario.
|
||||
* @param string $target El contenedor donde se mostrará la notificación (por defecto 'form').
|
||||
* @param int $delay Tiempo en milisegundos que durará la notificación en pantalla.
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException($type, $message, $target = 'form', $delay = 9000): void
|
||||
{
|
||||
$this->validationError = true;
|
||||
|
||||
$this->dispatch($this->getDispatche('refresh-offcanvas'));
|
||||
$this->dispatchNotification($type, $message, $target, $delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrae el nombre del campo duplicado de un error de base de datos MySQL.
|
||||
*
|
||||
* Esta función se utiliza para identificar el campo específico que causó un error
|
||||
* de duplicación de clave única, y genera un mensaje personalizado para el usuario.
|
||||
*
|
||||
* @param string $errorMessage El mensaje de error completo proporcionado por MySQL.
|
||||
* @return string Mensaje de error amigable para el usuario.
|
||||
*/
|
||||
private function extractDuplicateField($errorMessage): string
|
||||
{
|
||||
preg_match("/for key 'unique_(.*?)'/", $errorMessage, $matches);
|
||||
|
||||
return isset($matches[1])
|
||||
? "El valor ingresado para '" . str_replace('_', ' ', $matches[1]) . "' ya está en uso."
|
||||
: "Ya existe un registro con este valor.";
|
||||
}
|
||||
|
||||
// ===================== NOTIFICACIONES Y ÉXITO =====================
|
||||
|
||||
/**
|
||||
* Despacha una notificación tras el éxito de una operación.
|
||||
*
|
||||
* @param string $type Tipo de notificación (success, warning, danger)
|
||||
* @param string $message Mensaje a mostrar.
|
||||
* @return void
|
||||
*/
|
||||
protected function handleSuccess(string $type, string $message): void
|
||||
{
|
||||
$this->successProcess = true;
|
||||
|
||||
$this->dispatch($this->getDispatche('refresh-offcanvas'));
|
||||
$this->dispatch($this->getDispatche('reload-table'));
|
||||
|
||||
$this->dispatchNotification($type, $message, 'index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía una notificación al navegador.
|
||||
*
|
||||
* @param string $type Tipo de notificación (success, danger, etc.)
|
||||
* @param string $message Mensaje de la notificación
|
||||
* @param string $target Destino (form, index)
|
||||
* @param int $delay Duración de la notificación en milisegundos
|
||||
*/
|
||||
protected function dispatchNotification($type, $message, $target = 'form', $delay = 9000): void
|
||||
{
|
||||
$model = new ($this->model());
|
||||
|
||||
$this->tagName = $model->tagName;
|
||||
$this->columnNameLabel = $model->columnNameLabel;
|
||||
$this->singularName = $model->singularName;
|
||||
|
||||
$tagOffcanvas = ucfirst(Str::camel($model->tagName));
|
||||
|
||||
$targetNotifies = [
|
||||
"index" => '#bt-' . Str::kebab($model->tagName) . 's .notification-container',
|
||||
"form" => "#offcanvas{$tagOffcanvas} .notification-container",
|
||||
];
|
||||
|
||||
$this->dispatch(
|
||||
'notification',
|
||||
target: $target === 'index' ? $targetNotifies['index'] : $targetNotifies['form'],
|
||||
type: $type,
|
||||
message: $message,
|
||||
delay: $delay
|
||||
);
|
||||
}
|
||||
|
||||
// ===================== FORMULARIO Y CONVERSIÓN DE DATOS =====================
|
||||
|
||||
/**
|
||||
* Convierte los valores vacíos a `null` en los campos que son configurados como `nullable`.
|
||||
*
|
||||
* Esta función verifica las reglas de validación actuales y transforma todos los campos vacíos
|
||||
* en valores `null` si las reglas permiten valores nulos. Es útil para evitar insertar cadenas vacías
|
||||
* en la base de datos donde se espera un valor nulo.
|
||||
*
|
||||
* @param array $data Los datos del formulario que se deben procesar.
|
||||
* @return void
|
||||
*/
|
||||
protected function convertEmptyValuesToNull(array &$data): void
|
||||
{
|
||||
$nullableFields = array_keys(array_filter($this->dynamicRules($this->mode), function ($rules) {
|
||||
return in_array('nullable', (array) $rules);
|
||||
}));
|
||||
|
||||
foreach ($nullableFields as $field) {
|
||||
if (isset($data[$field]) && $data[$field] === '') {
|
||||
$data[$field] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica tipos de datos definidos en `$casts` a los campos del formulario.
|
||||
*
|
||||
* Esta función toma los datos de entrada y los transforma en el tipo de datos esperado según
|
||||
* lo definido en la propiedad `$casts`. Es útil para asegurar que los datos se almacenen en
|
||||
* el formato correcto, como convertir cadenas a números enteros o booleanos.
|
||||
*
|
||||
* @param array $data Los datos del formulario que necesitan ser casteados.
|
||||
* @return void
|
||||
*/
|
||||
protected function applyCasts(array &$data): void
|
||||
{
|
||||
foreach ($this->casts as $field => $type) {
|
||||
if (array_key_exists($field, $data)) {
|
||||
$data[$field] = $this->castValue($type, $data[$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Castea un valor a su tipo de dato correspondiente.
|
||||
*
|
||||
* Convierte un valor dado al tipo especificado, manejando adecuadamente los valores vacíos
|
||||
* o nulos. También asegura que valores como `0` o `''` sean tratados correctamente
|
||||
* para evitar errores al almacenarlos en la base de datos.
|
||||
*
|
||||
* @param string $type El tipo de dato al que se debe convertir (`boolean`, `integer`, `float`, `string`, `array`).
|
||||
* @param mixed $value El valor que se debe castear.
|
||||
* @return mixed El valor convertido al tipo especificado.
|
||||
*/
|
||||
protected function castValue($type, $value): mixed
|
||||
{
|
||||
// Convertir valores vacíos o cero a null si corresponde
|
||||
if (is_null($value) || $value === '' || $value === '0' || $value === 0.0) {
|
||||
return match ($type) {
|
||||
'boolean' => false, // No permitir null en booleanos
|
||||
'integer' => 0, // Valor por defecto para enteros
|
||||
'float', 'double' => 0.0, // Valor por defecto para decimales
|
||||
'string' => "", // Convertir cadena vacía en null
|
||||
'array' => [], // Evitar null en arrays
|
||||
default => null, // Valor por defecto para otros tipos
|
||||
};
|
||||
}
|
||||
|
||||
// Castear el valor si no es null ni vacío
|
||||
return match ($type) {
|
||||
'boolean' => (bool) $value,
|
||||
'integer' => (int) $value,
|
||||
'float', 'double' => (float) $value,
|
||||
'string' => (string) $value,
|
||||
'array' => (array) $value,
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// ===================== RENDERIZACIÓN DE VISTA =====================
|
||||
|
||||
/**
|
||||
* Renderiza la vista del formulario.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view($this->viewPath());
|
||||
}
|
||||
}
|
28
Livewire/Permissions/PermissionIndex.php
Normal file
28
Livewire/Permissions/PermissionIndex.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Permissions;
|
||||
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class PermissionIndex extends Component
|
||||
{
|
||||
public $roles_html_select;
|
||||
public $rows_roles;
|
||||
|
||||
public function render()
|
||||
{
|
||||
// Generamos Select y estilos HTML de roles
|
||||
$this->roles_html_select = "<select id=\"UserRole\" class=\"form-select text-capitalize\"><option value=\"\"> Selecciona un rol </option>";
|
||||
|
||||
foreach (Role::all() as $role) {
|
||||
$this->rows_roles[$role->name] = "<span class=\"badge bg-label-{$role->style} m-1\">{$role->name}</span>";
|
||||
$this->roles_html_select .= "<option value=\"{$role->name}\" class=\"text-capitalize\">{$role->name}</option>";
|
||||
}
|
||||
|
||||
$this->roles_html_select .= "</select>";
|
||||
|
||||
return view('vuexy-admin::livewire.permissions.index');
|
||||
}
|
||||
}
|
35
Livewire/Permissions/Permissions.php
Normal file
35
Livewire/Permissions/Permissions.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Permissions;
|
||||
|
||||
use Livewire\Component;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class Permissions extends Component
|
||||
{
|
||||
public $permissionName;
|
||||
|
||||
public function createPermission()
|
||||
{
|
||||
$this->validate([
|
||||
'permissionName' => 'required|unique:permissions,name'
|
||||
]);
|
||||
|
||||
Permission::create(['name' => $this->permissionName]);
|
||||
session()->flash('message', 'Permiso creado con éxito.');
|
||||
$this->reset('permissionName');
|
||||
}
|
||||
|
||||
public function deletePermission($id)
|
||||
{
|
||||
Permission::find($id)->delete();
|
||||
session()->flash('message', 'Permiso eliminado.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.permissions', [
|
||||
'permissions' => Permission::all()
|
||||
]);
|
||||
}
|
||||
}
|
182
Livewire/Roles/RoleCards.php
Normal file
182
Livewire/Roles/RoleCards.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Roles;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class RoleCards extends Component
|
||||
{
|
||||
public $roles = [];
|
||||
public $permissions = [];
|
||||
public $roleId;
|
||||
public $name;
|
||||
public $style;
|
||||
public $title;
|
||||
public $btn_submit_text;
|
||||
public $permissionsInputs = [];
|
||||
public $destroyRoleId;
|
||||
|
||||
protected $listeners = ['saveRole', 'deleteRole'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadRolesAndPermissions();
|
||||
$this->dispatch('reloadForm');
|
||||
}
|
||||
|
||||
private function loadRolesAndPermissions()
|
||||
{
|
||||
$this->roles = Auth::user()->hasRole('SuperAdmin') ?
|
||||
Role::all() :
|
||||
Role::where('name', '!=', 'SuperAdmin')->get();
|
||||
|
||||
// Obtener todos los permisos
|
||||
$permissions = Permission::all()->map(function ($permission) {
|
||||
$name = $permission->name;
|
||||
$action = substr($name, strrpos($name, '.') + 1);
|
||||
|
||||
return [
|
||||
'group_name' => $permission->group_name,
|
||||
'sub_group_name' => $permission->sub_group_name,
|
||||
$action => $name // Agregar la acción directamente al array
|
||||
];
|
||||
})->groupBy('group_name'); // Agrupar los permisos por grupo
|
||||
|
||||
|
||||
// Procesar los permisos agrupados para cargarlos en el componente
|
||||
$permissionsInputs = [];
|
||||
|
||||
$this->permissions = $permissions->map(function ($groupPermissions) use (&$permissionsInputs) {
|
||||
$permission = [
|
||||
'group_name' => $groupPermissions[0]['group_name'], // Tomar el grupo del primer permiso del grupo
|
||||
'sub_group_name' => $groupPermissions[0]['sub_group_name'], // Tomar la descripción del primer permiso del grupo
|
||||
];
|
||||
|
||||
// Agregar todas las acciones al permissionsInputs y al permission
|
||||
foreach ($groupPermissions as $permissionData) {
|
||||
foreach ($permissionData as $key => $value) {
|
||||
if ($key !== 'sub_group_name' && $key !== 'group_name') {
|
||||
$permissionsInputs[str_replace('.', '_', $value)] = false;
|
||||
$permission[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $permission;
|
||||
});
|
||||
|
||||
$this->permissionsInputs = $permissionsInputs;
|
||||
}
|
||||
|
||||
public function loadRoleData($action, $roleId = false)
|
||||
{
|
||||
$this->resetForm();
|
||||
|
||||
$this->title = 'Agregar un nuevo rol';
|
||||
$this->btn_submit_text = 'Crear nuevo rol';
|
||||
|
||||
if ($roleId) {
|
||||
$role = Role::findOrFail($roleId);
|
||||
|
||||
switch ($action) {
|
||||
case 'view':
|
||||
$this->title = $role->name;
|
||||
$this->name = $role->name;
|
||||
$this->style = $role->style;
|
||||
$this->dispatch('deshabilitarFormulario');
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
$this->title = 'Editar rol';
|
||||
$this->btn_submit_text = 'Guardar cambios';
|
||||
$this->roleId = $roleId;
|
||||
$this->name = $role->name;
|
||||
$this->style = $role->style;
|
||||
$this->dispatch('habilitarFormulario');
|
||||
break;
|
||||
|
||||
case 'clone':
|
||||
$this->style = $role->style;
|
||||
$this->dispatch('habilitarFormulario');
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($role->permissions as $permission) {
|
||||
$this->permissionsInputs[str_replace('.', '_', $permission->name)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->dispatch('reloadForm');
|
||||
}
|
||||
|
||||
public function loadDestroyRoleData() {}
|
||||
|
||||
public function saveRole()
|
||||
{
|
||||
$permissions = [];
|
||||
|
||||
foreach ($this->permissionsInputs as $permission => $value) {
|
||||
if ($value === true)
|
||||
$permissions[] = str_replace('_', '.', $permission);
|
||||
}
|
||||
|
||||
if ($this->roleId) {
|
||||
$role = Role::find($this->roleId);
|
||||
|
||||
$role->name = $this->name;
|
||||
$role->style = $this->style;
|
||||
|
||||
$role->save();
|
||||
|
||||
$role->syncPermissions($permissions);
|
||||
} else {
|
||||
$role = Role::create([
|
||||
'name' => $this->name,
|
||||
'style' => $this->style,
|
||||
]);
|
||||
|
||||
$role->syncPermissions($permissions);
|
||||
}
|
||||
|
||||
$this->loadRolesAndPermissions();
|
||||
|
||||
$this->dispatch('modalHide');
|
||||
$this->dispatch('reloadForm');
|
||||
}
|
||||
|
||||
public function deleteRole()
|
||||
{
|
||||
$role = Role::find($this->destroyRoleId);
|
||||
|
||||
if ($role)
|
||||
$role->delete();
|
||||
|
||||
$this->loadRolesAndPermissions();
|
||||
|
||||
$this->dispatch('modalDeleteHide');
|
||||
$this->dispatch('reloadForm');
|
||||
}
|
||||
|
||||
private function resetForm()
|
||||
{
|
||||
$this->roleId = '';
|
||||
$this->name = '';
|
||||
$this->style = '';
|
||||
|
||||
foreach ($this->permissionsInputs as $key => $permission) {
|
||||
$this->permissionsInputs[$key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.roles.cards');
|
||||
}
|
||||
}
|
61
Livewire/Roles/RoleIndex.php
Normal file
61
Livewire/Roles/RoleIndex.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Roles;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class RoleIndex extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $roleName;
|
||||
public $selectedRole;
|
||||
public $permissions = [];
|
||||
public $availablePermissions;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->availablePermissions = Permission::all();
|
||||
}
|
||||
|
||||
public function createRole()
|
||||
{
|
||||
$this->validate([
|
||||
'roleName' => 'required|unique:roles,name'
|
||||
]);
|
||||
|
||||
$role = Role::create(['name' => $this->roleName]);
|
||||
$this->reset(['roleName']);
|
||||
session()->flash('message', 'Rol creado con éxito.');
|
||||
}
|
||||
|
||||
public function selectRole($roleId)
|
||||
{
|
||||
$this->selectedRole = Role::find($roleId);
|
||||
$this->permissions = $this->selectedRole->permissions->pluck('id')->toArray();
|
||||
}
|
||||
|
||||
public function updateRolePermissions()
|
||||
{
|
||||
if ($this->selectedRole) {
|
||||
$this->selectedRole->syncPermissions($this->permissions);
|
||||
session()->flash('message', 'Permisos actualizados correctamente.');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteRole($roleId)
|
||||
{
|
||||
Role::find($roleId)->delete();
|
||||
session()->flash('message', 'Rol eliminado.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.roles', [
|
||||
'index' => Role::paginate(10)
|
||||
]);
|
||||
}
|
||||
}
|
174
Livewire/Table/AbstractIndexComponent.php
Normal file
174
Livewire/Table/AbstractIndexComponent.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Table;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Clase base abstracta para la creación de componentes tipo "Index" con Livewire.
|
||||
*
|
||||
* Provee una estructura general para:
|
||||
* - Configurar y renderizar tablas con Bootstrap Table.
|
||||
* - Definir columnas y formatos de manera estándar.
|
||||
* - Manejar búsquedas, filtros, o catálogos necesarios.
|
||||
* - Centralizar la lógica de montaje (mount).
|
||||
*
|
||||
* @package Koneko\VuexyAdmin\Livewire\Table
|
||||
*/
|
||||
abstract class AbstractIndexComponent extends Component
|
||||
{
|
||||
/**
|
||||
* Configuración principal para la tabla con Bootstrap Table.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $bt_datatable = [];
|
||||
|
||||
/**
|
||||
* Tag identificador del componente, derivado del modelo.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $tagName;
|
||||
|
||||
/**
|
||||
* Nombre singular del modelo (para mensajes, etiquetado, etc.).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $singularName;
|
||||
|
||||
/**
|
||||
* Identificador único del formulario (vinculado al Offcanvas o Modal).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $formId;
|
||||
|
||||
/**
|
||||
* Método para obtener la instancia del modelo asociado.
|
||||
*
|
||||
* Debe retornarse una instancia (o la clase) del modelo Eloquent que maneja este Index.
|
||||
*
|
||||
* @return Model|string
|
||||
*/
|
||||
abstract protected function model(): string;
|
||||
|
||||
/**
|
||||
* Define las columnas (header) de la tabla. Este array se fusionará
|
||||
* o se inyectará en la configuración principal $bt_datatable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function columns(): array;
|
||||
|
||||
/**
|
||||
* Define el formato (formatter) de las columnas.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function format(): array;
|
||||
|
||||
/**
|
||||
* Retorna la ruta de la vista Blade que renderizará el componente.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function viewPath(): string;
|
||||
|
||||
/**
|
||||
* Método que define la configuración base del DataTable.
|
||||
* Aquí puedes poner ajustes comunes (exportFileName, paginación, etc.).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function bootstraptableConfig(): array
|
||||
{
|
||||
return [
|
||||
'sortName' => 'id', // Campo por defecto para ordenar
|
||||
'exportFileName' => 'Listado', // Nombre de archivo para exportar
|
||||
'showFullscreen' => false,
|
||||
'showPaginationSwitch'=> false,
|
||||
'showRefresh' => false,
|
||||
'pagination' => false,
|
||||
// Agrega aquí cualquier otra configuración por defecto que uses
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Se ejecuta al montar el componente Livewire.
|
||||
* Configura $tagName, $singularName, $formId y $bt_datatable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Obtenemos el modelo
|
||||
$model = $this->model();
|
||||
if (is_string($model)) {
|
||||
// Si se retornó la clase en abstract protected function model(),
|
||||
// instanciamos manualmente
|
||||
$model = new $model;
|
||||
}
|
||||
|
||||
// Usamos las propiedades definidas en el modelo
|
||||
// (tagName, singularName, etc.), si existen en el modelo.
|
||||
// Ajusta nombres según tu convención.
|
||||
$this->tagName = $model->tagName ?? Str::snake(class_basename($model));
|
||||
$this->singularName = $model->singularName ?? class_basename($model);
|
||||
$this->formId = Str::kebab($this->tagName) . '-form';
|
||||
|
||||
// Inicia la configuración principal de la tabla
|
||||
$this->setupDataTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Combina la configuración base de la tabla con las columnas y formatos
|
||||
* definidos en las clases hijas.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setupDataTable(): void
|
||||
{
|
||||
$baseConfig = $this->bootstraptableConfig();
|
||||
|
||||
$this->bt_datatable = array_merge($baseConfig, [
|
||||
'header' => $this->columns(),
|
||||
'format' => $this->format(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza la vista definida en viewPath().
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view($this->viewPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejemplo de método para la lógica de filtrado que podrías sobreescribir en la clase hija.
|
||||
*
|
||||
* @param array $criteria
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function applyFilters($criteria = [])
|
||||
{
|
||||
// Aplica tu lógica de filtros, búsquedas, etc.
|
||||
// La clase hija podría sobrescribir este método o llamarlo desde su propia lógica.
|
||||
$query = $this->model()::query();
|
||||
|
||||
// Por ejemplo:
|
||||
/*
|
||||
if (!empty($criteria['store_id'])) {
|
||||
$query->where('store_id', $criteria['store_id']);
|
||||
}
|
||||
*/
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
31
Livewire/Users/UserCount.php
Normal file
31
Livewire/Users/UserCount.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Users;
|
||||
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class UserCount extends Component
|
||||
{
|
||||
public $total, $enabled, $disabled;
|
||||
|
||||
protected $listeners = ['refreshUserCount' => 'updateCounts'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->updateCounts();
|
||||
}
|
||||
|
||||
public function updateCounts()
|
||||
{
|
||||
$this->total = User::count();
|
||||
$this->enabled = User::where('status', User::STATUS_ENABLED)->count();
|
||||
$this->disabled = User::where('status', User::STATUS_DISABLED)->count();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.users.count');
|
||||
}
|
||||
}
|
306
Livewire/Users/UserForm.php
Normal file
306
Livewire/Users/UserForm.php
Normal file
@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Users;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormComponent;
|
||||
use Koneko\SatCatalogs\Models\{Colonia, Estado, Localidad, Municipio, Pais, RegimenFiscal};
|
||||
use Koneko\VuexyAdmin\Models\Store;
|
||||
|
||||
/**
|
||||
* Class UserForm
|
||||
*
|
||||
* Componente Livewire para manejar el formulario CRUD de sucursales en el sistema ERP.
|
||||
* Implementa la creación, edición y eliminación de sucursales con validaciones dinámicas.
|
||||
*/
|
||||
class UserForm extends AbstractFormComponent
|
||||
{
|
||||
/**
|
||||
* Campos específicos del formulario.
|
||||
*/
|
||||
public $code, $name, $description, $manager_id, $rfc, $nombre_fiscal, $c_regimen_fiscal,
|
||||
$domicilio_fiscal, $serie_ingresos, $serie_egresos, $serie_pagos, $c_codigo_postal,
|
||||
$c_pais, $c_estado, $c_localidad, $c_municipio, $c_colonia, $direccion, $num_ext,
|
||||
$num_int, $email, $tel, $tel2, $lat, $lng, $show_on_website, $enable_ecommerce, $status;
|
||||
|
||||
|
||||
public $confirmDeletion = false;
|
||||
|
||||
/**
|
||||
* Listas de opciones para selects en el formulario.
|
||||
*/
|
||||
public $manager_id_options = [],
|
||||
$c_regimen_fiscal_options = [],
|
||||
$c_pais_options = [],
|
||||
$c_estado_options = [],
|
||||
$c_localidad_options = [],
|
||||
$c_municipio_options = [],
|
||||
$c_colonia_options = [];
|
||||
|
||||
/**
|
||||
* Montar el formulario e inicializar datos específicos.
|
||||
*
|
||||
* @param string $mode Modo del formulario: create, edit, delete.
|
||||
* @param Store|null $store El modelo Store si está en modo edición o eliminación.
|
||||
*/
|
||||
public function mount(string $mode = 'create', mixed $store = null): void
|
||||
{
|
||||
parent::mount($mode, $store->id ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cargar opciones de formularios según el modo actual.
|
||||
*
|
||||
* @param string $mode
|
||||
*/
|
||||
private function loadOptions(string $mode): void
|
||||
{
|
||||
$this->manager_id_options = User::getUsersListWithInactive($this->manager_id, ['type' => 'user', 'status' => 1]);
|
||||
$this->c_regimen_fiscal_options = RegimenFiscal::selectList();
|
||||
$this->c_pais_options = Pais::selectList();
|
||||
$this->c_estado_options = Estado::selectList($this->c_pais)->toArray();
|
||||
|
||||
if ($mode !== 'create') {
|
||||
$this->c_localidad_options = Localidad::selectList($this->c_estado)->toArray();
|
||||
$this->c_municipio_options = Municipio::selectList($this->c_estado, $this->c_municipio)->toArray();
|
||||
$this->c_colonia_options = Colonia::selectList($this->c_codigo_postal, $this->c_colonia)->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== MÉTODOS OBLIGATORIOS =====================
|
||||
|
||||
/**
|
||||
* Devuelve el modelo Eloquent asociado.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Store::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reglas de validación dinámicas según el modo actual.
|
||||
*
|
||||
* @param string $mode
|
||||
* @return array
|
||||
*/
|
||||
protected function dynamicRules(string $mode): array
|
||||
{
|
||||
switch ($mode) {
|
||||
case 'create':
|
||||
case 'edit':
|
||||
return [
|
||||
'code' => [
|
||||
'required', 'string', 'alpha_num', 'max:16',
|
||||
Rule::unique('stores', 'code')->ignore($this->id)
|
||||
],
|
||||
'name' => 'required|string|max:96',
|
||||
'description' => 'nullable|string|max:1024',
|
||||
'manager_id' => 'nullable|exists:users,id',
|
||||
|
||||
// Información fiscal
|
||||
'rfc' => ['nullable', 'string', 'regex:/^([A-ZÑ&]{3,4})(\d{6})([A-Z\d]{3})$/i', 'max:13'],
|
||||
'nombre_fiscal' => 'nullable|string|max:255',
|
||||
'c_regimen_fiscal' => 'nullable|exists:sat_regimen_fiscal,c_regimen_fiscal',
|
||||
'domicilio_fiscal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal',
|
||||
|
||||
// Ubicación
|
||||
'c_pais' => 'nullable|exists:sat_pais,c_pais|string|size:3',
|
||||
'c_estado' => 'nullable|exists:sat_estado,c_estado|string|min:2|max:3',
|
||||
'c_municipio' => 'nullable|exists:sat_municipio,c_municipio|integer',
|
||||
'c_localidad' => 'nullable|integer',
|
||||
'c_codigo_postal' => 'nullable|exists:sat_codigo_postal,c_codigo_postal|integer',
|
||||
'c_colonia' => 'nullable|exists:sat_colonia,c_colonia|integer',
|
||||
'direccion' => 'nullable|string|max:255',
|
||||
'num_ext' => 'nullable|string|max:50',
|
||||
'num_int' => 'nullable|string|max:50',
|
||||
'lat' => 'nullable|numeric|between:-90,90',
|
||||
'lng' => 'nullable|numeric|between:-180,180',
|
||||
|
||||
// Contacto
|
||||
'email' => ['nullable', 'email', 'required_if:enable_ecommerce,true'],
|
||||
'tel' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'],
|
||||
'tel2' => ['nullable', 'regex:/^[0-9\s\-\+\(\)]+$/', 'max:15'],
|
||||
|
||||
// Configuración web y estado
|
||||
'show_on_website' => 'nullable|boolean',
|
||||
'enable_ecommerce' => 'nullable|boolean',
|
||||
'status' => 'nullable|boolean',
|
||||
];
|
||||
|
||||
case 'delete':
|
||||
return [
|
||||
'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación
|
||||
];
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa los datos del formulario en función del modo.
|
||||
*
|
||||
* @param Store|null $store
|
||||
* @param string $mode
|
||||
*/
|
||||
protected function initializeFormData(mixed $store, string $mode): void
|
||||
{
|
||||
if ($store) {
|
||||
$this->code = $store->code;
|
||||
$this->name = $store->name;
|
||||
$this->description = $store->description;
|
||||
$this->manager_id = $store->manager_id;
|
||||
$this->rfc = $store->rfc;
|
||||
$this->nombre_fiscal = $store->nombre_fiscal;
|
||||
$this->c_regimen_fiscal = $store->c_regimen_fiscal;
|
||||
$this->domicilio_fiscal = $store->domicilio_fiscal;
|
||||
$this->c_pais = $store->c_pais;
|
||||
$this->c_estado = $store->c_estado;
|
||||
$this->c_municipio = $store->c_municipio;
|
||||
$this->c_localidad = $store->c_localidad;
|
||||
$this->c_codigo_postal = $store->c_codigo_postal;
|
||||
$this->c_colonia = $store->c_colonia;
|
||||
$this->direccion = $store->direccion;
|
||||
$this->num_ext = $store->num_ext;
|
||||
$this->num_int = $store->num_int;
|
||||
$this->lat = $store->lat;
|
||||
$this->lng = $store->lng;
|
||||
$this->email = $store->email;
|
||||
$this->tel = $store->tel;
|
||||
$this->tel2 = $store->tel2;
|
||||
$this->show_on_website = (bool) $store->show_on_website;
|
||||
$this->enable_ecommerce = (bool) $store->enable_ecommerce;
|
||||
$this->status = (bool) $store->status;
|
||||
|
||||
} else {
|
||||
$this->c_pais = 'MEX';
|
||||
$this->status = true;
|
||||
$this->show_on_website = false;
|
||||
$this->enable_ecommerce = false;
|
||||
}
|
||||
|
||||
$this->loadOptions($mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepara los datos validados para su almacenamiento.
|
||||
*
|
||||
* @param array $validatedData
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareData(array $validatedData): array
|
||||
{
|
||||
return [
|
||||
'code' => $validatedData['code'],
|
||||
'name' => $validatedData['name'],
|
||||
'description' => strip_tags($validatedData['description']),
|
||||
'manager_id' => $validatedData['manager_id'],
|
||||
'rfc' => $validatedData['rfc'],
|
||||
'nombre_fiscal' => $validatedData['nombre_fiscal'],
|
||||
'c_regimen_fiscal' => $validatedData['c_regimen_fiscal'],
|
||||
'domicilio_fiscal' => $validatedData['domicilio_fiscal'],
|
||||
'c_codigo_postal' => $validatedData['c_codigo_postal'],
|
||||
'c_pais' => $validatedData['c_pais'],
|
||||
'c_estado' => $validatedData['c_estado'],
|
||||
'c_localidad' => $validatedData['c_localidad'],
|
||||
'c_municipio' => $validatedData['c_municipio'],
|
||||
'c_colonia' => $validatedData['c_colonia'],
|
||||
'direccion' => $validatedData['direccion'],
|
||||
'num_ext' => $validatedData['num_ext'],
|
||||
'num_int' => $validatedData['num_int'],
|
||||
'email' => $validatedData['email'],
|
||||
'tel' => $validatedData['tel'],
|
||||
'tel2' => $validatedData['tel2'],
|
||||
'lat' => $validatedData['lat'],
|
||||
'lng' => $validatedData['lng'],
|
||||
'status' => $validatedData['status'],
|
||||
'show_on_website' => $validatedData['show_on_website'],
|
||||
'enable_ecommerce' => $validatedData['enable_ecommerce'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Definición de los contenedores de notificación.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function targetNotifies(): array
|
||||
{
|
||||
return [
|
||||
"index" => "#bt-stores .notification-container",
|
||||
"form" => "#store-form .notification-container",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruta de vista asociada al formulario.
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
protected function viewPath(): string
|
||||
{
|
||||
return 'vuexy-store-manager::livewire.stores.form';
|
||||
}
|
||||
|
||||
// ===================== VALIDACIONES =====================
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'code' => 'código de sucursal',
|
||||
'name' => 'nombre de la sucursal',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error messages for the defined validation rules.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'code.required' => 'El código de la sucursal es obligatorio.',
|
||||
'code.unique' => 'Este código ya está en uso por otra sucursal.',
|
||||
'name.required' => 'El nombre de la sucursal es obligatorio.',
|
||||
];
|
||||
}
|
||||
|
||||
// ===================== PREPARACIÓN DE DATOS =====================
|
||||
|
||||
// ===================== NOTIFICACIONES Y EVENTOS =====================
|
||||
|
||||
/**
|
||||
* Definición de los eventos del componente.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function dispatches(): array
|
||||
{
|
||||
return [
|
||||
'on-failed-validation' => 'on-failed-validation-store',
|
||||
'on-hydrate' => 'on-hydrate-store-modal',
|
||||
];
|
||||
}
|
||||
|
||||
// ===================== REDIRECCIÓN =====================
|
||||
|
||||
/**
|
||||
* Define la ruta de redirección tras guardar o eliminar.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getRedirectRoute(): string
|
||||
{
|
||||
return 'admin.core.user.index';
|
||||
}
|
||||
|
||||
}
|
115
Livewire/Users/UserIndex.copy.php
Normal file
115
Livewire/Users/UserIndex.copy.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Users;
|
||||
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class UserIndex extends Component
|
||||
{
|
||||
public $statuses;
|
||||
public $status_options;
|
||||
|
||||
public $rows_roles = [];
|
||||
public $roles_options = [];
|
||||
public $roles_html_select;
|
||||
|
||||
public $total, $enabled, $disabled;
|
||||
|
||||
public $indexAlert;
|
||||
|
||||
public $userId, $name, $email, $password, $roles, $status, $photo, $src_photo;
|
||||
public $modalTitle;
|
||||
public $btnSubmitTxt;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->modalTitle = 'Crear usuario nuevo';
|
||||
$this->btnSubmitTxt = 'Crear usuario';
|
||||
|
||||
$this->statuses = [
|
||||
User::STATUS_ENABLED => ['title' => 'Activo', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_ENABLED]],
|
||||
User::STATUS_DISABLED => ['title' => 'Deshabilitado', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_DISABLED]],
|
||||
User::STATUS_REMOVED => ['title' => 'Eliminado', 'class' => 'badge bg-label-' . User::$statusListClass[User::STATUS_REMOVED]],
|
||||
];
|
||||
|
||||
$roles = Role::whereNotIn('name', ['Patient', 'Doctor'])->get();
|
||||
|
||||
$this->roles_html_select = "<select id=\"UserRole\" class=\"form-select text-capitalize\"><option value=\"\"> Selecciona un rol </option>";
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$this->rows_roles[$role->name] = "<span class=\"badge bg-label-" . $role->style . " mx-1\">" . $role->name . "</span>";
|
||||
|
||||
if (Auth::user()->hasRole('SuperAdmin') || $role->name != 'SuperAdmin') {
|
||||
$this->roles_html_select .= "<option value=\"" . $role->name . "\" class=\"text-capitalize\">" . $role->name . "</option>";
|
||||
$this->roles_options[$role->name] = $role->name;
|
||||
}
|
||||
}
|
||||
|
||||
$this->roles_html_select .= "</select>";
|
||||
|
||||
$this->status_options = [
|
||||
User::STATUS_ENABLED => User::$statusList[User::STATUS_ENABLED],
|
||||
User::STATUS_DISABLED => User::$statusList[User::STATUS_DISABLED],
|
||||
];
|
||||
}
|
||||
|
||||
public function countUsers()
|
||||
{
|
||||
$this->total = User::count();
|
||||
$this->enabled = User::where('status', User::STATUS_ENABLED)->count();
|
||||
$this->disabled = User::where('status', User::STATUS_DISABLED)->count();
|
||||
}
|
||||
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
$this->indexAlert = '';
|
||||
$this->modalTitle = 'Editar usuario: ' . $id;
|
||||
$this->btnSubmitTxt = 'Guardar cambios';
|
||||
|
||||
$this->userId = $user->id;
|
||||
$this->name = $user->name;
|
||||
$this->email = $user->email;
|
||||
$this->password = '';
|
||||
$this->roles = $user->roles->pluck('name')->toArray();
|
||||
$this->src_photo = $user->profile_photo_url;
|
||||
$this->status = $user->status;
|
||||
|
||||
$this->dispatch('openModal');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$user = User::find($id);
|
||||
|
||||
if ($user) {
|
||||
// Eliminar la imagen de perfil si existe
|
||||
if ($user->profile_photo_path)
|
||||
Storage::disk('public')->delete($user->profile_photo_path);
|
||||
|
||||
// Eliminar el usuario
|
||||
$user->delete();
|
||||
|
||||
$this->indexAlert = '<div class="alert alert-warning alert-dismissible" role="alert">Se eliminó correctamente el usuario.<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
|
||||
$this->dispatch('refreshUserCount');
|
||||
$this->dispatch('afterDelete');
|
||||
} else {
|
||||
$this->indexAlert = '<div class="alert alert-danger alert-dismissible" role="alert">Usuario no encontrado.<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('vuexy-admin::livewire.users.index', [
|
||||
'users' => User::paginate(10),
|
||||
]);
|
||||
}
|
||||
}
|
299
Livewire/Users/UserIndex.php
Normal file
299
Livewire/Users/UserIndex.php
Normal file
@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Users;
|
||||
|
||||
use Livewire\WithFileUploads;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
|
||||
|
||||
class UserIndex extends AbstractIndexComponent
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public $doc_file;
|
||||
public $dropzoneVisible = true;
|
||||
|
||||
/**
|
||||
* Almacena rutas útiles para la funcionalidad de edición o eliminación.
|
||||
*/
|
||||
public $routes = [];
|
||||
|
||||
/**
|
||||
* Método que define la clase o instancia del modelo a usar en este Index.
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return User::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna las columnas (header) de la tabla.
|
||||
*/
|
||||
protected function columns(): array
|
||||
{
|
||||
return [
|
||||
'action' => 'Acciones',
|
||||
'code' => 'Código personal',
|
||||
'full_name' => 'Nombre Completo',
|
||||
'email' => 'Correo Electrónico',
|
||||
'parent_name' => 'Responsable',
|
||||
'parent_email' => 'Correo Responsable',
|
||||
'company' => 'Empresa',
|
||||
'birth_date' => 'Fecha de Nacimiento',
|
||||
'hire_date' => 'Fecha de Contratación',
|
||||
'curp' => 'CURP',
|
||||
'nss' => 'NSS',
|
||||
'job_title' => 'Puesto',
|
||||
'rfc' => 'RFC',
|
||||
'nombre_fiscal' => 'Nombre Fiscal',
|
||||
'profile_photo_path' => 'Foto de Perfil',
|
||||
'is_partner' => 'Socio',
|
||||
'is_employee' => 'Empleado',
|
||||
'is_prospect' => 'Prospecto',
|
||||
'is_customer' => 'Cliente',
|
||||
'is_provider' => 'Proveedor',
|
||||
'is_user' => 'Usuario',
|
||||
'status' => 'Estatus',
|
||||
'creator' => 'Creado Por',
|
||||
'creator_email' => 'Correo Creador',
|
||||
'created_at' => 'Fecha de Creación',
|
||||
'updated_at' => 'Última Modificación',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna el formato (formatter) para cada columna.
|
||||
*/
|
||||
protected function format(): array
|
||||
{
|
||||
return [
|
||||
'action' => [
|
||||
'formatter' => 'userActionFormatter',
|
||||
'onlyFormatter' => true,
|
||||
],
|
||||
'code' => [
|
||||
'formatter' => [
|
||||
'name' => 'dynamicBadgeFormatter',
|
||||
'params' => ['color' => 'secondary'],
|
||||
],
|
||||
'align' => 'center',
|
||||
'switchable' => false,
|
||||
],
|
||||
'full_name' => [
|
||||
'formatter' => 'userProfileFormatter',
|
||||
],
|
||||
'email' => [
|
||||
'formatter' => 'emailFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'parent_name' => [
|
||||
'formatter' => 'contactParentFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'agent_name' => [
|
||||
'formatter' => 'agentFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'company' => [
|
||||
'formatter' => 'textNowrapFormatter',
|
||||
],
|
||||
'curp' => [
|
||||
'visible' => false,
|
||||
],
|
||||
'nss' => [
|
||||
'visible' => false,
|
||||
],
|
||||
'job_title' => [
|
||||
'formatter' => 'textNowrapFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'rfc' => [
|
||||
'visible' => false,
|
||||
],
|
||||
'nombre_fiscal' => [
|
||||
'formatter' => 'textNowrapFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'domicilio_fiscal' => [
|
||||
'visible' => false,
|
||||
],
|
||||
'c_uso_cfdi' => [
|
||||
'formatter' => 'usoCfdiFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'tipo_persona' => [
|
||||
'formatter' => 'dynamicBadgeFormatter',
|
||||
'align' => 'center',
|
||||
'visible' => false,
|
||||
],
|
||||
'c_regimen_fiscal' => [
|
||||
'formatter' => 'regimenFiscalFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'birth_date' => [
|
||||
'align' => 'center',
|
||||
'visible' => false,
|
||||
],
|
||||
'hire_date' => [
|
||||
'align' => 'center',
|
||||
'visible' => false,
|
||||
],
|
||||
'estado' => [
|
||||
'formatter' => 'textNowrapFormatter',
|
||||
],
|
||||
'municipio' => [
|
||||
'formatter' => 'textNowrapFormatter',
|
||||
],
|
||||
'localidad' => [
|
||||
'formatter' => 'textNowrapFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'is_partner' => [
|
||||
'formatter' => [
|
||||
'name' => 'dynamicBooleanFormatter',
|
||||
'params' => ['tag' => 'checkSI'],
|
||||
],
|
||||
'align' => 'center',
|
||||
],
|
||||
'is_employee' => [
|
||||
'formatter' => [
|
||||
'name' => 'dynamicBooleanFormatter',
|
||||
'params' => ['tag' => 'checkSI'],
|
||||
],
|
||||
'align' => 'center',
|
||||
],
|
||||
'is_prospect' => [
|
||||
'formatter' => [
|
||||
'name' => 'dynamicBooleanFormatter',
|
||||
'params' => ['tag' => 'checkSI'],
|
||||
],
|
||||
'align' => 'center',
|
||||
],
|
||||
'is_customer' => [
|
||||
'formatter' => [
|
||||
'name' => 'dynamicBooleanFormatter',
|
||||
'params' => ['tag' => 'checkSI'],
|
||||
],
|
||||
'align' => 'center',
|
||||
],
|
||||
'is_provider' => [
|
||||
'formatter' => [
|
||||
'name' => 'dynamicBooleanFormatter',
|
||||
'params' => ['tag' => 'checkSI'],
|
||||
],
|
||||
'align' => 'center',
|
||||
],
|
||||
'is_user' => [
|
||||
'formatter' => [
|
||||
'name' => 'dynamicBooleanFormatter',
|
||||
'params' => ['tag' => 'checkSI'],
|
||||
],
|
||||
'align' => 'center',
|
||||
],
|
||||
'status' => [
|
||||
'formatter' => 'statusIntBadgeBgFormatter',
|
||||
'align' => 'center',
|
||||
],
|
||||
'creator' => [
|
||||
'formatter' => 'creatorFormatter',
|
||||
'visible' => false,
|
||||
],
|
||||
'created_at' => [
|
||||
'formatter' => 'textNowrapFormatter',
|
||||
'align' => 'center',
|
||||
'visible' => false,
|
||||
],
|
||||
'updated_at' => [
|
||||
'formatter' => 'textNowrapFormatter',
|
||||
'align' => 'center',
|
||||
'visible' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa el documento recibido (CFDI XML o Constancia PDF).
|
||||
*/
|
||||
public function processDocument()
|
||||
{
|
||||
// Verificamos si el archivo es válido
|
||||
if (!$this->doc_file instanceof UploadedFile) {
|
||||
return $this->addError('doc_file', 'No se pudo recibir el archivo.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Validar tipo de archivo
|
||||
$this->validate([
|
||||
'doc_file' => 'required|mimes:pdf,xml|max:2048'
|
||||
]);
|
||||
|
||||
|
||||
// **Detectar el tipo de documento**
|
||||
$extension = strtolower($this->doc_file->getClientOriginalExtension());
|
||||
|
||||
// **Procesar según el tipo de archivo**
|
||||
switch ($extension) {
|
||||
case 'xml':
|
||||
$service = new FacturaXmlService();
|
||||
$data = $service->processUploadedFile($this->doc_file);
|
||||
break;
|
||||
|
||||
case 'pdf':
|
||||
$service = new ConstanciaFiscalService();
|
||||
$data = $service->extractData($this->doc_file);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Formato de archivo no soportado.");
|
||||
}
|
||||
|
||||
dd($data);
|
||||
|
||||
// **Asignar los valores extraídos al formulario**
|
||||
$this->rfc = $data['rfc'] ?? null;
|
||||
$this->name = $data['name'] ?? null;
|
||||
$this->email = $data['email'] ?? null;
|
||||
$this->tel = $data['telefono'] ?? null;
|
||||
//$this->direccion = $data['domicilio_fiscal'] ?? null;
|
||||
|
||||
// Ocultar el Dropzone después de procesar
|
||||
$this->dropzoneVisible = false;
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
$this->handleValidationException($e);
|
||||
|
||||
} catch (QueryException $e) {
|
||||
$this->handleDatabaseException($e);
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
$this->handleException('danger', 'Registro no encontrado.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->handleException('danger', 'Error al procesar el archivo: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Montamos el componente y llamamos al parent::mount() para configurar la tabla.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
parent::mount();
|
||||
|
||||
// Definimos las rutas específicas de este componente
|
||||
$this->routes = [
|
||||
'admin.user.show' => route('admin.core.users.show', ['user' => ':id']),
|
||||
'admin.user.edit' => route('admin.core.users.edit', ['user' => ':id']),
|
||||
'admin.user.delete' => route('admin.core.users.delete', ['user' => ':id']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retorna la vista a renderizar por este componente.
|
||||
*/
|
||||
protected function viewPath(): string
|
||||
{
|
||||
return 'vuexy-admin::livewire.users.index';
|
||||
}
|
||||
}
|
295
Livewire/Users/UserOffCanvasForm.php
Normal file
295
Livewire/Users/UserOffCanvasForm.php
Normal file
@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Users;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyContacts\Services\{ContactCatalogService,ConstanciaFiscalService,FacturaXmlService};
|
||||
use Koneko\VuexyStoreManager\Services\StoreCatalogService;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
/**
|
||||
* Class UserOffCanvasForm
|
||||
*
|
||||
* Componente Livewire para gestionar almacenes.
|
||||
* Extiende la clase AbstractFormOffCanvasComponent e implementa validaciones dinámicas,
|
||||
* manejo de formularios, eventos y actualizaciones en tiempo real.
|
||||
*
|
||||
* @package Koneko\VuexyAdmin\Livewire\Users
|
||||
*/
|
||||
class UserOffCanvasForm extends AbstractFormOffCanvasComponent
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public $doc_file;
|
||||
public $dropzoneVisible = true;
|
||||
|
||||
|
||||
/**
|
||||
* Propiedades del formulario relacionadas con el usuario.
|
||||
*/
|
||||
public $code,
|
||||
$parent_id,
|
||||
$name,
|
||||
$last_name,
|
||||
$email,
|
||||
$company,
|
||||
$rfc,
|
||||
$nombre_fiscal,
|
||||
$tipo_persona,
|
||||
$c_regimen_fiscal,
|
||||
$domicilio_fiscal,
|
||||
$is_partner,
|
||||
$is_employee,
|
||||
$is_prospect,
|
||||
$is_customer,
|
||||
$is_provider,
|
||||
$status;
|
||||
|
||||
/**
|
||||
* Listas de opciones para selects en el formulario.
|
||||
*/
|
||||
public $store_options = [],
|
||||
$work_center_options = [],
|
||||
$manager_options = [];
|
||||
|
||||
/**
|
||||
* Eventos de escucha de Livewire.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $listeners = [
|
||||
'editUsers' => 'loadFormModel',
|
||||
'confirmDeletionUsers' => 'loadFormModelForDeletion',
|
||||
];
|
||||
|
||||
/**
|
||||
* Definición de tipos de datos que se deben castear.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'status' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Define el modelo Eloquent asociado con el formulario.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return User::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define los campos del formulario.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function fields(): array
|
||||
{
|
||||
return (new User())->getFillable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Valores por defecto para el formulario.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function defaults(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Campo que se debe enfocar cuando se abra el formulario.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function focusOnOpen(): string
|
||||
{
|
||||
return 'name';
|
||||
}
|
||||
|
||||
/**
|
||||
* Define reglas de validación dinámicas basadas en el modo actual.
|
||||
*
|
||||
* @param string $mode El modo actual del formulario ('create', 'edit', 'delete').
|
||||
* @return array
|
||||
*/
|
||||
protected function dynamicRules(string $mode): array
|
||||
{
|
||||
switch ($mode) {
|
||||
case 'create':
|
||||
case 'edit':
|
||||
return [
|
||||
'code' => ['required', 'string', 'max:16', Rule::unique('contact', 'code')->ignore($this->id)],
|
||||
'name' => ['required', 'string', 'max:96'],
|
||||
'notes' => ['nullable', 'string', 'max:1024'],
|
||||
'tel' => ['nullable', 'regex:/^[0-9+\-\s]+$/', 'max:20'],
|
||||
];
|
||||
|
||||
case 'delete':
|
||||
return [
|
||||
'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación
|
||||
];
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== VALIDACIONES =====================
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function attributes(): array
|
||||
{
|
||||
return [
|
||||
'code' => 'código de usuario',
|
||||
'name' => 'nombre del usuario',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error messages for the defined validation rules.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function messages(): array
|
||||
{
|
||||
return [
|
||||
'code.unique' => 'Este código ya está en uso por otro usuario.',
|
||||
'name.required' => 'El nombre del usuario es obligatorio.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga el formulario con datos del usuario y actualiza las opciones dinámicas.
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function loadFormModel($id): void
|
||||
{
|
||||
parent::loadFormModel($id);
|
||||
|
||||
$this->work_center_options = $this->store_id
|
||||
? DB::table('store_work_centers')
|
||||
->where('store_id', $this->store_id)
|
||||
->pluck('name', 'id')
|
||||
->toArray()
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga el formulario para eliminar un usuario, actualizando las opciones necesarias.
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function loadFormModelForDeletion($id): void
|
||||
{
|
||||
parent::loadFormModelForDeletion($id);
|
||||
|
||||
$this->work_center_options = DB::table('store_work_centers')
|
||||
->where('store_id', $this->store_id)
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define las opciones de los selectores desplegables.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function options(): array
|
||||
{
|
||||
$storeCatalogService = app(StoreCatalogService::class);
|
||||
$contactCatalogService = app(ContactCatalogService::class);
|
||||
|
||||
return [
|
||||
'store_options' => $storeCatalogService->searchCatalog('stores', '', ['limit' => -1]),
|
||||
'manager_options' => $contactCatalogService->searchCatalog('users', '', ['limit' => -1]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa el documento recibido (CFDI XML o Constancia PDF).
|
||||
*/
|
||||
public function processDocument()
|
||||
{
|
||||
// Verificamos si el archivo es válido
|
||||
if (!$this->doc_file instanceof UploadedFile) {
|
||||
return $this->addError('doc_file', 'No se pudo recibir el archivo.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Validar tipo de archivo
|
||||
$this->validate([
|
||||
'doc_file' => 'required|mimes:pdf,xml|max:2048'
|
||||
]);
|
||||
|
||||
|
||||
// **Detectar el tipo de documento**
|
||||
$extension = strtolower($this->doc_file->getClientOriginalExtension());
|
||||
|
||||
// **Procesar según el tipo de archivo**
|
||||
switch ($extension) {
|
||||
case 'xml':
|
||||
$service = new FacturaXmlService();
|
||||
$data = $service->processUploadedFile($this->doc_file);
|
||||
break;
|
||||
|
||||
case 'pdf':
|
||||
$service = new ConstanciaFiscalService();
|
||||
$data = $service->extractData($this->doc_file);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Formato de archivo no soportado.");
|
||||
}
|
||||
|
||||
dd($data);
|
||||
|
||||
// **Asignar los valores extraídos al formulario**
|
||||
$this->rfc = $data['rfc'] ?? null;
|
||||
$this->name = $data['name'] ?? null;
|
||||
$this->email = $data['email'] ?? null;
|
||||
$this->tel = $data['telefono'] ?? null;
|
||||
//$this->direccion = $data['domicilio_fiscal'] ?? null;
|
||||
|
||||
// Ocultar el Dropzone después de procesar
|
||||
$this->dropzoneVisible = false;
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
$this->handleValidationException($e);
|
||||
|
||||
} catch (QueryException $e) {
|
||||
$this->handleDatabaseException($e);
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
$this->handleException('danger', 'Registro no encontrado.');
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->handleException('danger', 'Error al procesar el archivo: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruta de la vista asociada con este formulario.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function viewPath(): string
|
||||
{
|
||||
return 'vuexy-admin::livewire.users.offcanvas-form';
|
||||
}
|
||||
}
|
283
Livewire/Users/UserShow.php
Normal file
283
Livewire/Users/UserShow.php
Normal file
@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Livewire\Users;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Catalog\DropdownList;
|
||||
use Koneko\SatCatalogs\Models\UsoCfdi;
|
||||
use Koneko\SatCatalogs\Models\RegimenFiscal;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\Component;
|
||||
|
||||
class UserShow extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public $image;
|
||||
|
||||
public User $user;
|
||||
public $status_options, $pricelists_options;
|
||||
public $regimen_fiscal_options, $uso_cfdi_options;
|
||||
public $userId,
|
||||
$name,
|
||||
$cargo,
|
||||
$profile_photo,
|
||||
$profile_photo_path,
|
||||
$email,
|
||||
$password,
|
||||
$password_confirmation,
|
||||
$tipo_persona,
|
||||
$rfc,
|
||||
$nombre_fiscal,
|
||||
$c_regimen_fiscal,
|
||||
$domicilio_fiscal,
|
||||
$c_uso_cfdi,
|
||||
$pricelist_id,
|
||||
$enable_credit,
|
||||
$credit_days,
|
||||
$credit_limit,
|
||||
$is_prospect,
|
||||
$is_customer,
|
||||
$is_provider,
|
||||
$is_user,
|
||||
$status;
|
||||
public $deleteUserImage;
|
||||
public $cuentaUsuarioAlert,
|
||||
$accesosAlert,
|
||||
$facturacionElectronicaAlert;
|
||||
|
||||
// Reglas de validación para la cuenta de usuario
|
||||
protected $rulesUser = [
|
||||
'tipo_persona' => 'nullable|integer',
|
||||
'name' => 'required|string|min:3|max:255',
|
||||
'cargo' => 'nullable|string|min:3|max:255',
|
||||
'is_prospect' => 'nullable|boolean',
|
||||
'is_customer' => 'nullable|boolean',
|
||||
'is_provider' => 'nullable|boolean',
|
||||
'is_user' => 'nullable|boolean',
|
||||
'pricelist_id' => 'nullable|integer',
|
||||
'enable_credit' => 'nullable|boolean',
|
||||
'credit_days' => 'nullable|integer',
|
||||
'credit_limit' => 'nullable|numeric|min:0|max:9999999.99|regex:/^\d{1,7}(\.\d{1,2})?$/',
|
||||
'image' => 'nullable|mimes:jpg,png|image|max:20480', // 20MB Max
|
||||
];
|
||||
|
||||
// Reglas de validación para los campos fiscales
|
||||
protected $rulesFacturacion = [
|
||||
'rfc' => 'nullable|string|max:13',
|
||||
'domicilio_fiscal' => [
|
||||
'nullable',
|
||||
'regex:/^[0-9]{5}$/',
|
||||
'exists:sat_codigo_postal,c_codigo_postal'
|
||||
],
|
||||
'nombre_fiscal' => 'nullable|string|max:255',
|
||||
'c_regimen_fiscal' => 'nullable|integer',
|
||||
'c_uso_cfdi' => 'nullable|string',
|
||||
];
|
||||
|
||||
public function mount($userId)
|
||||
{
|
||||
$this->user = User::findOrFail($userId);
|
||||
|
||||
$this->reloadUserData();
|
||||
|
||||
$this->pricelists_options = DropdownList::selectList(DropdownList::POS_PRICELIST);
|
||||
|
||||
$this->status_options = [
|
||||
User::STATUS_ENABLED => User::$statusList[User::STATUS_ENABLED],
|
||||
User::STATUS_DISABLED => User::$statusList[User::STATUS_DISABLED],
|
||||
];
|
||||
|
||||
$this->regimen_fiscal_options = RegimenFiscal::selectList();
|
||||
$this->uso_cfdi_options = UsoCfdi::selectList();
|
||||
}
|
||||
|
||||
|
||||
public function reloadUserData()
|
||||
{
|
||||
$this->tipo_persona = $this->user->tipo_persona;
|
||||
$this->name = $this->user->name;
|
||||
$this->cargo = $this->user->cargo;
|
||||
$this->is_prospect = $this->user->is_prospect? true : false;
|
||||
$this->is_customer = $this->user->is_customer? true : false;
|
||||
$this->is_provider = $this->user->is_provider? true : false;
|
||||
$this->is_user = $this->user->is_user? true : false;
|
||||
$this->pricelist_id = $this->user->pricelist_id;
|
||||
$this->enable_credit = $this->user->enable_credit? true : false;
|
||||
$this->credit_days = $this->user->credit_days;
|
||||
$this->credit_limit = $this->user->credit_limit;
|
||||
$this->profile_photo = $this->user->profile_photo_url;
|
||||
$this->profile_photo_path = $this->user->profile_photo_path;
|
||||
$this->image = null;
|
||||
$this->deleteUserImage = false;
|
||||
|
||||
$this->status = $this->user->status;
|
||||
$this->email = $this->user->email;
|
||||
$this->password = null;
|
||||
$this->password_confirmation = null;
|
||||
|
||||
$this->rfc = $this->user->rfc;
|
||||
$this->domicilio_fiscal = $this->user->domicilio_fiscal;
|
||||
$this->nombre_fiscal = $this->user->nombre_fiscal;
|
||||
$this->c_regimen_fiscal = $this->user->c_regimen_fiscal;
|
||||
$this->c_uso_cfdi = $this->user->c_uso_cfdi;
|
||||
|
||||
$this->cuentaUsuarioAlert = null;
|
||||
$this->accesosAlert = null;
|
||||
$this->facturacionElectronicaAlert = null;
|
||||
}
|
||||
|
||||
|
||||
public function saveCuentaUsuario()
|
||||
{
|
||||
try {
|
||||
// Validar Información de usuario
|
||||
$validatedData = $this->validate($this->rulesUser);
|
||||
|
||||
$validatedData['name'] = trim($validatedData['name']);
|
||||
$validatedData['cargo'] = $validatedData['cargo']? trim($validatedData['cargo']): null;
|
||||
$validatedData['is_prospect'] = $validatedData['is_prospect'] ? 1 : 0;
|
||||
$validatedData['is_customer'] = $validatedData['is_customer'] ? 1 : 0;
|
||||
$validatedData['is_provider'] = $validatedData['is_provider'] ? 1 : 0;
|
||||
$validatedData['is_user'] = $validatedData['is_user'] ? 1 : 0;
|
||||
$validatedData['pricelist_id'] = $validatedData['pricelist_id'] ?: null;
|
||||
$validatedData['enable_credit'] = $validatedData['enable_credit'] ? 1 : 0;
|
||||
$validatedData['credit_days'] = $validatedData['credit_days'] ?: null;
|
||||
$validatedData['credit_limit'] = $validatedData['credit_limit'] ?: null;
|
||||
|
||||
if($this->tipo_persona == User::TIPO_RFC_PUBLICO){
|
||||
$validatedData['cargo'] = null;
|
||||
$validatedData['is_prospect'] = null;
|
||||
$validatedData['is_provider'] = null;
|
||||
$validatedData['is_user'] = null;
|
||||
$validatedData['enable_credit'] = null;
|
||||
$validatedData['credit_days'] = null;
|
||||
$validatedData['credit_limit'] = null;
|
||||
}
|
||||
|
||||
if(!$this->user->is_prospect && !$this->user->is_customer){
|
||||
$validatedData['pricelist_id'] = null;
|
||||
}
|
||||
|
||||
if(!$this->user->is_customer){
|
||||
$validatedData['enable_credit'] = null;
|
||||
$validatedData['credit_days'] = null;
|
||||
$validatedData['credit_limit'] = null;
|
||||
}
|
||||
|
||||
$this->user->update($validatedData);
|
||||
|
||||
|
||||
if($this->deleteUserImage && $this->user->profile_photo_path){
|
||||
$this->user->deleteProfilePhoto();
|
||||
|
||||
// Reiniciar variables después de la eliminación
|
||||
$this->deleteUserImage = false;
|
||||
$this->profile_photo_path = null;
|
||||
$this->profile_photo = $this->user->profile_photo_url;
|
||||
|
||||
}else if ($this->image) {
|
||||
$image = ImageManager::imagick()->read($this->image->getRealPath());
|
||||
$image = $image->scale(520, 520);
|
||||
|
||||
$imageName = $this->image->hashName(); // Genera un nombre único
|
||||
|
||||
$image->save(storage_path('app/public/profile-photos/' . $imageName));
|
||||
|
||||
$this->user->deleteProfilePhoto();
|
||||
|
||||
$this->profile_photo_path = $this->user->profile_photo_path = 'profile-photos/' . $imageName;
|
||||
$this->profile_photo = $this->user->profile_photo_url;
|
||||
$this->user->save();
|
||||
|
||||
unlink($this->image->getRealPath());
|
||||
|
||||
$this->reset('image');
|
||||
}
|
||||
|
||||
// Puedes también devolver un mensaje de éxito si lo deseas
|
||||
$this->setAlert('Se guardó los cambios exitosamente.', 'cuentaUsuarioAlert');
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
// Si hay errores de validación, los puedes capturar y manejar aquí
|
||||
$this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'cuentaUsuarioAlert', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
public function saveAccesos()
|
||||
{
|
||||
try {
|
||||
$validatedData = $this->validate([
|
||||
'status' => 'integer',
|
||||
'email' => ['required', 'email', 'unique:users,email,' . $this->user->id],
|
||||
'password' => ['nullable', 'string', 'min:6', 'max:32', 'confirmed'], // La regla 'confirmed' valida que ambas contraseñas coincidan
|
||||
], [
|
||||
'email.required' => 'El correo electrónico es obligatorio.',
|
||||
'email.email' => 'Debes ingresar un correo electrónico válido.',
|
||||
'email.unique' => 'Este correo ya está en uso.',
|
||||
'password.min' => 'La contraseña debe tener al menos 5 caracteres.',
|
||||
'password.max' => 'La contraseña no puede tener más de 32 caracteres.',
|
||||
'password.confirmed' => 'Las contraseñas no coinciden.',
|
||||
]);
|
||||
|
||||
// Si la validación es exitosa, continuar con el procesamiento
|
||||
$validatedData['email'] = trim($this->email);
|
||||
|
||||
if ($this->password)
|
||||
$validatedData['password'] = bcrypt($this->password);
|
||||
|
||||
else
|
||||
unset($validatedData['password']);
|
||||
|
||||
$this->user->update($validatedData);
|
||||
|
||||
$this->password = null;
|
||||
$this->password_confirmation = null;
|
||||
|
||||
// Puedes también devolver un mensaje de éxito si lo deseas
|
||||
$this->setAlert('Se guardó los cambios exitosamente.', 'accesosAlert');
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
// Si hay errores de validación, los puedes capturar y manejar aquí
|
||||
$this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'accesosAlert', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
public function saveFacturacionElectronica()
|
||||
{
|
||||
try {
|
||||
// Validar Información fiscal
|
||||
$validatedData = $this->validate($this->rulesFacturacion);
|
||||
|
||||
$validatedData['rfc'] = strtoupper(trim($validatedData['rfc'])) ?: null;
|
||||
$validatedData['domicilio_fiscal'] = $validatedData['domicilio_fiscal'] ?: null;
|
||||
$validatedData['nombre_fiscal'] = strtoupper(trim($validatedData['nombre_fiscal'])) ?: null;
|
||||
$validatedData['c_regimen_fiscal'] = $validatedData['c_regimen_fiscal'] ?: null;
|
||||
$validatedData['c_uso_cfdi'] = $validatedData['c_uso_cfdi'] ?: null;
|
||||
|
||||
$this->user->update($validatedData);
|
||||
|
||||
// Puedes también devolver un mensaje de éxito si lo deseas
|
||||
$this->setAlert('Se guardó los cambios exitosamente.', 'facturacionElectronicaAlert');
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
// Si hay errores de validación, los puedes capturar y manejar aquí
|
||||
$this->setAlert('Ocurrieron errores en la validación: ' . $e->validator->errors()->first(), 'facturacionElectronicaAlert', 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function setAlert($message, $alertName, $type = 'success')
|
||||
{
|
||||
$this->$alertName = [
|
||||
'message' => $message,
|
||||
'type' => $type
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.crm.contact-view');
|
||||
}
|
||||
}
|
62
Models/MediaItem.php
Normal file
62
Models/MediaItem.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MediaItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'url',
|
||||
'imageable_type',
|
||||
'imageable_id',
|
||||
'type',
|
||||
'sub_type',
|
||||
'url',
|
||||
'path',
|
||||
'title',
|
||||
'description',
|
||||
'order',
|
||||
];
|
||||
|
||||
// the list of types values that can be stored in table
|
||||
const TYPE_CARD = 1;
|
||||
const TYPE_BANNER = 2;
|
||||
const TYPE_COVER = 3;
|
||||
const TYPE_GALLERY = 4;
|
||||
const TYPE_BANNER_HOME = 5;
|
||||
const TYPE_CARD2 = 6;
|
||||
const TYPE_BANNER2 = 7;
|
||||
const TYPE_COVER2 = 8;
|
||||
|
||||
/**
|
||||
* List of names for each types.
|
||||
* @var array
|
||||
*/
|
||||
public static $typesList = [
|
||||
self::TYPE_CARD => 'Card',
|
||||
self::TYPE_BANNER => 'Banner',
|
||||
self::TYPE_COVER => 'Cover',
|
||||
self::TYPE_GALLERY => 'Gallery',
|
||||
self::TYPE_BANNER_HOME => 'Banner Home',
|
||||
self::TYPE_CARD2 => 'Card 2',
|
||||
self::TYPE_BANNER2 => 'Banner 2',
|
||||
self::TYPE_COVER2 => 'Cover 2',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the parent imageable model (user or post).
|
||||
*/
|
||||
public function imageable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
39
Models/Setting.php
Normal file
39
Models/Setting.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'value',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
// Relación con el usuario
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
// Scope para obtener configuraciones de un usuario específico
|
||||
public function scopeForUser($query, $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
// Configuraciones globales (sin usuario)
|
||||
public function scopeGlobal($query)
|
||||
{
|
||||
return $query->whereNull('user_id');
|
||||
}
|
||||
}
|
377
Models/User copy.php
Normal file
377
Models/User copy.php
Normal file
@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Models;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Intervention\Image\Typography\FontFactory;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
use OwenIt\Auditing\Auditable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use Koneko\VuexyAdmin\Notifications\CustomResetPasswordNotification;
|
||||
|
||||
if (trait_exists(\Koneko\VuexyContacts\Traits\HasContactsAttributes::class)) {
|
||||
trait DynamicContactsAttributes {
|
||||
use \Koneko\VuexyContacts\Traits\HasContactsAttributes;
|
||||
}
|
||||
} else {
|
||||
trait DynamicContactsAttributes {}
|
||||
}
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail, AuditableContract
|
||||
{
|
||||
use HasRoles,
|
||||
HasApiTokens,
|
||||
HasFactory,
|
||||
Notifiable,
|
||||
TwoFactorAuthenticatable,
|
||||
Auditable,
|
||||
DynamicContactsAttributes;
|
||||
|
||||
// the list of status values that can be stored in table
|
||||
const STATUS_ENABLED = 10;
|
||||
const STATUS_DISABLED = 1;
|
||||
const STATUS_REMOVED = 0;
|
||||
|
||||
const AVATAR_DISK = 'public';
|
||||
const PROFILE_PHOTO_DIR = 'profile-photos';
|
||||
const INITIAL_AVATAR_DIR = 'initial-avatars';
|
||||
const INITIAL_MAX_LENGTH = 4;
|
||||
|
||||
const AVATAR_WIDTH = 512;
|
||||
const AVATAR_HEIGHT = 512;
|
||||
const AVATAR_BACKGROUND = '#EBF4FF'; // Fondo por defecto
|
||||
const AVATAR_COLORS = [
|
||||
'#7367f0',
|
||||
'#808390',
|
||||
'#28c76f',
|
||||
'#ff4c51',
|
||||
'#ff9f43',
|
||||
'#00bad1',
|
||||
'#4b4b4b',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of names for each status.
|
||||
* @var array
|
||||
*/
|
||||
public static $statusList = [
|
||||
self::STATUS_ENABLED => 'Habilitado',
|
||||
self::STATUS_DISABLED => 'Deshabilitado',
|
||||
self::STATUS_REMOVED => 'Eliminado',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of names for each status.
|
||||
* @var array
|
||||
*/
|
||||
public static $statusListClass = [
|
||||
self::STATUS_ENABLED => 'success',
|
||||
self::STATUS_DISABLED => 'warning',
|
||||
self::STATUS_REMOVED => 'danger',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'last_name',
|
||||
'email',
|
||||
'password',
|
||||
'profile_photo_path',
|
||||
'status',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_secret',
|
||||
];
|
||||
|
||||
/**
|
||||
* The accessors to append to the model's array form.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $appends = [
|
||||
'profile_photo_url',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attributes to include in the Audit.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $auditInclude = [
|
||||
'name',
|
||||
'email',
|
||||
];
|
||||
|
||||
public function updateProfilePhoto(UploadedFile $image_avatar)
|
||||
{
|
||||
try {
|
||||
// Verificar si el archivo existe
|
||||
if (!file_exists($image_avatar->getRealPath()))
|
||||
throw new \Exception('El archivo no existe en la ruta especificada.');
|
||||
|
||||
if (!in_array($image_avatar->getClientOriginalExtension(), ['jpg', 'jpeg', 'png']))
|
||||
throw new \Exception('El formato del archivo debe ser JPG o PNG.');
|
||||
|
||||
// Directorio donde se guardarán los avatares
|
||||
$avatarDisk = self::AVATAR_DISK;
|
||||
$avatarPath = self::PROFILE_PHOTO_DIR;
|
||||
$avatarName = uniqid('avatar_') . '.png'; // Nombre único para el avatar
|
||||
|
||||
// Crear la instancia de ImageManager
|
||||
$driver = config('image.driver', 'gd');
|
||||
$manager = new ImageManager($driver);
|
||||
|
||||
// Crear el directorio si no existe
|
||||
if (!Storage::disk($avatarDisk)->exists($avatarPath))
|
||||
Storage::disk($avatarDisk)->makeDirectory($avatarPath);
|
||||
|
||||
// Leer la imagen
|
||||
$image = $manager->read($image_avatar->getRealPath());
|
||||
|
||||
// crop the best fitting 5:3 (600x360) ratio and resize to 600x360 pixel
|
||||
$image->cover(self::AVATAR_WIDTH, self::AVATAR_HEIGHT);
|
||||
|
||||
// Guardar la imagen en el disco de almacenamiento gestionado por Laravel
|
||||
Storage::disk($avatarDisk)->put($avatarPath . '/' . $avatarName, $image->toPng(indexed: true));
|
||||
|
||||
// Elimina el avatar existente si hay uno
|
||||
$this->deleteProfilePhoto();
|
||||
|
||||
// Update the user's profile photo path
|
||||
$this->forceFill([
|
||||
'profile_photo_path' => $avatarName,
|
||||
])->save();
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception('Ocurrió un error al actualizar el avatar. ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteProfilePhoto()
|
||||
{
|
||||
if (!empty($this->profile_photo_path)) {
|
||||
$avatarDisk = self::AVATAR_DISK;
|
||||
|
||||
Storage::disk($avatarDisk)->delete($this->profile_photo_path);
|
||||
|
||||
$this->forceFill([
|
||||
'profile_photo_path' => null,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAvatarColor()
|
||||
{
|
||||
// Selecciona un color basado en el id del usuario
|
||||
return self::AVATAR_COLORS[$this->id % count(self::AVATAR_COLORS)];
|
||||
}
|
||||
|
||||
public static function getAvatarImage($name, $color, $background, $size)
|
||||
{
|
||||
$avatarDisk = self::AVATAR_DISK;
|
||||
$directory = self::INITIAL_AVATAR_DIR;
|
||||
$initials = self::getInitials($name);
|
||||
|
||||
$cacheKey = "avatar-{$initials}-{$color}-{$background}-{$size}";
|
||||
$path = "{$directory}/{$cacheKey}.png";
|
||||
$storagePath = storage_path("app/public/{$path}");
|
||||
|
||||
// Verificar si el avatar ya está en caché
|
||||
if (Storage::disk($avatarDisk)->exists($path))
|
||||
return response()->file($storagePath);
|
||||
|
||||
// Crear el avatar
|
||||
$image = self::createAvatarImage($name, $color, $background, $size);
|
||||
|
||||
// Guardar en el directorio de iniciales
|
||||
Storage::disk($avatarDisk)->put($path, $image->toPng(indexed: true));
|
||||
|
||||
// Retornar la imagen directamente
|
||||
return response()->file($storagePath);
|
||||
}
|
||||
|
||||
private static function createAvatarImage($name, $color, $background, $size)
|
||||
{
|
||||
// Usar la configuración del driver de imagen
|
||||
$driver = config('image.driver', 'gd');
|
||||
$manager = new ImageManager($driver);
|
||||
|
||||
$initials = self::getInitials($name);
|
||||
|
||||
// Obtener la ruta correcta de la fuente dentro del paquete
|
||||
$fontPath = __DIR__ . '/../storage/fonts/OpenSans-Bold.ttf';
|
||||
|
||||
// Crear la imagen con fondo
|
||||
$image = $manager->create($size, $size)
|
||||
->fill($background);
|
||||
|
||||
// Escribir texto en la imagen
|
||||
$image->text(
|
||||
$initials,
|
||||
$size / 2, // Centrar horizontalmente
|
||||
$size / 2, // Centrar verticalmente
|
||||
function (FontFactory $font) use ($color, $size, $fontPath) {
|
||||
$font->file($fontPath);
|
||||
$font->size($size * 0.4);
|
||||
$font->color($color);
|
||||
$font->align('center');
|
||||
$font->valign('middle');
|
||||
}
|
||||
);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
public static function getInitials($name)
|
||||
{
|
||||
// Manejar casos de nombres vacíos o nulos
|
||||
if (empty($name))
|
||||
return 'NA';
|
||||
|
||||
// Usar array_map para mayor eficiencia
|
||||
$initials = implode('', array_map(function ($word) {
|
||||
return mb_substr($word, 0, 1);
|
||||
}, explode(' ', $name)));
|
||||
|
||||
$initials = substr($initials, 0, self::INITIAL_MAX_LENGTH);
|
||||
|
||||
return strtoupper($initials);
|
||||
}
|
||||
|
||||
public function getProfilePhotoUrlAttribute()
|
||||
{
|
||||
if ($this->profile_photo_path)
|
||||
return Storage::url(self::PROFILE_PHOTO_DIR . '/' . $this->profile_photo_path);
|
||||
|
||||
// Generar URL del avatar por iniciales
|
||||
$name = urlencode($this->fullname);
|
||||
$color = ltrim($this->getAvatarColor(), '#');
|
||||
$background = ltrim(self::AVATAR_BACKGROUND, '#');
|
||||
$size = (self::AVATAR_WIDTH + self::AVATAR_HEIGHT) / 2;
|
||||
|
||||
return url("/admin/usuario/avatar?name={$name}&color={$color}&background={$background}&size={$size}");
|
||||
}
|
||||
|
||||
public function getFullnameAttribute()
|
||||
{
|
||||
return trim($this->name . ' ' . $this->last_name);
|
||||
}
|
||||
|
||||
public function getInitialsAttribute()
|
||||
{
|
||||
return self::getInitials($this->fullname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía la notificación de restablecimiento de contraseña.
|
||||
*
|
||||
* @param string $token
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
// Usar la notificación personalizada
|
||||
$this->notify(new CustomResetPasswordNotification($token));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Obtener usuarios activos con una excepción para incluir un usuario específico desactivado.
|
||||
*
|
||||
* @param array $filters Filtros opcionales como ['type' => 'user', 'status' => 1]
|
||||
* @param int|null $includeUserId ID de usuario específico a incluir aunque esté inactivo
|
||||
* @return array
|
||||
*/
|
||||
public static function getUsersListWithInactive(int $includeUserId = null, array $filters = []): array
|
||||
{
|
||||
$query = self::query();
|
||||
|
||||
// Filtro por tipo de usuario
|
||||
if (isset($filters['type'])) {
|
||||
switch ($filters['type']) {
|
||||
case 'partner':
|
||||
$query->where('is_partner', 1);
|
||||
break;
|
||||
case 'employee':
|
||||
$query->where('is_employee', 1);
|
||||
break;
|
||||
case 'prospect':
|
||||
$query->where('is_prospect', 1);
|
||||
break;
|
||||
case 'customer':
|
||||
$query->where('is_customer', 1);
|
||||
break;
|
||||
case 'provider':
|
||||
$query->where('is_provider', 1);
|
||||
break;
|
||||
case 'user':
|
||||
$query->where('is_user', 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Incluir usuarios activos o el usuario desactivado seleccionado
|
||||
$query->where(function ($q) use ($filters, $includeUserId) {
|
||||
if (isset($filters['status'])) {
|
||||
$q->where('status', $filters['status']);
|
||||
}
|
||||
|
||||
if ($includeUserId) {
|
||||
$q->orWhere('id', $includeUserId);
|
||||
}
|
||||
});
|
||||
|
||||
// Formatear los datos como id => "Nombre Apellido"
|
||||
return $query->pluck(\DB::raw("CONCAT(name, ' ', IFNULL(last_name, ''))"), 'id')->toArray();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Relations
|
||||
*/
|
||||
|
||||
// User who created this user
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'created_by');
|
||||
}
|
||||
|
||||
public function isActive()
|
||||
{
|
||||
return $this->status === self::STATUS_ENABLED;
|
||||
}
|
||||
|
||||
}
|
237
Models/User.php
Normal file
237
Models/User.php
Normal file
@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Models;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
use OwenIt\Auditing\Auditable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use Koneko\VuexyAdmin\Notifications\CustomResetPasswordNotification;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail, AuditableContract
|
||||
{
|
||||
use HasRoles, HasApiTokens, HasFactory, Notifiable,
|
||||
TwoFactorAuthenticatable, Auditable;
|
||||
|
||||
// the list of status values that can be stored in table
|
||||
const STATUS_ENABLED = 10;
|
||||
const STATUS_DISABLED = 1;
|
||||
const STATUS_REMOVED = 0;
|
||||
|
||||
const AVATAR_DISK = 'public';
|
||||
const PROFILE_PHOTO_DIR = 'profile-photos';
|
||||
const INITIAL_AVATAR_DIR = 'initial-avatars';
|
||||
const INITIAL_MAX_LENGTH = 4;
|
||||
|
||||
const AVATAR_WIDTH = 512;
|
||||
const AVATAR_HEIGHT = 512;
|
||||
const AVATAR_BACKGROUND = '#EBF4FF'; // Fondo por defecto
|
||||
const AVATAR_COLORS = [
|
||||
'#7367f0',
|
||||
'#808390',
|
||||
'#28c76f',
|
||||
'#ff4c51',
|
||||
'#ff9f43',
|
||||
'#00bad1',
|
||||
'#4b4b4b',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of names for each status.
|
||||
* @var array
|
||||
*/
|
||||
public static $statusList = [
|
||||
self::STATUS_ENABLED => 'Habilitado',
|
||||
self::STATUS_DISABLED => 'Deshabilitado',
|
||||
self::STATUS_REMOVED => 'Eliminado',
|
||||
];
|
||||
|
||||
/**
|
||||
* List of names for each status.
|
||||
* @var array
|
||||
*/
|
||||
public static $statusListClass = [
|
||||
self::STATUS_ENABLED => 'success',
|
||||
self::STATUS_DISABLED => 'warning',
|
||||
self::STATUS_REMOVED => 'danger',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'last_name',
|
||||
'email',
|
||||
'password',
|
||||
'profile_photo_path',
|
||||
'status',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_secret',
|
||||
];
|
||||
|
||||
/**
|
||||
* The accessors to append to the model's array form.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $appends = [
|
||||
'profile_photo_url',
|
||||
];
|
||||
|
||||
/**
|
||||
* Nombre de la etiqueta para generar Componentes
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $tagName = 'User';
|
||||
|
||||
/**
|
||||
* Nombre de la columna que contiee el nombre del registro
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $columnNameLabel = 'full_name';
|
||||
|
||||
/**
|
||||
* Nombre singular del registro.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $singularName = 'usuario';
|
||||
|
||||
/**
|
||||
* Nombre plural del registro.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $pluralName = 'usuarios';
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attributes to include in the Audit.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $auditInclude = [
|
||||
'name',
|
||||
'email',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the full name of the user.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFullnameAttribute()
|
||||
{
|
||||
return trim($this->name . ' ' . $this->last_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initials of the user's full name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInitialsAttribute()
|
||||
{
|
||||
return self::getInitials($this->fullname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía la notificación de restablecimiento de contraseña.
|
||||
*
|
||||
* @param string $token
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
// Usar la notificación personalizada
|
||||
$this->notify(new CustomResetPasswordNotification($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener usuarios activos con una excepción para incluir un usuario específico desactivado.
|
||||
*
|
||||
* @param array $filters Filtros opcionales como ['type' => 'user', 'status' => 1]
|
||||
* @param int|null $includeUserId ID de usuario específico a incluir aunque esté inactivo
|
||||
* @return array
|
||||
*/
|
||||
public static function getUsersListWithInactive($includeUserId = null, array $filters = []): array
|
||||
{
|
||||
$query = self::query();
|
||||
|
||||
// Filtro por tipo de usuario dinámico
|
||||
$tipoUsuarios = [
|
||||
'partner' => 'is_partner',
|
||||
'employee' => 'is_employee',
|
||||
'prospect' => 'is_prospect',
|
||||
'customer' => 'is_customer',
|
||||
'provider' => 'is_provider',
|
||||
'user' => 'is_user',
|
||||
];
|
||||
|
||||
if (isset($filters['type']) && isset($tipoUsuarios[$filters['type']])) {
|
||||
$query->where($tipoUsuarios[$filters['type']], 1);
|
||||
}
|
||||
|
||||
// Filtrar por estado o incluir usuario inactivo
|
||||
$query->where(function ($q) use ($filters, $includeUserId) {
|
||||
if (isset($filters['status'])) {
|
||||
$q->where('status', $filters['status']);
|
||||
}
|
||||
|
||||
if ($includeUserId) {
|
||||
$q->orWhere('id', $includeUserId);
|
||||
}
|
||||
});
|
||||
|
||||
return $query->pluck(\DB::raw("CONCAT(name, ' ', IFNULL(last_name, ''))"), 'id')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* User who created this user
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is active
|
||||
*/
|
||||
public function isActive()
|
||||
{
|
||||
return $this->status === self::STATUS_ENABLED;
|
||||
}
|
||||
|
||||
}
|
14
Models/UserLogin.php
Normal file
14
Models/UserLogin.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserLogin extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'ip_address',
|
||||
'user_agent'
|
||||
];
|
||||
}
|
117
Notifications/CustomResetPasswordNotification.php
Normal file
117
Notifications/CustomResetPasswordNotification.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Notifications;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
class CustomResetPasswordNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* Crea una nueva instancia de notificación.
|
||||
*/
|
||||
public function __construct($token)
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configura el canal de la notificación.
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configura el mensaje de correo.
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
try {
|
||||
// Cargar configuración SMTP desde la base de datos
|
||||
$this->loadDynamicMailConfig();
|
||||
|
||||
$resetUrl = url(route('password.reset', [
|
||||
'token' => $this->token,
|
||||
'email' => $notifiable->getEmailForPasswordReset()
|
||||
], false));
|
||||
|
||||
$appTitle = Setting::global()->where('key', 'website_title')->first()->value ?? Config::get('koneko.appTitle');
|
||||
$imageBase64 = 'data:image/png;base64,' . base64_encode(file_get_contents(public_path('/assets/img/logo/koneko-04.png')));
|
||||
$expireMinutes = Config::get('auth.passwords.' . Config::get('auth.defaults.passwords') . '.expire', 60);
|
||||
|
||||
Config::set('app.name', $appTitle);
|
||||
|
||||
return (new MailMessage)
|
||||
->subject("Restablece tu contraseña - {$appTitle}")
|
||||
->markdown('vuexy-admin::notifications.email', [ // Usar tu plantilla del módulo
|
||||
'greeting' => "Hola {$notifiable->name}",
|
||||
'introLines' => [
|
||||
'Estás recibiendo este correo porque solicitaste restablecer tu contraseña.',
|
||||
],
|
||||
'actionText' => 'Restablecer contraseña',
|
||||
'actionUrl' => $resetUrl,
|
||||
'outroLines' => [
|
||||
"Este enlace expirará en {$expireMinutes} minutos.",
|
||||
'Si no solicitaste este cambio, no se requiere realizar ninguna acción.',
|
||||
],
|
||||
'displayableActionUrl' => $resetUrl, // Para el subcopy
|
||||
'image' => $imageBase64, // Imagen del logo
|
||||
]);
|
||||
|
||||
/*
|
||||
*/
|
||||
} catch (\Exception $e) {
|
||||
// Registrar el error
|
||||
Log::error('Error al enviar el correo de restablecimiento: ' . $e->getMessage());
|
||||
|
||||
// Retornar un mensaje alternativo
|
||||
return (new MailMessage)
|
||||
->subject('Restablece tu contraseña')
|
||||
->line('Ocurrió un error al enviar el correo. Por favor, intenta de nuevo más tarde.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cargar configuración SMTP desde la base de datos.
|
||||
*/
|
||||
protected function loadDynamicMailConfig()
|
||||
{
|
||||
try {
|
||||
$smtpConfig = Setting::where('key', 'LIKE', 'mail_%')
|
||||
->pluck('value', 'key');
|
||||
|
||||
if ($smtpConfig->isEmpty()) {
|
||||
throw new Exception('No SMTP configuration found in the database.');
|
||||
}
|
||||
|
||||
Config::set('mail.mailers.smtp.host', $smtpConfig['mail_mailers_smtp_host'] ?? null);
|
||||
Config::set('mail.mailers.smtp.port', $smtpConfig['mail_mailers_smtp_port'] ?? null);
|
||||
Config::set('mail.mailers.smtp.username', $smtpConfig['mail_mailers_smtp_username'] ?? null);
|
||||
Config::set(
|
||||
'mail.mailers.smtp.password',
|
||||
isset($smtpConfig['mail_mailers_smtp_password'])
|
||||
? Crypt::decryptString($smtpConfig['mail_mailers_smtp_password'])
|
||||
: null
|
||||
);
|
||||
Config::set('mail.mailers.smtp.encryption', $smtpConfig['mail_mailers_smtp_encryption'] ?? null);
|
||||
Config::set('mail.from.address', $smtpConfig['mail_from_address'] ?? null);
|
||||
Config::set('mail.from.name', $smtpConfig['mail_from_name'] ?? null);
|
||||
} catch (Exception $e) {
|
||||
Log::error('SMTP Configuration Error: ' . $e->getMessage());
|
||||
// Opcional: Puedes lanzar la excepción o manejarla de otra manera.
|
||||
throw new Exception('Error al cargar la configuración SMTP.');
|
||||
}
|
||||
}
|
||||
}
|
31
Providers/ConfigServiceProvider.php
Normal file
31
Providers/ConfigServiceProvider.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
|
||||
|
||||
class ConfigServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
// Cargar configuración del sistema
|
||||
$this->mergeConfigFrom(__DIR__.'/../config/vuexy.php', 'vuexy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
// Cargar configuración del sistema
|
||||
$globalSettingsService = app(GlobalSettingsService::class);
|
||||
$globalSettingsService->loadSystemConfig();
|
||||
|
||||
// Cargar configuración del sistema a través del servicio
|
||||
app(GlobalSettingsService::class)->loadSystemConfig();
|
||||
}
|
||||
}
|
124
Providers/FortifyServiceProvider.php
Normal file
124
Providers/FortifyServiceProvider.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\Fortify;
|
||||
use Koneko\VuexyAdmin\Actions\Fortify\CreateNewUser;
|
||||
use Koneko\VuexyAdmin\Actions\Fortify\ResetUserPassword;
|
||||
use Koneko\VuexyAdmin\Actions\Fortify\UpdateUserPassword;
|
||||
use Koneko\VuexyAdmin\Actions\Fortify\UpdateUserProfileInformation;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyAdmin\Services\AdminTemplateService;
|
||||
|
||||
class FortifyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
|
||||
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())) . '|' . $request->ip());
|
||||
|
||||
return Limit::perMinute(5)->by($throttleKey);
|
||||
});
|
||||
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||
});
|
||||
|
||||
Fortify::authenticateUsing(function (Request $request) {
|
||||
$user = User::where('email', $request->email)
|
||||
->where('status', User::STATUS_ENABLED)
|
||||
->first();
|
||||
|
||||
if ($user && Hash::check($request->password, $user->password)) {
|
||||
return $user;
|
||||
}
|
||||
});
|
||||
|
||||
// Simula lo que hace tu middleware y comparte `_admin`
|
||||
$viewMode = Config::get('vuexy.custom.authViewMode');
|
||||
$adminVars = app(AdminTemplateService::class)->getAdminVars();
|
||||
|
||||
// Configurar la vista del login
|
||||
Fortify::loginView(function () use ($viewMode, $adminVars) {
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
view()->share('_admin', $adminVars);
|
||||
|
||||
return view("vuexy-admin::auth.login-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
});
|
||||
|
||||
// Configurar la vista del registro (si lo necesitas)
|
||||
Fortify::registerView(function () use ($viewMode, $adminVars) {
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
view()->share('_admin', $adminVars);
|
||||
|
||||
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
});
|
||||
|
||||
// Configurar la vista de restablecimiento de contraseñas
|
||||
Fortify::requestPasswordResetLinkView(function () use ($viewMode, $adminVars) {
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
view()->share('_admin', $adminVars);
|
||||
|
||||
return view("vuexy-admin::auth.forgot-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
});
|
||||
|
||||
Fortify::resetPasswordView(function ($request) use ($viewMode, $adminVars) {
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
view()->share('_admin', $adminVars);
|
||||
|
||||
return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs, 'request' => $request]);
|
||||
});
|
||||
|
||||
// Vista de verificación de correo electrónico
|
||||
Fortify::verifyEmailView(function () use ($viewMode, $adminVars) {
|
||||
view()->share('_admin', $adminVars);
|
||||
|
||||
return view("vuexy-admin::auth.verify-email-{$viewMode}");
|
||||
});
|
||||
|
||||
// Vista de confirmación de contraseña
|
||||
Fortify::confirmPasswordView(function () use ($viewMode, $adminVars) {
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
view()->share('_admin', $adminVars);
|
||||
|
||||
return view("vuexy-admin::auth.confirm-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
});
|
||||
|
||||
// Configurar la vista para la verificación de dos factores
|
||||
Fortify::twoFactorChallengeView(function () use ($viewMode, $adminVars) {
|
||||
$pageConfigs = ['myLayout' => 'blank'];
|
||||
|
||||
view()->share('_admin', $adminVars);
|
||||
|
||||
return view("vuexy-admin::auth.two-factor-challenge-{$viewMode}", ['pageConfigs' => $pageConfigs]);
|
||||
});
|
||||
}
|
||||
}
|
132
Providers/VuexyAdminServiceProvider.php
Normal file
132
Providers/VuexyAdminServiceProvider.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Providers;
|
||||
|
||||
use Koneko\VuexyAdmin\Http\Middleware\AdminTemplateMiddleware;
|
||||
use Koneko\VuexyAdmin\Listeners\{ClearUserCache,HandleUserLogin};
|
||||
use Koneko\VuexyAdmin\Livewire\Users\{UserIndex,UserShow,UserForm,UserOffCanvasForm};
|
||||
use Koneko\VuexyAdmin\Livewire\Roles\RoleIndex;
|
||||
use Koneko\VuexyAdmin\Livewire\Permissions\PermissionIndex;
|
||||
use Koneko\VuexyAdmin\Livewire\Cache\{CacheFunctions,CacheStats,SessionStats,MemcachedStats,RedisStats};
|
||||
use Koneko\VuexyAdmin\Livewire\AdminSettings\{ApplicationSettings,GeneralSettings,InterfaceSettings,MailSmtpSettings,MailSenderResponseSettings};
|
||||
use Koneko\VuexyAdmin\Console\Commands\CleanInitialAvatars;
|
||||
use Koneko\VuexyAdmin\Helpers\VuexyHelper;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Support\Facades\{URL,Event,Blade};
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Foundation\AliasLoader;
|
||||
use Illuminate\Auth\Events\{Login,Logout};
|
||||
use Livewire\Livewire;
|
||||
use OwenIt\Auditing\AuditableObserver;
|
||||
use Spatie\Permission\PermissionServiceProvider;
|
||||
|
||||
class VuexyAdminServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Cargar configuraciones personalizadas
|
||||
$this->mergeConfigFrom(__DIR__.'/../config/koneko.php', 'koneko');
|
||||
|
||||
// Register the module's services and providers
|
||||
$this->app->register(ConfigServiceProvider::class);
|
||||
$this->app->register(FortifyServiceProvider::class);
|
||||
$this->app->register(PermissionServiceProvider::class);
|
||||
|
||||
// Register the module's aliases
|
||||
AliasLoader::getInstance()->alias('Helper', VuexyHelper::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if(env('FORCE_HTTPS', false)){
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
// Registrar alias del middleware
|
||||
$this->app['router']->aliasMiddleware('admin', AdminTemplateMiddleware::class);
|
||||
|
||||
// Sobrescribir ruta de traducciones para asegurar que se usen las del paquete
|
||||
$this->app->bind('path.lang', function () {
|
||||
return __DIR__ . '/../resources/lang';
|
||||
});
|
||||
|
||||
// Register the module's routes
|
||||
$this->loadRoutesFrom(__DIR__.'/../routes/admin.php');
|
||||
|
||||
|
||||
// Cargar vistas del paquete
|
||||
$this->loadViewsFrom(__DIR__.'/../resources/views', 'vuexy-admin');
|
||||
|
||||
// Registrar Componentes Blade
|
||||
Blade::componentNamespace('VuexyAdmin\\View\\Components', 'vuexy-admin');
|
||||
|
||||
|
||||
// Publicar los archivos necesarios
|
||||
$this->publishes([
|
||||
__DIR__.'/../config/fortify.php' => config_path('fortify.php'),
|
||||
__DIR__.'/../config/image.php' => config_path('image.php'),
|
||||
__DIR__.'/../config/vuexy_menu.php' => config_path('vuexy_menu.php'),
|
||||
], 'vuexy-admin-config');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__.'/../database/seeders/' => database_path('seeders'),
|
||||
__DIR__.'/../database/data' => database_path('data'),
|
||||
], 'vuexy-admin-seeders');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__.'/../resources/img' => public_path('vendor/vuexy-admin/img'),
|
||||
], 'vuexy-admin-images');
|
||||
|
||||
|
||||
// Register the migrations
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
||||
|
||||
|
||||
// Registrar eventos
|
||||
Event::listen(Login::class, HandleUserLogin::class);
|
||||
Event::listen(Logout::class, ClearUserCache::class);
|
||||
|
||||
|
||||
// Registrar comandos de consola
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
CleanInitialAvatars::class,
|
||||
]);
|
||||
}
|
||||
|
||||
// Registrar Livewire Components
|
||||
$components = [
|
||||
'user-index' => UserIndex::class,
|
||||
'user-show' => UserShow::class,
|
||||
'user-form' => UserForm::class,
|
||||
'user-offcanvas-form' => UserOffCanvasForm::class,
|
||||
'role-index' => RoleIndex::class,
|
||||
'permission-index' => PermissionIndex::class,
|
||||
|
||||
|
||||
'general-settings' => GeneralSettings::class,
|
||||
'application-settings' => ApplicationSettings::class,
|
||||
'interface-settings' => InterfaceSettings::class,
|
||||
'mail-smtp-settings' => MailSmtpSettings::class,
|
||||
'mail-sender-response-settings' => MailSenderResponseSettings::class,
|
||||
'cache-stats' => CacheStats::class,
|
||||
'session-stats' => SessionStats::class,
|
||||
'redis-stats' => RedisStats::class,
|
||||
'memcached-stats' => MemcachedStats::class,
|
||||
'cache-functions' => CacheFunctions::class,
|
||||
];
|
||||
|
||||
foreach ($components as $alias => $component) {
|
||||
Livewire::component($alias, $component);
|
||||
}
|
||||
|
||||
// Registrar auditoría en usuarios
|
||||
User::observe(AuditableObserver::class);
|
||||
}
|
||||
}
|
104
Queries/BootstrapTableQueryBuilder.php
Normal file
104
Queries/BootstrapTableQueryBuilder.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Queries;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
abstract class BootstrapTableQueryBuilder
|
||||
{
|
||||
protected $query;
|
||||
protected $request;
|
||||
protected $config;
|
||||
|
||||
public function __construct(Request $request, array $config)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->config = $config;
|
||||
$this->query = DB::table($config['table']);
|
||||
|
||||
$this->applyJoins();
|
||||
$this->applyFilters();
|
||||
}
|
||||
|
||||
protected function applyJoins()
|
||||
{
|
||||
if (!empty($this->config['joins'])) {
|
||||
foreach ($this->config['joins'] as $join) {
|
||||
$type = $join['type'] ?? 'join';
|
||||
|
||||
$this->query->{$type}($join['table'], function($joinObj) use ($join) {
|
||||
$joinObj->on($join['first'], '=', $join['second']);
|
||||
|
||||
// Soporte para AND en ON, si está definidio
|
||||
if (!empty($join['and'])) {
|
||||
foreach ((array) $join['and'] as $andCondition) {
|
||||
// 'sat_codigo_postal.c_estado = sat_localidad.c_estado'
|
||||
$parts = explode('=', $andCondition);
|
||||
|
||||
if (count($parts) === 2) {
|
||||
$left = trim($parts[0]);
|
||||
$right = trim($parts[1]);
|
||||
|
||||
$joinObj->whereRaw("$left = $right");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyFilters()
|
||||
{
|
||||
if (!empty($this->config['filters'])) {
|
||||
foreach ($this->config['filters'] as $filter => $column) {
|
||||
if ($this->request->filled($filter)) {
|
||||
$this->query->where($column, 'LIKE', '%' . $this->request->input($filter) . '%');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyGrouping()
|
||||
{
|
||||
if (!empty($this->config['group_by'])) {
|
||||
$this->query->groupBy($this->config['group_by']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getJson()
|
||||
{
|
||||
$this->applyGrouping();
|
||||
|
||||
// Calcular total de filas antes de aplicar paginación
|
||||
$total = DB::select("SELECT COUNT(*) as num_rows FROM (" . $this->query->selectRaw('0')->toSql() . ") as items", $this->query->getBindings())[0]->num_rows;
|
||||
|
||||
// Para ver la sentencia SQL (con placeholders ?)
|
||||
//dump($this->query->toSql()); dd($this->query->getBindings());
|
||||
|
||||
// Aplicar orden, paginación y selección de columnas
|
||||
$this->query
|
||||
->select($this->config['columns'])
|
||||
->when($this->request->input('sort'), function ($query) {
|
||||
$query->orderBy($this->request->input('sort'), $this->request->input('order', 'asc'));
|
||||
})
|
||||
->when($this->request->input('offset'), function ($query) {
|
||||
$query->offset($this->request->input('offset'));
|
||||
})
|
||||
->limit($this->request->input('limit', 10));
|
||||
|
||||
// Obtener resultados y limpiar los datos antes de enviarlos
|
||||
$rows = $this->query->get()->map(function ($item) {
|
||||
return collect($item)
|
||||
->reject(fn($val) => is_null($val) || $val === '') // Eliminar valores nulos o vacíos
|
||||
->map(fn($val) => is_numeric($val) ? (float) $val : $val) // Convertir números correctamente
|
||||
->toArray();
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
"total" => $total,
|
||||
"rows" => $rows,
|
||||
]);
|
||||
}
|
||||
}
|
8
Queries/GenericQueryBuilder.php
Normal file
8
Queries/GenericQueryBuilder.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Queries;
|
||||
|
||||
class GenericQueryBuilder extends BootstrapTableQueryBuilder
|
||||
{
|
||||
// Custom query builder
|
||||
}
|
223
README.md
223
README.md
@ -1,185 +1,130 @@
|
||||
# 🎨 Laravel Vuexy Admin
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
<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="mailto:contacto@koneko.mx"><img src="https://img.shields.io/badge/contact-email-green" alt="Email"></a>
|
||||
<a href="https://git.koneko.mx/koneko"><img src="https://img.shields.io/badge/Git%20Server-Koneko%20Git-orange" alt="Servidor Git"></a>
|
||||
<a href="https://github.com/koneko-mx/laravel-vuexy-admin/actions/workflows/tests.yml"><img src="https://github.com/koneko-mx/laravel-vuexy-admin/actions/workflows/tests.yml/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://github.com/koneko-mx/laravel-vuexy-admin/issues"><img src="https://img.shields.io/github/issues/koneko/laravel-vuexy-admin" alt="Issues"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
# Laravel Vuexy Admin para México
|
||||
## 📌 Descripción
|
||||
|
||||
**Laravel Vuexy Admin para México** es un proyecto basado en Laravel optimizado para necesidades específicas del mercado mexicano. Incluye integración con los catálogos del SAT (CFDI 4.0), herramientas avanzadas y una interfaz moderna inspirada en el template premium Vuexy.
|
||||
**Laravel Vuexy Admin** es un módulo de administración optimizado para México, basado en Laravel 11 y diseñado para integrarse con **Vuexy Admin Template**. Incluye gestión avanzada de usuarios, roles, permisos y auditoría de acciones.
|
||||
|
||||
## Características destacadas
|
||||
|
||||
- **Optimización para México**:
|
||||
- Uso de los catálogos oficiales del SAT (versión CFDI 4.0):
|
||||
- Banco (`sat_banco`)
|
||||
- Clave de Producto o Servicio (`sat_clave_prod_serv`)
|
||||
- Clave de Unidad (`sat_clave_unidad`)
|
||||
- Forma de Pago (`sat_forma_pago`)
|
||||
- Moneda (`sat_moneda`)
|
||||
- Código Postal (`sat_codigo_postal`)
|
||||
- Régimen Fiscal (`sat_regimen_fiscal`)
|
||||
- País (`sat_pais`)
|
||||
- Uso CFDI (`sat_uso_cfdi`)
|
||||
- Colonia (`sat_colonia`)
|
||||
- Estado (`sat_estado`)
|
||||
- Localidad (`sat_localidad`)
|
||||
- Municipio (`sat_municipio`)
|
||||
- Deducción (`sat_deduccion`)
|
||||
- Percepción (`sat_percepcion`)
|
||||
- Compatible con los lineamientos y formatos del Anexo 20 del SAT.
|
||||
- Útil para generar comprobantes fiscales digitales (CFDI) y otros procesos administrativos locales.
|
||||
|
||||
- **Otras características avanzadas**:
|
||||
- Autenticación y gestión de usuarios con Laravel Fortify.
|
||||
- Gestión de roles y permisos usando Spatie Permission.
|
||||
- Tablas dinámicas con Laravel Datatables y Yajra.
|
||||
- Integración con Redis para caching eficiente.
|
||||
- Exportación y manejo de Excel mediante Maatwebsite.
|
||||
|
||||
## Requisitos del Sistema
|
||||
|
||||
- **PHP**: >= 8.2
|
||||
- **Composer**: >= 2.0
|
||||
- **Node.js**: >= 16.x
|
||||
- **MySQL** o cualquier base de datos compatible con Laravel.
|
||||
### ✨ Características
|
||||
- 🔹 Sistema de autenticación con Laravel Fortify.
|
||||
- 🔹 Gestión avanzada de usuarios con Livewire.
|
||||
- 🔹 Control de roles y permisos con Spatie Permissions.
|
||||
- 🔹 Auditoría de acciones con Laravel Auditing.
|
||||
- 🔹 Publicación de configuraciones y vistas.
|
||||
- 🔹 Soporte para cache y optimización de rendimiento.
|
||||
|
||||
---
|
||||
|
||||
## Instalación
|
||||
## 📦 Instalación
|
||||
|
||||
Este proyecto ofrece dos métodos de instalación: mediante Composer o manualmente. A continuación, te explicamos ambos procesos.
|
||||
|
||||
### Opción 1: Usar Composer (Recomendado)
|
||||
|
||||
Para instalar el proyecto rápidamente usando Composer, ejecuta el siguiente comando:
|
||||
Instalar vía **Composer**:
|
||||
|
||||
```bash
|
||||
composer create-project koneko/laravel-vuexy-admin
|
||||
composer require koneko/laravel-vuexy-admin
|
||||
```
|
||||
|
||||
Este comando realizará automáticamente los siguientes pasos:
|
||||
1. Configurará el archivo `.env` basado en `.env.example`.
|
||||
2. Generará la clave de la aplicación.
|
||||
|
||||
Una vez completado, debes configurar una base de datos válida en el archivo `.env` y luego ejecutar:
|
||||
Publicar archivos de configuración y migraciones:
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=vuexy-admin-config
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Uso básico
|
||||
|
||||
```php
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
|
||||
$user = User::create([
|
||||
'name' => 'Juan Pérez',
|
||||
'email' => 'juan@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Configuración adicional
|
||||
|
||||
Si necesitas personalizar la configuración del módulo, publica el archivo de configuración:
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=vuexy-admin-config
|
||||
```
|
||||
|
||||
Esto generará `config/vuexy_menu.php`, donde puedes modificar valores predeterminados.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Dependencias
|
||||
|
||||
Este paquete requiere las siguientes dependencias:
|
||||
- Laravel 11
|
||||
- `laravel/fortify` (autenticación)
|
||||
- `spatie/laravel-permission` (gestión de roles y permisos)
|
||||
- `owen-it/laravel-auditing` (auditoría de usuarios)
|
||||
- `livewire/livewire` (interfaz dinámica)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Publicación de Assets y Configuraciones
|
||||
|
||||
Para publicar configuraciones y seeders:
|
||||
|
||||
```bash
|
||||
php artisan vendor:publish --tag=vuexy-admin-config
|
||||
php artisan vendor:publish --tag=vuexy-admin-seeders
|
||||
php artisan migrate --seed
|
||||
```
|
||||
|
||||
Finalmente, compila los activos iniciales:
|
||||
Para publicar imágenes del tema:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Inicia el servidor local con:
|
||||
|
||||
```bash
|
||||
php artisan serve
|
||||
php artisan vendor:publish --tag=vuexy-admin-images
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Opción 2: Instalación manual
|
||||
## 🌍 Repositorio Principal y Sincronización
|
||||
|
||||
Si prefieres instalar el proyecto de forma manual, sigue estos pasos:
|
||||
Este repositorio es una **copia sincronizada** del repositorio principal alojado en **[Tea - Koneko Git](https://git.koneko.mx/koneko/laravel-vuexy-admin)**.
|
||||
|
||||
1. Clona el repositorio:
|
||||
```bash
|
||||
git clone https://git.koneko.mx/Koneko-ST/laravel-vuexy-admin.git
|
||||
cd laravel-vuexy-admin
|
||||
```
|
||||
### 🔄 Sincronización con GitHub
|
||||
- **Repositorio Principal:** [git.koneko.mx](https://git.koneko.mx/koneko/laravel-vuexy-admin)
|
||||
- **Repositorio en GitHub:** [github.com/koneko-mx/laravel-vuexy-admin](https://github.com/koneko-mx/laravel-vuexy-admin)
|
||||
- **Los cambios pueden reflejarse primero en Tea antes de GitHub.**
|
||||
|
||||
2. Instala las dependencias de Composer:
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
### 🤝 Contribuciones
|
||||
Si deseas contribuir:
|
||||
1. Puedes abrir un **Issue** en [GitHub Issues](https://github.com/koneko-mx/laravel-vuexy-admin/issues).
|
||||
2. Para Pull Requests, **preferimos contribuciones en Tea**. Contacta a `admin@koneko.mx` para solicitar acceso.
|
||||
|
||||
3. Instala las dependencias de npm:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Configura las variables de entorno:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
5. Configura una base de datos válida en el archivo `.env`.
|
||||
|
||||
6. Genera la clave de la aplicación:
|
||||
```bash
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
7. Migra y llena la base de datos:
|
||||
```bash
|
||||
php artisan migrate --seed
|
||||
```
|
||||
|
||||
8. Compila los activos frontend:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
9. Inicia el servidor de desarrollo:
|
||||
```bash
|
||||
php artisan serve
|
||||
```
|
||||
⚠️ **Nota:** Algunos cambios pueden tardar en reflejarse en GitHub, ya que este repositorio se actualiza automáticamente desde Tea.
|
||||
|
||||
---
|
||||
|
||||
## Notas importantes
|
||||
## 🏅 Licencia
|
||||
|
||||
- Asegúrate de tener instalado:
|
||||
- **PHP**: >= 8.2
|
||||
- **Composer**: >= 2.0
|
||||
- **Node.js**: >= 16.x
|
||||
- Este proyecto utiliza los catálogos SAT de la versión CFDI 4.0. Si deseas más información, visita la documentación oficial del SAT en [Anexo 20](http://omawww.sat.gob.mx/tramitesyservicios/Paginas/anexo_20.htm).
|
||||
Este paquete es de código abierto bajo la licencia [MIT](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
## Uso del Template Vuexy
|
||||
|
||||
Este proyecto está diseñado para funcionar con el template premium [Vuexy](https://themeforest.net/item/vuexy-vuejs-html-laravel-admin-dashboard-template/23328599). Para utilizarlo:
|
||||
|
||||
1. Adquiere una licencia válida de Vuexy en [ThemeForest](https://themeforest.net/item/vuexy-vuejs-html-laravel-admin-dashboard-template/23328599).
|
||||
2. Incluye los archivos necesarios en las carpetas correspondientes (`resources`, `public`, etc.) de este proyecto.
|
||||
|
||||
---
|
||||
|
||||
## Créditos
|
||||
|
||||
Este proyecto utiliza herramientas y recursos de código abierto, así como un template premium. Queremos agradecer a los desarrolladores y diseñadores que hacen posible esta implementación:
|
||||
|
||||
- [Laravel](https://laravel.com)
|
||||
- [Vuexy Template](https://themeforest.net/item/vuexy-vuejs-html-laravel-admin-dashboard-template/23328599)
|
||||
- [Spatie Permission](https://spatie.be/docs/laravel-permission)
|
||||
- [Yajra Datatables](https://yajrabox.com/docs/laravel-datatables)
|
||||
|
||||
---
|
||||
|
||||
## Licencia
|
||||
|
||||
Este proyecto está licenciado bajo la licencia MIT. Consulta el archivo [LICENSE](LICENSE) para más detalles.
|
||||
|
||||
El template "Vuexy" debe adquirirse por separado y está sujeto a su propia licencia comercial.
|
||||
|
||||
---
|
||||
|
||||
<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);
|
||||
}
|
||||
}
|
@ -1,40 +1,28 @@
|
||||
{
|
||||
"name": "koneko/laravel-vuexy-admin-module",
|
||||
"description": "Base modular para proyectos Laravel altamente personalizados.",
|
||||
"name": "koneko/laravel-vuexy-admin",
|
||||
"description": "Laravel Vuexy Admin, un modulo de administracion optimizado para México.",
|
||||
"keywords": ["laravel", "koneko", "framework", "vuexy", "admin", "mexico"],
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"intervention/image-laravel": "^1.3",
|
||||
"intervention/image-laravel": "^1.4",
|
||||
"laravel/framework": "^11.31",
|
||||
"laravel/fortify": "^1.25",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"livewire/livewire": "^3.5",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"owen-it/laravel-auditing": "^13.6",
|
||||
"spatie/laravel-permission": "^6.10",
|
||||
"yajra/laravel-datatables-oracle": "^11.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.14",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.26",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"spatie/laravel-ignition": "^2.4"
|
||||
"spatie/laravel-permission": "^6.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Koneko\\VuexyAdminModule\\": "src/"
|
||||
"Koneko\\VuexyAdmin\\": ""
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Koneko\\VuexyAdminModule\\BaseServiceProvider"
|
||||
"Koneko\\VuexyAdmin\\Providers\\VuexyAdminServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -43,5 +31,11 @@
|
||||
"name": "Arturo Corro Pacheco",
|
||||
"email": "arturo@koneko.mx"
|
||||
}
|
||||
]
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/koneko-mx/laravel-vuexy-admin",
|
||||
"issues": "https://github.com/koneko-mx/laravel-vuexy-admin/issues"
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
159
config/fortify.php
Normal file
159
config/fortify.php
Normal file
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Guard
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which authentication guard Fortify will use while
|
||||
| authenticating users. This value should correspond with one of your
|
||||
| guards that is already present in your "auth" configuration file.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => 'web',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Password Broker
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which password broker Fortify can use when a user
|
||||
| is resetting their password. This configured value should match one
|
||||
| of your password brokers setup in your "auth" configuration file.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => 'users',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Username / Email
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value defines which model attribute should be considered as your
|
||||
| application's "username" field. Typically, this might be the email
|
||||
| address of the users but you are free to change this value here.
|
||||
|
|
||||
| Out of the box, Fortify expects forgot password and reset password
|
||||
| requests to have a field named 'email'. If the application uses
|
||||
| another name for the field you may define it below as needed.
|
||||
|
|
||||
*/
|
||||
|
||||
'username' => 'email',
|
||||
|
||||
'email' => 'email',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Lowercase Usernames
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value defines whether usernames should be lowercased before saving
|
||||
| them in the database, as some database system string fields are case
|
||||
| sensitive. You may disable this for your application if necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'lowercase_usernames' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Home Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the path where users will get redirected during
|
||||
| authentication or password reset when the operations are successful
|
||||
| and the user is authenticated. You are free to change this value.
|
||||
|
|
||||
*/
|
||||
|
||||
'home' => '/admin',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Routes Prefix / Subdomain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which prefix Fortify will assign to all the routes
|
||||
| that it registers with the application. If necessary, you may change
|
||||
| subdomain under which all of the Fortify routes will be available.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => '',
|
||||
|
||||
'domain' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Routes Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which middleware Fortify will assign to the routes
|
||||
| that it registers with the application. If necessary, you may change
|
||||
| these middleware but typically this provided default is preferred.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Rate Limiting
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default, Fortify will throttle logins to five requests per minute for
|
||||
| every email and IP address combination. However, if you would like to
|
||||
| specify a custom rate limiter to call then you may specify it here.
|
||||
|
|
||||
*/
|
||||
|
||||
'limiters' => [
|
||||
'login' => 'login',
|
||||
'two-factor' => 'two-factor',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register View Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify if the routes returning views should be disabled as
|
||||
| you may not need them when building your own application. This may be
|
||||
| especially true if you're writing a custom single-page application.
|
||||
|
|
||||
*/
|
||||
|
||||
'views' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Features
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some of the Fortify features are optional. You may disable the features
|
||||
| by removing them from this array. You're free to only remove some of
|
||||
| these features or you can even remove all of these if you need to.
|
||||
|
|
||||
*/
|
||||
|
||||
'features' => [
|
||||
Features::registration(),
|
||||
Features::resetPasswords(),
|
||||
Features::emailVerification(),
|
||||
Features::updateProfileInformation(),
|
||||
Features::updatePasswords(),
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
'window' => 1,
|
||||
]),
|
||||
],
|
||||
|
||||
];
|
42
config/image.php
Normal file
42
config/image.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Image Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Intervention Image supports “GD Library” and “Imagick” to process images
|
||||
| internally. Depending on your PHP setup, you can choose one of them.
|
||||
|
|
||||
| Included options:
|
||||
| - \Intervention\Image\Drivers\Gd\Driver::class
|
||||
| - \Intervention\Image\Drivers\Imagick\Driver::class
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => \Intervention\Image\Drivers\Imagick\Driver::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Configuration Options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options control the behavior of Intervention Image.
|
||||
|
|
||||
| - "autoOrientation" controls whether an imported image should be
|
||||
| automatically rotated according to any existing Exif data.
|
||||
|
|
||||
| - "decodeAnimation" decides whether a possibly animated image is
|
||||
| decoded as such or whether the animation is discarded.
|
||||
|
|
||||
| - "blendingColor" Defines the default blending color.
|
||||
*/
|
||||
|
||||
'options' => [
|
||||
'autoOrientation' => true,
|
||||
'decodeAnimation' => true,
|
||||
'blendingColor' => 'ffffff',
|
||||
]
|
||||
];
|
14
config/koneko.php
Normal file
14
config/koneko.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
// Variables
|
||||
return [
|
||||
"appName" => "koneko.mx",
|
||||
"appTitle" => "Koneko Soluciones Tecnológicas",
|
||||
"appDescription" => "Koneko Soluciones Tecnológicas",
|
||||
"appLogo" => "../vendor/vuexy-admin/img/logo/koneko-04.png",
|
||||
"appFavicon" => "../vendor/vuexy-admin/img/logo/koneko-04.png",
|
||||
"author" => "arturo@koneko.mx",
|
||||
"creatorName" => "Koneko Soluciones Tecnológicas",
|
||||
"creatorUrl" => "https://koneko.mx",
|
||||
"licenseUrl" => "https://koneko.mx/koneko-admin/licencia",
|
||||
"supportUrl" => "https://koneko.mx/soporte",
|
||||
];
|
36
config/vuexy.php
Normal file
36
config/vuexy.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
// Custom Config
|
||||
// -------------------------------------------------------------------------------------
|
||||
//! IMPORTANT: Make sure you clear the browser local storage In order to see the config changes in the template.
|
||||
//! To clear local storage: (https://www.leadshook.com/help/how-to-clear-local-storage-in-google-chrome-browser/).
|
||||
|
||||
return [
|
||||
'custom' => [
|
||||
'myLayout' => 'horizontal', // Options[String]: vertical(default), horizontal
|
||||
'myTheme' => 'theme-semi-dark', // Options[String]: theme-default(default), theme-bordered, theme-semi-dark
|
||||
'myStyle' => 'light', // Options[String]: light(default), dark & system mode
|
||||
'myRTLSupport' => false, // options[Boolean]: true(default), false // To provide RTLSupport or not
|
||||
'myRTLMode' => false, // options[Boolean]: false(default), true // To set layout to RTL layout (myRTLSupport must be true for rtl mode)
|
||||
'hasCustomizer' => true, // options[Boolean]: true(default), false // Display customizer or not THIS WILL REMOVE INCLUDED JS FILE. SO LOCAL STORAGE WON'T WORK
|
||||
'displayCustomizer' => true, // options[Boolean]: true(default), false // Display customizer UI or not, THIS WON'T REMOVE INCLUDED JS FILE. SO LOCAL STORAGE WILL WORK
|
||||
'contentLayout' => 'compact', // options[String]: 'compact', 'wide' (compact=container-xxl, wide=container-fluid)
|
||||
'navbarType' => 'static', // options[String]: 'sticky', 'static', 'hidden' (Only for vertical Layout)
|
||||
'footerFixed' => false, // options[Boolean]: false(default), true // Footer Fixed
|
||||
'menuFixed' => false, // options[Boolean]: true(default), false // Layout(menu) Fixed (Only for vertical Layout)
|
||||
'menuCollapsed' => true, // options[Boolean]: false(default), true // Show menu collapsed, (Only for vertical Layout)
|
||||
'headerType' => 'static', // options[String]: 'static', 'fixed' (for horizontal layout only)
|
||||
'showDropdownOnHover' => false, // true, false (for horizontal layout only)
|
||||
'authViewMode' => 'cover', // Options[String]: cover(default), basic
|
||||
'maxQuickLinks' => 8, // options[Integer]: 6(default), 8, 10
|
||||
'customizerControls' => [
|
||||
//'rtl',
|
||||
'style',
|
||||
'headerType',
|
||||
'contentLayout',
|
||||
'layoutCollapsed',
|
||||
'layoutNavbarOptions',
|
||||
'themes',
|
||||
], // To show/hide customizer options
|
||||
],
|
||||
];
|
848
config/vuexy_menu.php
Normal file
848
config/vuexy_menu.php
Normal file
@ -0,0 +1,848 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'Inicio' => [
|
||||
'breadcrumbs' => false,
|
||||
'icon' => 'menu-icon tf-icons ti ti-home',
|
||||
'submenu' => [
|
||||
'Inicio' => [
|
||||
'route' => 'admin.core.home.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-home',
|
||||
],
|
||||
'Sitio Web' => [
|
||||
'url' => env('APP_URL'),
|
||||
'icon' => 'menu-icon tf-icons ti ti-world-www',
|
||||
],
|
||||
'Ajustes' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-settings-cog',
|
||||
'submenu' => [
|
||||
'Aplicación' => [
|
||||
'submenu' => [
|
||||
'Ajustes generales' => [
|
||||
'route' => 'admin.core.general-settings.index',
|
||||
'can' => 'admin.core.general-settings.allow',
|
||||
],
|
||||
'Ajustes de caché' => [
|
||||
'route' => 'admin.core.cache-manager.index',
|
||||
'can' => 'admin.core.cache-manager.view',
|
||||
],
|
||||
'Servidor de correo SMTP' => [
|
||||
'route' => 'admin.core.smtp-settings.index',
|
||||
'can' => 'admin.core.smtp-settings.allow',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Empresa' => [
|
||||
'submenu' => [
|
||||
'Información general' => [
|
||||
'route' => 'admin.store-manager.company.index',
|
||||
'can' => 'admin.store-manager.company.view',
|
||||
],
|
||||
'Sucursales' => [
|
||||
'route' => 'admin.store-manager.stores.index',
|
||||
'can' => 'admin.store-manager.stores.view',
|
||||
],
|
||||
'Centros de trabajo' => [
|
||||
'route' => 'admin.store-manager.work-centers.index',
|
||||
'can' => 'admin.store-manager.stores.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'BANXICO' => [
|
||||
'route' => 'admin.finance.banxico.index',
|
||||
'can' => 'admin.finance.banxico.allow',
|
||||
],
|
||||
'Conectividad bancaria' => [
|
||||
'route' => 'admin.finance.banking.index',
|
||||
'can' => 'admin.finance.banking.allow',
|
||||
],
|
||||
'Punto de venta' => [
|
||||
'submenu' => [
|
||||
'Ticket' => [
|
||||
'route' => 'admin.sales.ticket-config.index',
|
||||
'can' => 'admin.sales.ticket-config.allow',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Facturación' => [
|
||||
'submenu' => [
|
||||
'Certificados de Sello Digital' => [
|
||||
'route' => 'admin.billing.csds-settings.index',
|
||||
'can' => 'admin.billing.csds-settings.allow',
|
||||
],
|
||||
'Paquete de timbrado' => [
|
||||
'route' => 'admin.billing.stamping-package.index',
|
||||
'can' => 'admin.billing.stamping-package.allow',
|
||||
],
|
||||
'Servidor de correo SMTP' => [
|
||||
'route' => 'admin.billing.smtp-settings.index',
|
||||
'can' => 'admin.billing.smtp-settings.allow',
|
||||
],
|
||||
'Descarga masiva de CFDI' => [
|
||||
'route' => 'admin.billing.mass-cfdi-download.index',
|
||||
'can' => 'admin.billing.mass-cfdi-download.allow',
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
'Sistema' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-user-cog',
|
||||
'submenu' => [
|
||||
'Usuarios' => [
|
||||
'route' => 'admin.core.users.index',
|
||||
'can' => 'admin.core.users.view',
|
||||
],
|
||||
'Roles' => [
|
||||
'route' => 'admin.core.roles.index',
|
||||
'can' => 'admin.core.roles.view',
|
||||
],
|
||||
'Permisos' => [
|
||||
'route' => 'admin.core.permissions.index',
|
||||
'can' => 'admin.core.permissions.view',
|
||||
]
|
||||
]
|
||||
],
|
||||
'Catálogos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-library',
|
||||
'submenu' => [
|
||||
'Importar catálogos SAT' => [
|
||||
'route' => 'admin.core.sat-catalogs.index',
|
||||
'can' => 'admin.core.sat-catalogs.allow',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Configuración de cuenta' => [
|
||||
'route' => 'admin.core.user-profile.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-user-cog',
|
||||
],
|
||||
'Acerca de' => [
|
||||
'route' => 'admin.core.about.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-cat',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Herramientas Avanzadas' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-device-ipad-cog',
|
||||
'submenu' => [
|
||||
'Asistente AI' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-brain',
|
||||
'submenu' => [
|
||||
'Panel de IA' => [
|
||||
'route' => 'admin.ai.dashboard.index',
|
||||
'can' => 'ai.dashboard.view',
|
||||
],
|
||||
'Generación de Contenidos' => [
|
||||
'route' => 'admin.ai.content.index',
|
||||
'can' => 'ai.content.create',
|
||||
],
|
||||
'Análisis de Datos' => [
|
||||
'route' => 'admin.ai.analytics.index',
|
||||
'can' => 'ai.analytics.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Chatbot' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-message-chatbot',
|
||||
'submenu' => [
|
||||
'Configuración' => [
|
||||
'route' => 'admin.chatbot.config.index',
|
||||
'can' => 'chatbot.config.view',
|
||||
],
|
||||
'Flujos de Conversación' => [
|
||||
'route' => 'admin.chatbot.flows.index',
|
||||
'can' => 'chatbot.flows.manage',
|
||||
],
|
||||
'Historial de Interacciones' => [
|
||||
'route' => 'admin.chatbot.history.index',
|
||||
'can' => 'chatbot.history.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'IoT Box' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-cpu',
|
||||
'submenu' => [
|
||||
'Dispositivos Conectados' => [
|
||||
'route' => 'admin.iot.devices.index',
|
||||
'can' => 'iot.devices.view',
|
||||
],
|
||||
'Sensores y Configuración' => [
|
||||
'route' => 'admin.iot.sensors.index',
|
||||
'can' => 'iot.sensors.manage',
|
||||
],
|
||||
'Monitoreo en Tiempo Real' => [
|
||||
'route' => 'admin.iot.monitoring.index',
|
||||
'can' => 'iot.monitoring.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Reconocimiento Facial' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-face-id',
|
||||
'submenu' => [
|
||||
'Gestión de Perfiles' => [
|
||||
'route' => 'admin.facial-recognition.profiles.index',
|
||||
'can' => 'facial-recognition.profiles.manage',
|
||||
],
|
||||
'Verificación en Vivo' => [
|
||||
'route' => 'admin.facial-recognition.live.index',
|
||||
'can' => 'facial-recognition.live.verify',
|
||||
],
|
||||
'Historial de Accesos' => [
|
||||
'route' => 'admin.facial-recognition.history.index',
|
||||
'can' => 'facial-recognition.history.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Servidor de Impresión' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-printer',
|
||||
'submenu' => [
|
||||
'Cola de Impresión' => [
|
||||
'route' => 'admin.print.queue.index',
|
||||
'can' => 'print.queue.view',
|
||||
],
|
||||
'Historial de Impresiones' => [
|
||||
'route' => 'admin.print.history.index',
|
||||
'can' => 'print.history.view',
|
||||
],
|
||||
'Configuración de Impresoras' => [
|
||||
'route' => 'admin.print.settings.index',
|
||||
'can' => 'print.settings.manage',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Sitio Web' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-tools',
|
||||
'submenu' => [
|
||||
'Ajustes generales' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-tools',
|
||||
'route' => 'admin.website.general-settings.index',
|
||||
'can' => 'website.general-settings.allow',
|
||||
],
|
||||
'Avisos legales' => [
|
||||
'route' => 'admin.website.legal.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-writing-sign',
|
||||
'can' => 'website.legal.view',
|
||||
],
|
||||
'Preguntas frecuentes' => [
|
||||
'route' => 'admin.website.faq.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-bubble-text',
|
||||
'can' => 'website.faq.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Blog' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-news',
|
||||
'submenu' => [
|
||||
'Categorias' => [
|
||||
'route' => 'admin.blog.categories.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-category',
|
||||
'can' => 'blog.categories.view',
|
||||
],
|
||||
'Etiquetas' => [
|
||||
'route' => 'admin.blog.tags.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-tags',
|
||||
'can' => 'blog.tags.view',
|
||||
],
|
||||
'Articulos' => [
|
||||
'route' => 'admin.blog.articles.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-news',
|
||||
'can' => 'blog.articles.view',
|
||||
],
|
||||
'Comentarios' => [
|
||||
'route' => 'admin.blog.comments.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-message',
|
||||
'can' => 'blog.comments.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Contactos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-users',
|
||||
'submenu' => [
|
||||
'Contactos' => [
|
||||
'route' => 'admin.crm.contacts.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-users',
|
||||
'can' => 'crm.contacts.view',
|
||||
],
|
||||
'Campañas de marketing' => [
|
||||
'route' => 'admin.crm.marketing-campaigns.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-ad-2',
|
||||
'can' => 'crm.marketing-campaigns.view',
|
||||
],
|
||||
'Oportunidades ' => [
|
||||
'route' => 'admin.crm.leads.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-target-arrow',
|
||||
'can' => 'crm.leads.view',
|
||||
],
|
||||
'Newsletter' => [
|
||||
'route' => 'admin.crm.newsletter.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-notebook',
|
||||
'can' => 'crm.newsletter.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'RRHH' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-users-group',
|
||||
'submenu' => [
|
||||
'Gestión de Empleados' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-id-badge-2',
|
||||
'submenu' => [
|
||||
'Lista de Empleados' => [
|
||||
'route' => 'admin.rrhh.employees.index',
|
||||
'can' => 'rrhh.employees.view',
|
||||
],
|
||||
'Agregar Nuevo Empleado' => [
|
||||
'route' => 'admin.rrhh.employees.create',
|
||||
'can' => 'rrhh.employees.create',
|
||||
],
|
||||
'Puestos de trabajo' => [
|
||||
'route' => 'admin.rrhh.jobs.index',
|
||||
'can' => 'rrhh.jobs.view',
|
||||
],
|
||||
'Estructura Organizacional' => [
|
||||
'route' => 'admin.rrhh.organization.index',
|
||||
'can' => 'rrhh.organization.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Reclutamiento' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-user-search',
|
||||
'submenu' => [
|
||||
'Vacantes Disponibles' => [
|
||||
'route' => 'admin.recruitment.jobs.index',
|
||||
'can' => 'recruitment.jobs.view',
|
||||
],
|
||||
'Seguimiento de Candidatos' => [
|
||||
'route' => 'admin.recruitment.candidates.index',
|
||||
'can' => 'recruitment.candidates.view',
|
||||
],
|
||||
'Entrevistas y Evaluaciones' => [
|
||||
'route' => 'admin.recruitment.interviews.index',
|
||||
'can' => 'recruitment.interviews.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Nómina' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-cash',
|
||||
'submenu' => [
|
||||
'Contratos' => [
|
||||
'route' => 'admin.payroll.contracts.index',
|
||||
'can' => 'payroll.contracts.view',
|
||||
],
|
||||
'Procesar Nómina' => [
|
||||
'route' => 'admin.payroll.process.index',
|
||||
'can' => 'payroll.process.view',
|
||||
],
|
||||
'Recibos de Nómina' => [
|
||||
'route' => 'admin.payroll.receipts.index',
|
||||
'can' => 'payroll.receipts.view',
|
||||
],
|
||||
'Reportes Financieros' => [
|
||||
'route' => 'admin.payroll.reports.index',
|
||||
'can' => 'payroll.reports.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Asistencia' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-calendar-exclamation',
|
||||
'submenu' => [
|
||||
'Registro de Horarios' => [
|
||||
'route' => 'admin.attendance.records.index',
|
||||
'can' => 'attendance.records.view',
|
||||
],
|
||||
'Asistencia con Biométricos' => [
|
||||
'route' => 'admin.attendance.biometric.index',
|
||||
'can' => 'attendance.biometric.view',
|
||||
],
|
||||
'Justificación de Ausencias' => [
|
||||
'route' => 'admin.attendance.absences.index',
|
||||
'can' => 'attendance.absences.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Productos y servicios' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-package',
|
||||
'submenu' => [
|
||||
'Categorias' => [
|
||||
'route' => 'admin.inventory.product-categories.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-category',
|
||||
'can' => 'admin.inventory.product-categories.view',
|
||||
],
|
||||
'Catálogos' => [
|
||||
'route' => 'admin.inventory.product-catalogs.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-library',
|
||||
'can' => 'admin.inventory.product-catalogs.view',
|
||||
],
|
||||
'Productos y servicios' => [
|
||||
'route' => 'admin.inventory.products.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-packages',
|
||||
'can' => 'admin.inventory.products.view',
|
||||
],
|
||||
'Agregar producto o servicio' => [
|
||||
'route' => 'admin.inventory.products.create',
|
||||
'icon' => 'menu-icon tf-icons ti ti-package',
|
||||
'can' => 'admin.inventory.products.create',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Ventas' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-cash-register',
|
||||
'submenu' => [
|
||||
'Tablero' => [
|
||||
'route' => 'admin.sales.dashboard.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
|
||||
'can' => 'admin.sales.dashboard.allow',
|
||||
],
|
||||
'Clientes' => [
|
||||
'route' => 'admin.sales.customers.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-users-group',
|
||||
'can' => 'admin.sales.customers.view',
|
||||
],
|
||||
'Lista de precios' => [
|
||||
'route' => 'admin.sales.pricelist.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-report-search',
|
||||
'can' => 'admin.sales.sales.view',
|
||||
],
|
||||
'Cotizaciones' => [
|
||||
'route' => 'admin.sales.quotes.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-file-dollar',
|
||||
'can' => 'admin.sales.quotes.view',
|
||||
],
|
||||
'Ventas' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-cash-register',
|
||||
'submenu' => [
|
||||
'Crear venta' => [
|
||||
'route' => 'admin.sales.sales.create',
|
||||
'can' => 'admin.sales.sales.create',
|
||||
],
|
||||
'Ventas' => [
|
||||
'route' => 'admin.sales.sales.index',
|
||||
'can' => 'admin.sales.sales.view',
|
||||
],
|
||||
'Ventas por producto o servicio' => [
|
||||
'route' => 'admin.sales.sales-by-product.index',
|
||||
'can' => 'admin.sales.sales.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Remisiones' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-receipt',
|
||||
'submenu' => [
|
||||
'Crear remisión' => [
|
||||
'route' => 'admin.sales.remissions.create',
|
||||
'can' => 'admin.sales.remissions.create',
|
||||
],
|
||||
'Remisiones' => [
|
||||
'route' => 'admin.sales.remissions.index',
|
||||
'can' => 'admin.sales.remissions.view',
|
||||
],
|
||||
'Remisiones por producto o servicio' => [
|
||||
'route' => 'admin.sales.remissions-by-product.index',
|
||||
'can' => 'admin.sales.remissions.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Notas de crédito' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-receipt-refund',
|
||||
'submenu' => [
|
||||
'Crear nota de crédito' => [
|
||||
'route' => 'admin.sales.credit-notes.create',
|
||||
'can' => 'admin.sales.credit-notes.create',
|
||||
],
|
||||
'Notas de créditos' => [
|
||||
'route' => 'admin.sales.credit-notes.index',
|
||||
'can' => 'admin.sales.credit-notes.view',
|
||||
],
|
||||
'Notas de crédito por producto o servicio' => [
|
||||
'route' => 'admin.sales.credit-notes-by-product.index',
|
||||
'can' => 'admin.sales.credit-notes.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
'Finanzas' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-coins',
|
||||
'submenu' => [
|
||||
'Tablero Financiero' => [
|
||||
'route' => 'admin.accounting.dashboard.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
|
||||
'can' => 'accounting.dashboard.view',
|
||||
],
|
||||
'Contabilidad' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-pie',
|
||||
'submenu' => [
|
||||
'Cuentas Contables' => [
|
||||
'route' => 'admin.accounting.charts.index',
|
||||
'can' => 'accounting.charts.view',
|
||||
],
|
||||
'Cuentas por pagar' => [
|
||||
'route' => 'admin.finance.accounts-payable.index',
|
||||
'can' => 'finance.accounts-payable.view',
|
||||
],
|
||||
'Cuentas por cobrar' => [
|
||||
'route' => 'admin.finance.accounts-receivable.index',
|
||||
'can' => 'finance.accounts-receivable.view',
|
||||
],
|
||||
'Balance General' => [
|
||||
'route' => 'admin.accounting.balance.index',
|
||||
'can' => 'accounting.balance.view',
|
||||
],
|
||||
'Estado de Resultados' => [
|
||||
'route' => 'admin.accounting.income-statement.index',
|
||||
'can' => 'accounting.income-statement.view',
|
||||
],
|
||||
'Libro Mayor' => [
|
||||
'route' => 'admin.accounting.ledger.index',
|
||||
'can' => 'accounting.ledger.view',
|
||||
],
|
||||
'Registros Contables' => [
|
||||
'route' => 'admin.accounting.entries.index',
|
||||
'can' => 'accounting.entries.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Tablero de Gastos' => [
|
||||
'route' => 'admin.expenses.dashboard.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
|
||||
'can' => 'expenses.dashboard.view',
|
||||
],
|
||||
'Gestión de Gastos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-receipt-2',
|
||||
'submenu' => [
|
||||
'Nuevo gasto' => [
|
||||
'route' => 'admin.expenses.expenses.create',
|
||||
'can' => 'expenses.expenses.create',
|
||||
],
|
||||
'Gastos' => [
|
||||
'route' => 'admin.expenses.expenses.index',
|
||||
'can' => 'expenses.expenses.view',
|
||||
],
|
||||
'Categorías de Gastos' => [
|
||||
'route' => 'admin.expenses.categories.index',
|
||||
'can' => 'expenses.categories.view',
|
||||
],
|
||||
'Historial de Gastos' => [
|
||||
'route' => 'admin.expenses.history.index',
|
||||
'can' => 'expenses.history.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
|
||||
|
||||
'Facturación' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-rubber-stamp',
|
||||
'submenu' => [
|
||||
'Tablero' => [
|
||||
'route' => 'admin.billing.dashboard.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
|
||||
'can' => 'admin.billing.dashboard.allow',
|
||||
],
|
||||
'Ingresos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-file-certificate',
|
||||
'submenu' => [
|
||||
'Facturar ventas' => [
|
||||
'route' => 'admin.billing.ingresos-stamp.index',
|
||||
'can' => 'admin.billing.ingresos.create',
|
||||
],
|
||||
'CFDI Ingresos' => [
|
||||
'route' => 'admin.billing.ingresos.index',
|
||||
'can' => 'admin.billing.ingresos.view',
|
||||
],
|
||||
'CFDI Ingresos por producto o servicio' => [
|
||||
'route' => 'admin.billing.ingresos-by-product.index',
|
||||
'can' => 'admin.billing.ingresos.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Egresos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-file-certificate',
|
||||
'submenu' => [
|
||||
'Facturar notas de crédito' => [
|
||||
'route' => 'admin.billing.egresos-stamp.index',
|
||||
'can' => 'admin.billing.egresos.create',
|
||||
],
|
||||
'CFDI Engresos' => [
|
||||
'route' => 'admin.billing.egresos.index',
|
||||
'can' => 'admin.billing.egresos.view',
|
||||
],
|
||||
'CFDI Engresos por producto o servicio' => [
|
||||
'route' => 'admin.billing.egresos-by-product.index',
|
||||
'can' => 'admin.billing.egresos.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Pagos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-file-certificate',
|
||||
'submenu' => [
|
||||
'Facturar pagos' => [
|
||||
'route' => 'admin.billing.pagos-stamp.index',
|
||||
'can' => 'admin.billing.pagos.created',
|
||||
],
|
||||
'CFDI Pagos' => [
|
||||
'route' => 'admin.billing.pagos.index',
|
||||
'can' => 'admin.billing.pagos.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'CFDI Nómina' => [
|
||||
'route' => 'admin.billing.nomina.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-file-certificate',
|
||||
'can' => 'admin.billing.nomina.view',
|
||||
],
|
||||
'Verificador de CFDI 4.0' => [
|
||||
'route' => 'admin.billing.verify-cfdi.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-rosette-discount-check',
|
||||
'can' => 'admin.billing.verify-cfdi.allow',
|
||||
],
|
||||
]
|
||||
],
|
||||
|
||||
'Inventario y Logística' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-truck-delivery',
|
||||
'submenu' => [
|
||||
'Cadena de Suministro' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-dots-3',
|
||||
'submenu' => [
|
||||
'Proveedores' => [
|
||||
'route' => 'admin.inventory.suppliers.index',
|
||||
'can' => 'admin.inventory.suppliers.view',
|
||||
],
|
||||
'Órdenes de Compra' => [
|
||||
'route' => 'admin.inventory.orders.index',
|
||||
'can' => 'admin.inventory.orders.view',
|
||||
],
|
||||
'Recepción de Productos' => [
|
||||
'route' => 'admin.inventory.reception.index',
|
||||
'can' => 'admin.inventory.reception.view',
|
||||
],
|
||||
'Gestión de Insumos' => [
|
||||
'route' => 'admin.inventory.materials.index',
|
||||
'can' => 'admin.inventory.materials.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Gestión de Almacenes' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-building-warehouse',
|
||||
'submenu' => [
|
||||
'Almacenes' => [
|
||||
'route' => 'admin.inventory.warehouse.index',
|
||||
'can' => 'admin.inventory.warehouse.view',
|
||||
],
|
||||
'Stock de Inventario' => [
|
||||
'route' => 'admin.inventory.stock.index',
|
||||
'can' => 'admin.inventory.stock.view',
|
||||
],
|
||||
'Movimientos de almacenes' => [
|
||||
'route' => 'admin.inventory.movements.index',
|
||||
'can' => 'admin.inventory.movements.view',
|
||||
],
|
||||
'Transferencias entre Almacenes' => [
|
||||
'route' => 'admin.inventory.transfers.index',
|
||||
'can' => 'admin.inventory.transfers.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Envíos y Logística' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-truck',
|
||||
'submenu' => [
|
||||
'Órdenes de Envío' => [
|
||||
'route' => 'admin.inventory.shipping-orders.index',
|
||||
'can' => 'admin.inventory.shipping-orders.view',
|
||||
],
|
||||
'Seguimiento de Envíos' => [
|
||||
'route' => 'admin.inventory.shipping-tracking.index',
|
||||
'can' => 'admin.inventory.shipping-tracking.view',
|
||||
],
|
||||
'Transportistas' => [
|
||||
'route' => 'admin.inventory.shipping-carriers.index',
|
||||
'can' => 'admin.inventory.shipping-carriers.view',
|
||||
],
|
||||
'Tarifas y Métodos de Envío' => [
|
||||
'route' => 'admin.inventory.shipping-rates.index',
|
||||
'can' => 'admin.inventory.shipping-rates.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Gestión de Activos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-tools-kitchen',
|
||||
'submenu' => [
|
||||
'Activos Registrados' => [
|
||||
'route' => 'admin.inventory.asset.index',
|
||||
'can' => 'admin.inventory.asset.view',
|
||||
],
|
||||
'Mantenimiento Preventivo' => [
|
||||
'route' => 'admin.inventory.asset-maintenance.index',
|
||||
'can' => 'admin.inventory.asset-maintenance.view',
|
||||
],
|
||||
'Control de Vida Útil' => [
|
||||
'route' => 'admin.inventory.asset-lifecycle.index',
|
||||
'can' => 'admin.inventory.asset-lifecycle.view',
|
||||
],
|
||||
'Asignación de Activos' => [
|
||||
'route' => 'admin.inventory.asset-assignments.index',
|
||||
'can' => 'admin.inventory.asset-assignments.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'Gestión Empresarial' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-briefcase',
|
||||
'submenu' => [
|
||||
'Gestión de Proyectos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-layout-kanban',
|
||||
'submenu' => [
|
||||
'Tablero de Proyectos' => [
|
||||
'route' => 'admin.projects.dashboard.index',
|
||||
'can' => 'projects.dashboard.view',
|
||||
],
|
||||
'Proyectos Activos' => [
|
||||
'route' => 'admin.projects.index',
|
||||
'can' => 'projects.view',
|
||||
],
|
||||
'Crear Proyecto' => [
|
||||
'route' => 'admin.projects.create',
|
||||
'can' => 'projects.create',
|
||||
],
|
||||
'Gestión de Tareas' => [
|
||||
'route' => 'admin.projects.tasks.index',
|
||||
'can' => 'projects.tasks.view',
|
||||
],
|
||||
'Historial de Proyectos' => [
|
||||
'route' => 'admin.projects.history.index',
|
||||
'can' => 'projects.history.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Producción y Manufactura' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-building-factory',
|
||||
'submenu' => [
|
||||
'Órdenes de Producción' => [
|
||||
'route' => 'admin.production.orders.index',
|
||||
'can' => 'production.orders.view',
|
||||
],
|
||||
'Nueva Orden de Producción' => [
|
||||
'route' => 'admin.production.orders.create',
|
||||
'can' => 'production.orders.create',
|
||||
],
|
||||
'Control de Procesos' => [
|
||||
'route' => 'admin.production.process.index',
|
||||
'can' => 'production.process.view',
|
||||
],
|
||||
'Historial de Producción' => [
|
||||
'route' => 'admin.production.history.index',
|
||||
'can' => 'production.history.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Control de Calidad' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-award',
|
||||
'submenu' => [
|
||||
'Inspecciones de Calidad' => [
|
||||
'route' => 'admin.quality.inspections.index',
|
||||
'can' => 'quality.inspections.view',
|
||||
],
|
||||
'Crear Inspección' => [
|
||||
'route' => 'admin.quality.inspections.create',
|
||||
'can' => 'quality.inspections.create',
|
||||
],
|
||||
'Reportes de Calidad' => [
|
||||
'route' => 'admin.quality.reports.index',
|
||||
'can' => 'quality.reports.view',
|
||||
],
|
||||
'Historial de Inspecciones' => [
|
||||
'route' => 'admin.quality.history.index',
|
||||
'can' => 'quality.history.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Flujos de Trabajo y Automatización' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-dots-3',
|
||||
'submenu' => [
|
||||
'Gestión de Flujos de Trabajo' => [
|
||||
'route' => 'admin.workflows.index',
|
||||
'can' => 'workflows.view',
|
||||
],
|
||||
'Crear Flujo de Trabajo' => [
|
||||
'route' => 'admin.workflows.create',
|
||||
'can' => 'workflows.create',
|
||||
],
|
||||
'Automatizaciones' => [
|
||||
'route' => 'admin.workflows.automations.index',
|
||||
'can' => 'workflows.automations.view',
|
||||
],
|
||||
'Historial de Flujos' => [
|
||||
'route' => 'admin.workflows.history.index',
|
||||
'can' => 'workflows.history.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'Contratos' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-writing-sign',
|
||||
'submenu' => [
|
||||
'Mis Contratos' => [
|
||||
'route' => 'admin.contracts.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-file-description',
|
||||
'can' => 'contracts.view',
|
||||
],
|
||||
'Firmar Contrato' => [
|
||||
'route' => 'admin.contracts.sign',
|
||||
'icon' => 'menu-icon tf-icons ti ti-signature',
|
||||
'can' => 'contracts.sign',
|
||||
],
|
||||
'Contratos Automatizados' => [
|
||||
'route' => 'admin.contracts.automated',
|
||||
'icon' => 'menu-icon tf-icons ti ti-robot',
|
||||
'can' => 'contracts.automated.view',
|
||||
],
|
||||
'Historial de Contratos' => [
|
||||
'route' => 'admin.contracts.history',
|
||||
'icon' => 'menu-icon tf-icons ti ti-archive',
|
||||
'can' => 'contracts.history.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
'Atención al Cliente' => [
|
||||
'icon' => 'menu-icon tf-icons ti ti-messages',
|
||||
'submenu' => [
|
||||
'Tablero' => [
|
||||
'route' => 'admin.sales.dashboard.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-infographic',
|
||||
'can' => 'ticketing.dashboard.view',
|
||||
],
|
||||
'Mis Tickets' => [
|
||||
'route' => 'admin.ticketing.tickets.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-ticket',
|
||||
'can' => 'ticketing.tickets.view',
|
||||
],
|
||||
'Crear Ticket' => [
|
||||
'route' => 'admin.ticketing.tickets.create',
|
||||
'icon' => 'menu-icon tf-icons ti ti-square-plus',
|
||||
'can' => 'ticketing.tickets.create',
|
||||
],
|
||||
'Categorías de Tickets' => [
|
||||
'route' => 'admin.ticketing.categories.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-category',
|
||||
'can' => 'ticketing.categories.view',
|
||||
],
|
||||
'Estadísticas de Atención' => [
|
||||
'route' => 'admin.ticketing.analytics.index',
|
||||
'icon' => 'menu-icon tf-icons ti ti-chart-bar',
|
||||
'can' => 'ticketing.analytics.view',
|
||||
],
|
||||
]
|
||||
],
|
||||
];
|
510
database/data/rbac-config.json
Normal file
510
database/data/rbac-config.json
Normal file
@ -0,0 +1,510 @@
|
||||
{
|
||||
"roles": {
|
||||
"SuperAdmin" : {
|
||||
"style": "dark",
|
||||
"permissions" : [
|
||||
"admin.core.general-settings.allow",
|
||||
"admin.core.cache-manager.view",
|
||||
"admin.core.smtp-settings.allow",
|
||||
"admin.store-manager.company.view",
|
||||
"admin.store-manager.stores.view",
|
||||
"admin.store-manager.stores.view",
|
||||
"admin.finance.banxico.allow",
|
||||
"admin.finance.banking.allow",
|
||||
"admin.sales.ticket-config.allow",
|
||||
"admin.billing.csds-settings.allow",
|
||||
"admin.billing.stamping-package.allow",
|
||||
"admin.billing.smtp-settings.allow",
|
||||
"admin.billing.mass-cfdi-download.allow",
|
||||
"admin.core.users.view",
|
||||
"admin.core.roles.view",
|
||||
"admin.core.permissions.view",
|
||||
"admin.core.import-sat-catalogs.allow",
|
||||
"admin.ai.dashboard.view",
|
||||
"admin.ai.content.create",
|
||||
"admin.ai.analytics.view",
|
||||
"admin.chatbot.config.view",
|
||||
"admin.chatbot.flows.manage",
|
||||
"admin.chatbot.history.view",
|
||||
"admin.iot.devices.view",
|
||||
"admin.iot.sensors.manage",
|
||||
"admin.iot.monitoring.view",
|
||||
"admin.facial-recognition.profiles.manage",
|
||||
"admin.facial-recognition.live.verify",
|
||||
"admin.facial-recognition.history.view",
|
||||
"admin.print.queue.view",
|
||||
"admin.print.history.view",
|
||||
"admin.print.settings.manage",
|
||||
"admin.website.general-settings.allow",
|
||||
"admin.website.legal.view",
|
||||
"admin.website.faq.view",
|
||||
"admin.blog.categories.view",
|
||||
"admin.blog.tags.view",
|
||||
"admin.blog.articles.view",
|
||||
"admin.blog.comments.view",
|
||||
"admin.contacts.contacts.view",
|
||||
"admin.contacts.employees.view",
|
||||
"admin.contacts.employees.create",
|
||||
"admin.rrhh.jobs.view",
|
||||
"admin.rrhh.organization.view",
|
||||
"admin.recruitment.jobs.view",
|
||||
"admin.recruitment.candidates.view",
|
||||
"admin.recruitment.interviews.view",
|
||||
"admin.payroll.contracts.view",
|
||||
"admin.payroll.process.view",
|
||||
"admin.payroll.receipts.view",
|
||||
"admin.payroll.reports.view",
|
||||
"admin.attendance.records.view",
|
||||
"admin.attendance.biometric.view",
|
||||
"admin.attendance.absences.view",
|
||||
"admin.inventory.product-categories.view",
|
||||
"admin.inventory.product-catalogs.view",
|
||||
"admin.inventory.products.view",
|
||||
"admin.inventory.products.create",
|
||||
"admin.sales.dashboard.allow",
|
||||
"admin.contacts.customers.view",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.quotes.view",
|
||||
"admin.sales.sales.create",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.remissions.create",
|
||||
"admin.sales.remissions.view",
|
||||
"admin.sales.remissions.view",
|
||||
"admin.sales.credit-notes.create",
|
||||
"admin.sales.credit-notes.view",
|
||||
"admin.sales.credit-notes.view",
|
||||
"admin.accounting.dashboard.view",
|
||||
"admin.accounting.charts.view",
|
||||
"admin.finance.accounts-payable.view",
|
||||
"admin.finance.accounts-receivable.view",
|
||||
"admin.accounting.balance.view",
|
||||
"admin.accounting.income-statement.view",
|
||||
"admin.accounting.ledger.view",
|
||||
"admin.accounting.entries.view",
|
||||
"admin.expenses.dashboard.view",
|
||||
"admin.expenses.expenses.create",
|
||||
"admin.expenses.expenses.view",
|
||||
"admin.expenses.categories.view",
|
||||
"admin.expenses.history.view",
|
||||
"admin.billing.dashboard.allow",
|
||||
"admin.billing.ingresos.create",
|
||||
"admin.billing.ingresos.view",
|
||||
"admin.billing.ingresos.view",
|
||||
"admin.billing.egresos.create",
|
||||
"admin.billing.egresos.view",
|
||||
"admin.billing.egresos.view",
|
||||
"admin.billing.pagos.created",
|
||||
"admin.billing.pagos.view",
|
||||
"admin.billing.nomina.view",
|
||||
"admin.billing.verify-cfdi.allow",
|
||||
"admin.contacts.suppliers.view",
|
||||
"admin.inventory.orders.view",
|
||||
"admin.inventory.reception.view",
|
||||
"admin.inventory.materials.view",
|
||||
"admin.inventory.warehouse.view",
|
||||
"admin.inventory.stock.view",
|
||||
"admin.inventory.movements.view",
|
||||
"admin.inventory.transfers.view",
|
||||
"admin.inventory.shipping-orders.view",
|
||||
"admin.inventory.shipping-tracking.view",
|
||||
"admin.inventory.shipping-carriers.view",
|
||||
"admin.inventory.shipping-rates.view",
|
||||
"admin.inventory.assets.view",
|
||||
"admin.inventory.asset-maintenance.view",
|
||||
"admin.inventory.asset-lifecycle.view",
|
||||
"admin.inventory.asset-assignments.view",
|
||||
"admin.projects.dashboard.view",
|
||||
"admin.projects.view",
|
||||
"admin.projects.create",
|
||||
"admin.projects.tasks.view",
|
||||
"admin.projects.history.view",
|
||||
"admin.production.orders.view",
|
||||
"admin.production.orders.create",
|
||||
"admin.production.process.view",
|
||||
"admin.production.history.view",
|
||||
"admin.quality.inspections.view",
|
||||
"admin.quality.inspections.create",
|
||||
"admin.quality.reports.view",
|
||||
"admin.quality.history.view",
|
||||
"admin.workflows.view",
|
||||
"admin.workflows.create",
|
||||
"admin.workflows.automations.view",
|
||||
"admin.workflows.history.view",
|
||||
"admin.contracts.view",
|
||||
"admin.contracts.sign",
|
||||
"admin.contracts.automated.view",
|
||||
"admin.contracts.history.view",
|
||||
"admin.ticketing.dashboard.view",
|
||||
"admin.ticketing.tickets.view",
|
||||
"admin.ticketing.tickets.create",
|
||||
"admin.ticketing.categories.view",
|
||||
"admin.ticketing.analytics.view"
|
||||
]
|
||||
},
|
||||
"Admin" : {
|
||||
"style": "primary",
|
||||
"permissions" : [
|
||||
"admin.core.general-settings.allow",
|
||||
"admin.core.cache-manager.view",
|
||||
"admin.core.smtp-settings.allow",
|
||||
"admin.website.general-settings.allow",
|
||||
"admin.website.legal.view",
|
||||
"admin.store-manager.company.view",
|
||||
"admin.store-manager.stores.view",
|
||||
"admin.store-manager.stores.view",
|
||||
"admin.core.users.view",
|
||||
"admin.core.roles.view",
|
||||
"admin.core.permissions.view",
|
||||
"admin.core.import-sat-catalogs.allow",
|
||||
"admin.contacts.contacts.view",
|
||||
"admin.contacts.contacts.create",
|
||||
"admin.contacts.employees.view",
|
||||
"admin.contacts.employees.create",
|
||||
"admin.contacts.customers.view",
|
||||
"admin.contacts.customers.create",
|
||||
"admin.rrhh.jobs.view",
|
||||
"admin.rrhh.organization.view",
|
||||
"admin.inventory.product-categories.view",
|
||||
"admin.inventory.product-catalogs.view",
|
||||
"admin.inventory.products.view",
|
||||
"admin.inventory.products.create",
|
||||
"admin.contacts.suppliers.view",
|
||||
"admin.contacts.suppliers.create",
|
||||
"admin.inventory.warehouse.view",
|
||||
"admin.inventory.orders.view",
|
||||
"admin.inventory.reception.view",
|
||||
"admin.inventory.materials.view",
|
||||
"admin.inventory.stock.view",
|
||||
"admin.inventory.movements.view",
|
||||
"admin.inventory.transfers.view",
|
||||
"admin.inventory.assets.view",
|
||||
"admin.inventory.asset-maintenance.view",
|
||||
"admin.inventory.asset-lifecycle.view",
|
||||
"admin.inventory.asset-assignments.view"
|
||||
]
|
||||
},
|
||||
"Administrador Web" : {
|
||||
"style": "primary",
|
||||
"permissions" : []
|
||||
},
|
||||
"Editor" : {
|
||||
"style": "primary",
|
||||
"permissions" : []
|
||||
},
|
||||
"Almacenista" : {
|
||||
"style": "success",
|
||||
"permissions" : [
|
||||
"admin.inventory.product-categories.view",
|
||||
"admin.inventory.product-catalogs.view",
|
||||
"admin.inventory.products.view",
|
||||
"admin.inventory.products.create",
|
||||
"admin.inventory.warehouse.view",
|
||||
"admin.inventory.stock.view",
|
||||
"admin.inventory.movements.view",
|
||||
"admin.inventory.transfers.view"
|
||||
]
|
||||
},
|
||||
"Productos y servicios" : {
|
||||
"style": "info",
|
||||
"permissions" : []
|
||||
},
|
||||
"Recursos humanos" : {
|
||||
"style": "success",
|
||||
"permissions" : []
|
||||
},
|
||||
"Nómina" : {
|
||||
"style": "success",
|
||||
"permissions" : []
|
||||
},
|
||||
"Activos fijos" : {
|
||||
"style": "secondary",
|
||||
"permissions" : []
|
||||
},
|
||||
"Compras y gastos" : {
|
||||
"style": "info",
|
||||
"permissions" : []
|
||||
},
|
||||
"CRM" : {
|
||||
"style": "warning",
|
||||
"permissions" : []
|
||||
},
|
||||
"Vendedor" : {
|
||||
"style": "info",
|
||||
"permissions" : []
|
||||
},
|
||||
"Gerente" : {
|
||||
"style": "danger",
|
||||
"permissions" : []
|
||||
},
|
||||
"Facturación" : {
|
||||
"style": "info",
|
||||
"permissions" : []
|
||||
},
|
||||
"Facturación avanzado" : {
|
||||
"style": "danger",
|
||||
"permissions" : []
|
||||
},
|
||||
"Finanzas" : {
|
||||
"style": "info",
|
||||
"permissions" : []
|
||||
},
|
||||
"Auditor" : {
|
||||
"style": "dark",
|
||||
"permissions" : [
|
||||
"admin.core.cache-manager.view",
|
||||
"admin.store-manager.company.view",
|
||||
"admin.store-manager.stores.view",
|
||||
"admin.store-manager.stores.view",
|
||||
"admin.core.users.view",
|
||||
"admin.core.roles.view",
|
||||
"admin.core.permissions.view",
|
||||
"admin.ai.dashboard.view",
|
||||
"admin.ai.analytics.view",
|
||||
"admin.chatbot.config.view",
|
||||
"admin.chatbot.history.view",
|
||||
"admin.iot.devices.view",
|
||||
"admin.iot.monitoring.view",
|
||||
"admin.facial-recognition.history.view",
|
||||
"admin.print.queue.view",
|
||||
"admin.print.history.view",
|
||||
"admin.website.legal.view",
|
||||
"admin.website.faq.view",
|
||||
"admin.blog.categories.view",
|
||||
"admin.blog.tags.view",
|
||||
"admin.blog.articles.view",
|
||||
"admin.blog.comments.view",
|
||||
"admin.contacts.contacts.view",
|
||||
"admin.crm.marketing-campaigns.view",
|
||||
"admin.crm.leads.view",
|
||||
"admin.crm.newsletter.view",
|
||||
"admin.contacts.employees.view",
|
||||
"admin.rrhh.jobs.view",
|
||||
"admin.rrhh.organization.view",
|
||||
"admin.recruitment.jobs.view",
|
||||
"admin.recruitment.candidates.view",
|
||||
"admin.recruitment.interviews.view",
|
||||
"admin.payroll.contracts.view",
|
||||
"admin.payroll.process.view",
|
||||
"admin.payroll.receipts.view",
|
||||
"admin.payroll.reports.view",
|
||||
"admin.attendance.records.view",
|
||||
"admin.attendance.biometric.view",
|
||||
"admin.attendance.absences.view",
|
||||
"admin.inventory.product-categories.view",
|
||||
"admin.inventory.product-catalogs.view",
|
||||
"admin.inventory.products.view",
|
||||
"admin.contacts.customers.view",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.quotes.view",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.remissions.view",
|
||||
"admin.sales.remissions.view",
|
||||
"admin.sales.credit-notes.view",
|
||||
"admin.sales.credit-notes.view",
|
||||
"admin.accounting.dashboard.view",
|
||||
"admin.accounting.charts.view",
|
||||
"admin.finance.accounts-payable.view",
|
||||
"admin.finance.accounts-receivable.view",
|
||||
"admin.accounting.balance.view",
|
||||
"admin.accounting.income-statement.view",
|
||||
"admin.accounting.ledger.view",
|
||||
"admin.accounting.entries.view",
|
||||
"admin.expenses.dashboard.view",
|
||||
"admin.expenses.expenses.view",
|
||||
"admin.expenses.categories.view",
|
||||
"admin.expenses.history.view",
|
||||
"admin.billing.ingresos.view",
|
||||
"admin.billing.ingresos.view",
|
||||
"admin.billing.egresos.view",
|
||||
"admin.billing.egresos.view",
|
||||
"admin.billing.pagos.view",
|
||||
"admin.billing.nomina.view",
|
||||
"admin.contacts.suppliers.view",
|
||||
"admin.inventory.orders.view",
|
||||
"admin.inventory.reception.view",
|
||||
"admin.inventory.materials.view",
|
||||
"admin.inventory.warehouse.view",
|
||||
"admin.inventory.stock.view",
|
||||
"admin.inventory.movements.view",
|
||||
"admin.inventory.transfers.view",
|
||||
"admin.inventory.shipping-orders.view",
|
||||
"admin.inventory.shipping-tracking.view",
|
||||
"admin.inventory.shipping-carriers.view",
|
||||
"admin.inventory.shipping-rates.view",
|
||||
"admin.inventory.assets.view",
|
||||
"admin.inventory.asset-maintenance.view",
|
||||
"admin.inventory.asset-lifecycle.view",
|
||||
"admin.inventory.asset-assignments.view",
|
||||
"admin.projects.dashboard.view",
|
||||
"admin.projects.view",
|
||||
"admin.projects.tasks.view",
|
||||
"admin.projects.history.view",
|
||||
"admin.production.orders.view",
|
||||
"admin.production.process.view",
|
||||
"admin.production.history.view",
|
||||
"admin.quality.inspections.view",
|
||||
"admin.quality.reports.view",
|
||||
"admin.quality.history.view",
|
||||
"admin.workflows.view",
|
||||
"admin.workflows.automations.view",
|
||||
"admin.workflows.history.view",
|
||||
"admin.contracts.view",
|
||||
"admin.contracts.automated.view",
|
||||
"admin.contracts.history.view",
|
||||
"admin.ticketing.dashboard.view",
|
||||
"admin.ticketing.tickets.view",
|
||||
"admin.ticketing.categories.view",
|
||||
"admin.ticketing.analytics.view"
|
||||
]
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"admin.core.general-settings.allow",
|
||||
"admin.core.cache-manager.view",
|
||||
"admin.core.smtp-settings.allow",
|
||||
"admin.store-manager.company.view",
|
||||
"admin.store-manager.stores.view",
|
||||
"admin.store-manager.stores.view",
|
||||
"admin.finance.banxico.allow",
|
||||
"admin.finance.banking.allow",
|
||||
"admin.sales.ticket-config.allow",
|
||||
"admin.billing.csds-settings.allow",
|
||||
"admin.billing.stamping-package.allow",
|
||||
"admin.billing.smtp-settings.allow",
|
||||
"admin.billing.mass-cfdi-download.allow",
|
||||
"admin.core.users.view",
|
||||
"admin.core.roles.view",
|
||||
"admin.core.permissions.view",
|
||||
"admin.core.import-sat-catalogs.allow",
|
||||
"admin.ai.dashboard.view",
|
||||
"admin.ai.content.create",
|
||||
"admin.ai.analytics.view",
|
||||
"admin.chatbot.config.view",
|
||||
"admin.chatbot.flows.manage",
|
||||
"admin.chatbot.history.view",
|
||||
"admin.iot.devices.view",
|
||||
"admin.iot.sensors.manage",
|
||||
"admin.iot.monitoring.view",
|
||||
"admin.facial-recognition.profiles.manage",
|
||||
"admin.facial-recognition.live.verify",
|
||||
"admin.facial-recognition.history.view",
|
||||
"admin.print.queue.view",
|
||||
"admin.print.history.view",
|
||||
"admin.print.settings.manage",
|
||||
"admin.website.general-settings.allow",
|
||||
"admin.website.legal.view",
|
||||
"admin.website.faq.view",
|
||||
"admin.blog.categories.view",
|
||||
"admin.blog.tags.view",
|
||||
"admin.blog.articles.view",
|
||||
"admin.blog.comments.view",
|
||||
"admin.contacts.contacts.view",
|
||||
"admin.contacts.contacts.create",
|
||||
"admin.crm.marketing-campaigns.view",
|
||||
"admin.crm.leads.view",
|
||||
"admin.crm.newsletter.view",
|
||||
"admin.contacts.employees.view",
|
||||
"admin.contacts.employees.create",
|
||||
"admin.rrhh.jobs.view",
|
||||
"admin.rrhh.organization.view",
|
||||
"admin.recruitment.jobs.view",
|
||||
"admin.recruitment.candidates.view",
|
||||
"admin.recruitment.interviews.view",
|
||||
"admin.payroll.contracts.view",
|
||||
"admin.payroll.process.view",
|
||||
"admin.payroll.receipts.view",
|
||||
"admin.payroll.reports.view",
|
||||
"admin.attendance.records.view",
|
||||
"admin.attendance.biometric.view",
|
||||
"admin.attendance.absences.view",
|
||||
"admin.inventory.product-categories.view",
|
||||
"admin.inventory.product-catalogs.view",
|
||||
"admin.inventory.products.view",
|
||||
"admin.inventory.products.create",
|
||||
"admin.sales.dashboard.allow",
|
||||
"admin.contacts.customers.view",
|
||||
"admin.contacts.customers.create",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.quotes.view",
|
||||
"admin.sales.sales.create",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.sales.view",
|
||||
"admin.sales.remissions.create",
|
||||
"admin.sales.remissions.view",
|
||||
"admin.sales.remissions.view",
|
||||
"admin.sales.credit-notes.create",
|
||||
"admin.sales.credit-notes.view",
|
||||
"admin.sales.credit-notes.view",
|
||||
"admin.accounting.dashboard.view",
|
||||
"admin.accounting.charts.view",
|
||||
"admin.finance.accounts-payable.view",
|
||||
"admin.finance.accounts-receivable.view",
|
||||
"admin.accounting.balance.view",
|
||||
"admin.accounting.income-statement.view",
|
||||
"admin.accounting.ledger.view",
|
||||
"admin.accounting.entries.view",
|
||||
"admin.expenses.dashboard.view",
|
||||
"admin.expenses.expenses.create",
|
||||
"admin.expenses.expenses.view",
|
||||
"admin.expenses.categories.view",
|
||||
"admin.expenses.history.view",
|
||||
"admin.billing.dashboard.allow",
|
||||
"admin.billing.ingresos.create",
|
||||
"admin.billing.ingresos.view",
|
||||
"admin.billing.ingresos.view",
|
||||
"admin.billing.egresos.create",
|
||||
"admin.billing.egresos.view",
|
||||
"admin.billing.egresos.view",
|
||||
"admin.billing.pagos.created",
|
||||
"admin.billing.pagos.view",
|
||||
"admin.billing.nomina.view",
|
||||
"admin.billing.verify-cfdi.allow",
|
||||
"admin.contacts.suppliers.view",
|
||||
"admin.contacts.suppliers.create",
|
||||
"admin.inventory.orders.view",
|
||||
"admin.inventory.reception.view",
|
||||
"admin.inventory.materials.view",
|
||||
"admin.inventory.warehouse.view",
|
||||
"admin.inventory.stock.view",
|
||||
"admin.inventory.movements.view",
|
||||
"admin.inventory.transfers.view",
|
||||
"admin.inventory.shipping-orders.view",
|
||||
"admin.inventory.shipping-tracking.view",
|
||||
"admin.inventory.shipping-carriers.view",
|
||||
"admin.inventory.shipping-rates.view",
|
||||
"admin.inventory.assets.view",
|
||||
"admin.inventory.asset-maintenance.view",
|
||||
"admin.inventory.asset-lifecycle.view",
|
||||
"admin.inventory.asset-assignments.view",
|
||||
"admin.projects.dashboard.view",
|
||||
"admin.projects.view",
|
||||
"admin.projects.create",
|
||||
"admin.projects.tasks.view",
|
||||
"admin.projects.history.view",
|
||||
"admin.production.orders.view",
|
||||
"admin.production.orders.create",
|
||||
"admin.production.process.view",
|
||||
"admin.production.history.view",
|
||||
"admin.quality.inspections.view",
|
||||
"admin.quality.inspections.create",
|
||||
"admin.quality.reports.view",
|
||||
"admin.quality.history.view",
|
||||
"admin.workflows.view",
|
||||
"admin.workflows.create",
|
||||
"admin.workflows.automations.view",
|
||||
"admin.workflows.history.view",
|
||||
"admin.contracts.view",
|
||||
"admin.contracts.sign",
|
||||
"admin.contracts.automated.view",
|
||||
"admin.contracts.history.view",
|
||||
"admin.ticketing.dashboard.view",
|
||||
"admin.ticketing.tickets.view",
|
||||
"admin.ticketing.tickets.create",
|
||||
"admin.ticketing.categories.view",
|
||||
"admin.ticketing.analytics.view"
|
||||
]
|
||||
}
|
||||
|
||||
|
14
database/data/users.csv
Normal file
14
database/data/users.csv
Normal file
@ -0,0 +1,14 @@
|
||||
name,email,role,password
|
||||
Administrador Web,webadmin@concierge.test,Administrador Web,LAdmin123
|
||||
Productos y servicios,productos@concierge.test,Productos y servicios,LAdmin123
|
||||
Recursos humanos,rrhh@concierge.test,Recursos humanos,LAdmin123
|
||||
Nómina,nomina@concierge.test,Nómina,LAdmin123
|
||||
Activos fijos,activos@concierge.test,Activos fijos,LAdmin123
|
||||
Compras y gastos,compras@concierge.test,Compras y gastos,LAdmin123
|
||||
CRM,crm@concierge.test,CRM,LAdmin123
|
||||
Vendedor,vendedor@concierge.test,Vendedor,LAdmin123
|
||||
Gerente,gerente@concierge.test,Gerente,LAdmin123
|
||||
Facturación,facturacion@concierge.test,Facturación,LAdmin123
|
||||
Facturación avanzado,facturacion_avanzado@concierge.test,Facturación avanzado,LAdmin123
|
||||
Finanzas,finanzas@concierge.test,Finanzas,LAdmin123
|
||||
Auditor,auditor@concierge.test,Auditor,LAdmin123
|
|
49
database/factories/UserFactory.php
Normal file
49
database/factories/UserFactory.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Database\factories;
|
||||
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Koneko\VuexyAdmin\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'remember_token' => Str::random(10),
|
||||
'profile_photo_path' => null,
|
||||
'status' => fake()->randomElement([User::STATUS_ENABLED, User::STATUS_DISABLED])
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn(array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
44
database/migrations/2024_12_14_030215_modify_users_table.php
Normal file
44
database/migrations/2024_12_14_030215_modify_users_table.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement('ALTER TABLE `users` MODIFY `id` BIGINT UNSIGNED NOT NULL;');
|
||||
DB::statement('ALTER TABLE `users` DROP PRIMARY KEY;');
|
||||
DB::statement('ALTER TABLE `users` MODIFY `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (`id`);');
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('last_name', 100)->nullable()->comment('Apellidos')->index()->after('name');
|
||||
$table->string('profile_photo_path', 2048)->nullable()->after('remember_token');
|
||||
$table->unsignedTinyInteger('status')->default(User::STATUS_DISABLED)->after('profile_photo_path');
|
||||
$table->unsignedMediumInteger('created_by')->nullable()->index()->after('status');
|
||||
|
||||
// Definir la relación con created_by
|
||||
$table->foreign('created_by')->references('id')->on('users')->onUpdate('restrict')->onDelete('restrict');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement('ALTER TABLE `users` MODIFY `id` MEDIUMINT UNSIGNED NOT NULL;');
|
||||
DB::statement('ALTER TABLE `users` DROP PRIMARY KEY;');
|
||||
DB::statement('ALTER TABLE `users` MODIFY `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (`id`);');
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['last_name', 'profile_photo_path', 'status', 'created_by']);
|
||||
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_logins', function (Blueprint $table) {
|
||||
$table->integerIncrements('id');
|
||||
|
||||
$table->unsignedMediumInteger('user_id')->nullable()->index();
|
||||
$table->ipAddress('ip_address')->nullable();
|
||||
$table->string('user_agent')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Relaciones
|
||||
$table->foreign('user_id')->references('id')->on('users');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Elimina tablas solo si existen
|
||||
Schema::dropIfExists('user_logins');
|
||||
}
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->string('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
}
|
||||
};
|
@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$teams = config('permission.teams');
|
||||
$tableNames = config('permission.table_names');
|
||||
$columnNames = config('permission.column_names');
|
||||
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||
|
||||
if (empty($tableNames)) {
|
||||
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
}
|
||||
|
||||
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
|
||||
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
}
|
||||
|
||||
Schema::create($tableNames['permissions'], function (Blueprint $table) {
|
||||
//$table->engine('InnoDB');
|
||||
$table->bigIncrements('id'); // permission id
|
||||
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||
$table->string('group_name')->nullable()->index();
|
||||
$table->string('sub_group_name')->nullable()->index();
|
||||
$table->string('action')->nullable()->index();
|
||||
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
$table->unique(['group_name', 'sub_group_name', 'action', 'guard_name']);
|
||||
});
|
||||
|
||||
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
|
||||
//$table->engine('InnoDB');
|
||||
$table->bigIncrements('id'); // role id
|
||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||
}
|
||||
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||
$table->string('style')->nullable();
|
||||
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||
$table->timestamps();
|
||||
if ($teams || config('permission.testing')) {
|
||||
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||
} else {
|
||||
$table->unique(['name', 'guard_name']);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->onDelete('cascade');
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||
|
||||
$table->primary(
|
||||
[$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary'
|
||||
);
|
||||
} else {
|
||||
$table->primary(
|
||||
[$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_permissions_permission_model_type_primary'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->onDelete('cascade');
|
||||
if ($teams) {
|
||||
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||
|
||||
$table->primary(
|
||||
[$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary'
|
||||
);
|
||||
} else {
|
||||
$table->primary(
|
||||
[$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||
'model_has_roles_role_model_type_primary'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
$table->foreign($pivotPermission)
|
||||
->references('id') // permission id
|
||||
->on($tableNames['permissions'])
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign($pivotRole)
|
||||
->references('id') // role id
|
||||
->on($tableNames['roles'])
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||
});
|
||||
|
||||
app('cache')
|
||||
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||
->forget(config('permission.cache.key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$tableNames = config('permission.table_names');
|
||||
|
||||
if (empty($tableNames)) {
|
||||
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
|
||||
}
|
||||
|
||||
Schema::drop($tableNames['role_has_permissions']);
|
||||
Schema::drop($tableNames['model_has_roles']);
|
||||
Schema::drop($tableNames['model_has_permissions']);
|
||||
Schema::drop($tableNames['roles']);
|
||||
Schema::drop($tableNames['permissions']);
|
||||
}
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->text('two_factor_secret')
|
||||
->after('password')
|
||||
->nullable();
|
||||
|
||||
$table->text('two_factor_recovery_codes')
|
||||
->after('two_factor_secret')
|
||||
->nullable();
|
||||
|
||||
if (Fortify::confirmsTwoFactorAuthentication()) {
|
||||
$table->timestamp('two_factor_confirmed_at')
|
||||
->after('two_factor_recovery_codes')
|
||||
->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(array_merge([
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
], Fortify::confirmsTwoFactorAuthentication() ? [
|
||||
'two_factor_confirmed_at',
|
||||
] : []));
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('settings', function (Blueprint $table) {
|
||||
$table->mediumIncrements('id');
|
||||
|
||||
$table->string('key')->index();
|
||||
$table->text('value');
|
||||
$table->unsignedMediumInteger('user_id')->nullable()->index();
|
||||
|
||||
// Unique constraints
|
||||
$table->unique(['user_id', 'key']);
|
||||
|
||||
// Relaciones
|
||||
$table->foreign('user_id')->references('id')->on('users');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('settings');
|
||||
}
|
||||
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('media_items', function (Blueprint $table) {
|
||||
$table->mediumIncrements('id');
|
||||
|
||||
// Relación polimórfica
|
||||
$table->unsignedMediumInteger('mediaable_id');
|
||||
$table->string('mediaable_type');
|
||||
|
||||
$table->unsignedTinyInteger('type')->index(); // Tipo de medio: 'image', 'video', 'file', 'youtube'
|
||||
$table->unsignedTinyInteger('sub_type')->index(); // Subtipo de medio: 'thumbnail', 'main', 'additional'
|
||||
|
||||
$table->string('url', 255)->nullable(); // URL del medio
|
||||
$table->string('path')->nullable(); // Ruta del archivo si está almacenado localmente
|
||||
|
||||
$table->string('title')->nullable()->index(); // Título del medio
|
||||
$table->mediumText('description')->nullable(); // Descripción del medio
|
||||
$table->unsignedTinyInteger('order')->nullable(); // Orden de presentación
|
||||
|
||||
// Authoría
|
||||
$table->timestamps();
|
||||
|
||||
// Índices
|
||||
$table->index(['mediaable_type', 'mediaable_id']);
|
||||
$table->index(['mediaable_type', 'mediaable_id', 'type']);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('images');
|
||||
}
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$connection = config('audit.drivers.database.connection', config('database.default'));
|
||||
$table = config('audit.drivers.database.table', 'audits');
|
||||
|
||||
Schema::connection($connection)->create($table, function (Blueprint $table) {
|
||||
|
||||
$morphPrefix = config('audit.user.morph_prefix', 'user');
|
||||
|
||||
$table->bigIncrements('id');
|
||||
$table->string($morphPrefix . '_type')->nullable();
|
||||
$table->unsignedBigInteger($morphPrefix . '_id')->nullable();
|
||||
$table->string('event');
|
||||
$table->morphs('auditable');
|
||||
$table->text('old_values')->nullable();
|
||||
$table->text('new_values')->nullable();
|
||||
$table->text('url')->nullable();
|
||||
$table->ipAddress('ip_address')->nullable();
|
||||
$table->string('user_agent', 1023)->nullable();
|
||||
$table->string('tags')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index([$morphPrefix . '_id', $morphPrefix . '_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$connection = config('audit.drivers.database.connection', config('database.default'));
|
||||
$table = config('audit.drivers.database.table', 'audits');
|
||||
|
||||
Schema::connection($connection)->drop($table);
|
||||
}
|
||||
};
|
14
database/seeders/PermissionSeeder.php
Normal file
14
database/seeders/PermissionSeeder.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Koneko\VuexyAdmin\Services\RBACService;
|
||||
|
||||
class PermissionSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
RBACService::loadRolesAndPermissions();
|
||||
}
|
||||
}
|
108
database/seeders/SettingSeeder.php
Normal file
108
database/seeders/SettingSeeder.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
class SettingSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$settings_array = [
|
||||
/*
|
||||
'app_title' => 'Quimiplastic S.A de C.V.',
|
||||
'app_faviconIcon' => '../assets/img/logo/koneko-02.png',
|
||||
'app_name' => 'Quimiplastic',
|
||||
'app_imageLogo' => '../assets/img/logo/koneko-02.png',
|
||||
|
||||
'app_myLayout' => 'vertical',
|
||||
'app_myTheme' => 'theme-default',
|
||||
'app_myStyle' => 'light',
|
||||
'app_navbarType' => 'sticky',
|
||||
'app_menuFixed' => true,
|
||||
'app_menuCollapsed' => false,
|
||||
'app_headerType' => 'static',
|
||||
'app_showDropdownOnHover' => false,
|
||||
'app_authViewMode' => 'cover',
|
||||
'app_maxQuickLinks' => 5,
|
||||
|
||||
|
||||
|
||||
'smtp.host' => 'webmail.koneko.mx',
|
||||
'smtp.port' => 465,
|
||||
'smtp.encryption' => 'tls',
|
||||
'smtp.username' => 'no-responder@koneko.mx',
|
||||
'smtp.password' => null,
|
||||
'smtp.from_email' => 'no-responder@koneko.mx',
|
||||
'smtp.from_name' => 'Koneko Soluciones en Tecnología',
|
||||
'smtp.reply_to_method' => 'smtp',
|
||||
'smtp.reply_to_email' => null,
|
||||
'smtp.reply_to_name' => null,
|
||||
|
||||
|
||||
|
||||
'website.title',
|
||||
'website.favicon',
|
||||
'website.description',
|
||||
'website.image_logo',
|
||||
'website.image_logoDark',
|
||||
|
||||
'admin.title',
|
||||
'admin.favicon',
|
||||
'admin.description',
|
||||
'admin.image_logo',
|
||||
'admin.image_logoDark',
|
||||
|
||||
|
||||
'favicon.icon' => null,
|
||||
|
||||
'contact.phone_number' => '(222) 462 0903',
|
||||
'contact.phone_number_ext' => 'Ext. 5',
|
||||
'contact.email' => 'virtualcompras@live.com.mx',
|
||||
'contact.form.email' => 'contacto@conciergetravellife.com',
|
||||
'contact.form.email_cc' => 'arturo@koneko.mx',
|
||||
'contact.form.subject' => 'Has recibido un mensaje del formulario de covirsast.com',
|
||||
'contact.direccion' => '51 PTE 505 loc. 14, Puebla, Pue.',
|
||||
'contact.horario' => '9am - 7 pm',
|
||||
'contact.location.lat' => '19.024439',
|
||||
'contact.location.lng' => '-98.215777',
|
||||
|
||||
'social.whatsapp' => '',
|
||||
'social.whatsapp.message' => '👋 Hola! Estoy buscando más información sobre Covirsa Soluciones en Tecnología. ¿Podrías proporcionarme los detalles que necesito? ¡Te lo agradecería mucho! 💻✨',
|
||||
|
||||
'social.facebook' => 'https://www.facebook.com/covirsast/?locale=es_LA',
|
||||
'social.Whatsapp' => '2228 200 201',
|
||||
'social.Whatsapp.message' => '¡Hola! 🌟 Estoy interesado en obtener más información acerca de Concierge Travel. ¿Podrías ayudarme con los detalles? ¡Gracias de antemano! ✈️🏝',
|
||||
'social.Facebook' => 'test',
|
||||
'social.Instagram' => 'test',
|
||||
'social.Linkedin' => 'test',
|
||||
'social.Tiktok' => 'test',
|
||||
'social.X_twitter' => 'test',
|
||||
'social.Google' => 'test',
|
||||
'social.Pinterest' => 'test',
|
||||
'social.Youtube' => 'test',
|
||||
'social.Vimeo' => 'test',
|
||||
|
||||
|
||||
'chat.provider' => '',
|
||||
'chat.whatsapp.number' => '',
|
||||
'chat.whatsapp.message' => '👋 Hola! Estoy buscando más información sobre Covirsa Soluciones en Tecnología. ¿Podrías proporcionarme los detalles que necesito? ¡Te lo agradecería mucho! 💻✨',
|
||||
|
||||
'webTpl.container' => 'custom-container',
|
||||
*/];
|
||||
|
||||
foreach ($settings_array as $key => $value) {
|
||||
Setting::create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
]);
|
||||
};
|
||||
}
|
||||
}
|
97
database/seeders/UserSeeder.php
Normal file
97
database/seeders/UserSeeder.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Koneko\VuexyAdmin\Models\User;
|
||||
use Koneko\VuexyAdmin\Services\AvatarImageService;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class UserSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
// Define el disco y la carpeta
|
||||
$disk = 'public';
|
||||
$directory = 'profile-photos';
|
||||
|
||||
// Verifica si la carpeta existe
|
||||
if (Storage::disk($disk)->exists($directory))
|
||||
Storage::disk($disk)->deleteDirectory($directory);
|
||||
|
||||
//
|
||||
$avatarImageService = new AvatarImageService();
|
||||
|
||||
// Super admin
|
||||
$user = User::create([
|
||||
'name' => 'Koneko Admin',
|
||||
'email' => 'arturo@koneko.mx',
|
||||
'email_verified_at' => now(),
|
||||
'password' => bcrypt('LAdmin123'),
|
||||
'status' => User::STATUS_ENABLED,
|
||||
])->assignRole('SuperAdmin');
|
||||
|
||||
// Actualizamos la foto
|
||||
$avatarImageService->updateProfilePhoto($user, new UploadedFile(
|
||||
'public/vendor/vuexy-admin/img/logo/koneko-02.png',
|
||||
'koneko-02.png'
|
||||
));
|
||||
|
||||
|
||||
// admin
|
||||
$avatarImageService = User::create([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@koneko.mx',
|
||||
'email_verified_at' => now(),
|
||||
'password' => bcrypt('LAdmin123'),
|
||||
'status' => User::STATUS_ENABLED,
|
||||
])->assignRole('Admin');
|
||||
|
||||
$avatarImageService->updateProfilePhoto($user, new UploadedFile(
|
||||
'public/vendor/vuexy-admin/img/logo/koneko-03.png',
|
||||
'koneko-03.png'
|
||||
));
|
||||
|
||||
// Almacenista
|
||||
$avatarImageService = User::create([
|
||||
'name' => 'Almacenista',
|
||||
'email' => 'almacenista@koneko.mx',
|
||||
'email_verified_at' => now(),
|
||||
'password' => bcrypt('LAdmin123'),
|
||||
'status' => User::STATUS_ENABLED,
|
||||
])->assignRole('Almacenista');
|
||||
|
||||
$avatarImageService->updateProfilePhoto($user, new UploadedFile(
|
||||
'public/vendor/vuexy-admin/img/logo/koneko-03.png',
|
||||
'koneko-03.png'
|
||||
));
|
||||
|
||||
|
||||
// Usuarios CSV
|
||||
$csvFile = fopen(base_path("database/data/users.csv"), "r");
|
||||
|
||||
$firstline = true;
|
||||
|
||||
while (($data = fgetcsv($csvFile, 2000, ",")) !== FALSE) {
|
||||
if (!$firstline) {
|
||||
User::create([
|
||||
'name' => $data['0'],
|
||||
'email' => $data['1'],
|
||||
'email_verified_at' => now(),
|
||||
'password' => bcrypt($data['3']),
|
||||
'status' => User::STATUS_ENABLED,
|
||||
])->assignRole($data['2']);
|
||||
}
|
||||
|
||||
$firstline = false;
|
||||
}
|
||||
|
||||
fclose($csvFile);
|
||||
}
|
||||
}
|
129
resources/assets/css/demo.css
Normal file
129
resources/assets/css/demo.css
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* demo.css
|
||||
* File include item demo only specific css only
|
||||
******************************************************************************/
|
||||
|
||||
.light-style .menu .app-brand.demo {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.dark-style .menu .app-brand.demo {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.app-brand-logo.demo {
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
width: 34px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.app-brand-logo.demo svg {
|
||||
width: 35px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.app-brand-text.demo {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
/* ! For .layout-navbar-fixed added fix padding top tpo .layout-page */
|
||||
.layout-navbar-fixed .layout-wrapper:not(.layout-without-menu) .layout-page {
|
||||
padding-top: 64px !important;
|
||||
}
|
||||
.layout-navbar-fixed .layout-wrapper:not(.layout-horizontal):not(.layout-without-menu) .layout-page {
|
||||
padding-top: 72px !important;
|
||||
}
|
||||
/* Navbar page z-index issue solution */
|
||||
.content-wrapper .navbar {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Content
|
||||
******************************************************************************/
|
||||
|
||||
.demo-blocks > * {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.demo-inline-spacing > * {
|
||||
margin: 1rem 0.375rem 0 0 !important;
|
||||
}
|
||||
|
||||
/* ? .demo-vertical-spacing class is used to have vertical margins between elements. To remove margin-top from the first-child, use .demo-only-element class with .demo-vertical-spacing class. For example, we have used this class in forms-input-groups.html file. */
|
||||
.demo-vertical-spacing > * {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.demo-vertical-spacing.demo-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.demo-vertical-spacing-lg > * {
|
||||
margin-top: 1.875rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.demo-vertical-spacing-lg.demo-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.demo-vertical-spacing-xl > * {
|
||||
margin-top: 5rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.demo-vertical-spacing-xl.demo-only-element > :first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.rtl-only {
|
||||
display: none !important;
|
||||
text-align: left !important;
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
[dir='rtl'] .rtl-only {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Dropdown buttons going out of small screens */
|
||||
@media (max-width: 576px) {
|
||||
#dropdown-variation-demo .btn-group .text-truncate {
|
||||
width: 254px;
|
||||
position: relative;
|
||||
}
|
||||
#dropdown-variation-demo .btn-group .text-truncate::after {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
right: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Layout demo
|
||||
******************************************************************************/
|
||||
|
||||
.layout-demo-wrapper {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.layout-demo-placeholder img {
|
||||
width: 900px;
|
||||
}
|
||||
.layout-demo-info {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
245
resources/assets/js/bootstrap-table/bootstrapTableManager.js
vendored
Normal file
245
resources/assets/js/bootstrap-table/bootstrapTableManager.js
vendored
Normal file
@ -0,0 +1,245 @@
|
||||
import '../../vendor/libs/bootstrap-table/bootstrap-table';
|
||||
import '../notifications/LivewireNotification';
|
||||
|
||||
class BootstrapTableManager {
|
||||
constructor(bootstrapTableWrap, config = {}) {
|
||||
const defaultConfig = {
|
||||
header: [],
|
||||
format: [],
|
||||
search_columns: [],
|
||||
actionColumn: false,
|
||||
height: 'auto',
|
||||
minHeight: 300,
|
||||
bottomMargin : 195,
|
||||
search: true,
|
||||
showColumns: true,
|
||||
showColumnsToggleAll: true,
|
||||
showExport: true,
|
||||
exportfileName: 'datatTable',
|
||||
exportWithDatetime: true,
|
||||
showFullscreen: true,
|
||||
showPaginationSwitch: true,
|
||||
showRefresh: true,
|
||||
showToggle: true,
|
||||
/*
|
||||
smartDisplay: false,
|
||||
searchOnEnterKey: true,
|
||||
showHeader: false,
|
||||
showFooter: true,
|
||||
showRefresh: true,
|
||||
showToggle: true,
|
||||
showFullscreen: true,
|
||||
detailView: true,
|
||||
searchAlign: 'right',
|
||||
buttonsAlign: 'right',
|
||||
toolbarAlign: 'left',
|
||||
paginationVAlign: 'bottom',
|
||||
paginationHAlign: 'right',
|
||||
paginationDetailHAlign: 'left',
|
||||
paginationSuccessivelySize: 5,
|
||||
paginationPagesBySide: 3,
|
||||
paginationUseIntermediate: true,
|
||||
*/
|
||||
clickToSelect: true,
|
||||
minimumCountColumns: 4,
|
||||
fixedColumns: true,
|
||||
fixedNumber: 1,
|
||||
idField: 'id',
|
||||
pagination: true,
|
||||
pageList: [25, 50, 100, 500, 1000],
|
||||
sortName: 'id',
|
||||
sortOrder: 'asc',
|
||||
cookie: false,
|
||||
cookieExpire: '365d',
|
||||
cookieIdTable: 'myTableCookies', // Nombre único para las cookies de la tabla
|
||||
cookieStorage: 'localStorage',
|
||||
cookiePath: '/',
|
||||
};
|
||||
|
||||
this.$bootstrapTable = $('.bootstrap-table', bootstrapTableWrap);
|
||||
this.$toolbar = $('.bt-toolbar', bootstrapTableWrap);
|
||||
this.$searchColumns = $('.search_columns', bootstrapTableWrap);
|
||||
this.$btnRefresh = $('.btn-refresh', bootstrapTableWrap);
|
||||
this.$btnClearFilters = $('.btn-clear-filters', bootstrapTableWrap);
|
||||
|
||||
this.config = { ...defaultConfig, ...config };
|
||||
|
||||
this.config.toolbar = `${bootstrapTableWrap} .bt-toolbar`;
|
||||
this.config.height = this.config.height == 'auto'? this.getTableHeight(): this.config.height;
|
||||
this.config.cookieIdTable = this.config.exportWithDatetime? this.config.cookieIdTable + '-' + this.getFormattedDateYMDHm(): this.config.cookieIdTable;
|
||||
|
||||
this.tableFormatters = {}; // Mueve la carga de formatters aquí
|
||||
|
||||
this.initTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula la altura de la tabla.
|
||||
*/
|
||||
getTableHeight() {
|
||||
const btHeight = window.innerHeight - this.$toolbar.height() - this.bottomMargin;
|
||||
|
||||
return btHeight < this.config.minHeight ? this.config.minHeight : btHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un ID único para la tabla basado en una cookie.
|
||||
*/
|
||||
getCookieId() {
|
||||
const generateShortHash = (str) => {
|
||||
let hash = 0;
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash &= hash; // Convertir a entero de 32 bits
|
||||
}
|
||||
|
||||
return Math.abs(hash).toString().substring(0, 12);
|
||||
};
|
||||
|
||||
return `bootstrap-table-cache-${generateShortHash(this.config.title)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga los formatters dinámicamente
|
||||
*/
|
||||
async loadFormatters() {
|
||||
const formattersModules = import.meta.glob('../../../../../**/resources/assets/js/bootstrap-table/*Formatters.js');
|
||||
|
||||
const formatterPromises = Object.entries(formattersModules).map(async ([path, importer]) => {
|
||||
const module = await importer();
|
||||
Object.assign(this.tableFormatters, module);
|
||||
});
|
||||
|
||||
await Promise.all(formatterPromises);
|
||||
}
|
||||
|
||||
btColumns() {
|
||||
const columns = [];
|
||||
|
||||
Object.entries(this.config.header).forEach(([key, value]) => {
|
||||
const columnFormat = this.config.format[key] || {};
|
||||
|
||||
if (typeof columnFormat.formatter === 'object') {
|
||||
const formatterName = columnFormat.formatter.name;
|
||||
const formatterParams = columnFormat.formatter.params || {};
|
||||
|
||||
const formatterFunction = this.tableFormatters[formatterName];
|
||||
if (formatterFunction) {
|
||||
columnFormat.formatter = (value, row, index) => formatterFunction(value, row, index, formatterParams);
|
||||
} else {
|
||||
console.warn(`Formatter "${formatterName}" no encontrado para la columna "${key}"`);
|
||||
}
|
||||
} else if (typeof columnFormat.formatter === 'string') {
|
||||
const formatterFunction = this.tableFormatters[columnFormat.formatter];
|
||||
if (formatterFunction) {
|
||||
columnFormat.formatter = formatterFunction;
|
||||
}
|
||||
}
|
||||
|
||||
if (columnFormat.onlyFormatter) {
|
||||
columns.push({
|
||||
align: 'center',
|
||||
formatter: columnFormat.formatter || (() => ''),
|
||||
forceHide: true,
|
||||
switchable: false,
|
||||
field: key,
|
||||
title: value,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const column = {
|
||||
title: value,
|
||||
field: key,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
columns.push({ ...column, ...columnFormat });
|
||||
});
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Petición AJAX para la tabla.
|
||||
*/
|
||||
ajaxRequest(params) {
|
||||
const url = `${window.location.href}?${$.param(params.data)}&${$('.bt-toolbar :input').serialize()}`;
|
||||
|
||||
$.get(url).then((res) => params.success(res));
|
||||
}
|
||||
|
||||
toValidFilename(str, extension = 'txt') {
|
||||
return str
|
||||
.normalize("NFD") // 🔹 Normaliza caracteres con tilde
|
||||
.replace(/[\u0300-\u036f]/g, "") // 🔹 Elimina acentos y diacríticos
|
||||
.replace(/[<>:"\/\\|?*\x00-\x1F]/g, '') // 🔹 Elimina caracteres inválidos
|
||||
.replace(/\s+/g, '-') // 🔹 Reemplaza espacios con guiones
|
||||
.replace(/-+/g, '-') // 🔹 Evita múltiples guiones seguidos
|
||||
.replace(/^-+|-+$/g, '') // 🔹 Elimina guiones al inicio y fin
|
||||
.toLowerCase() // 🔹 Convierte a minúsculas
|
||||
+ (extension ? '.' + extension.replace(/^\.+/, '') : ''); // 🔹 Asegura la extensión válida
|
||||
}
|
||||
|
||||
getFormattedDateYMDHm(date = new Date()) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // 🔹 Asegura dos dígitos
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${year}${month}${day}-${hours}${minutes}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inicia la tabla después de cargar los formatters
|
||||
*/
|
||||
async initTable() {
|
||||
await this.loadFormatters(); // Asegura que los formatters estén listos antes de inicializar
|
||||
|
||||
this.$bootstrapTable
|
||||
.bootstrapTable('destroy').bootstrapTable({
|
||||
height: this.config.height,
|
||||
locale: 'es-MX',
|
||||
ajax: (params) => this.ajaxRequest(params),
|
||||
toolbar: this.config.toolbar,
|
||||
search: this.config.search,
|
||||
showColumns: this.config.showColumns,
|
||||
showColumnsToggleAll: this.config.showColumnsToggleAll,
|
||||
showExport: this.config.showExport,
|
||||
exportTypes: ['csv', 'txt', 'xlsx'],
|
||||
exportOptions: {
|
||||
fileName: this.config.fileName,
|
||||
},
|
||||
showFullscreen: this.config.showFullscreen,
|
||||
showPaginationSwitch: this.config.showPaginationSwitch,
|
||||
showRefresh: this.config.showRefresh,
|
||||
showToggle: this.config.showToggle,
|
||||
clickToSelect: this.config.clickToSelect,
|
||||
minimumCountColumns: this.config.minimumCountColumns,
|
||||
fixedColumns: this.config.fixedColumns,
|
||||
fixedNumber: this.config.fixedNumber,
|
||||
idField: this.config.idField,
|
||||
pagination: this.config.pagination,
|
||||
pageList: this.config.pageList,
|
||||
sidePagination: "server",
|
||||
sortName: this.config.sortName,
|
||||
sortOrder: this.config.sortOrder,
|
||||
mobileResponsive: true,
|
||||
resizable: true,
|
||||
cookie: this.config.cookie,
|
||||
cookieExpire: this.config.cookieExpire,
|
||||
cookieIdTable: this.config.cookieIdTable,
|
||||
columns: this.btColumns(),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.BootstrapTableManager = BootstrapTableManager;
|
132
resources/assets/js/bootstrap-table/globalConfig.js
Normal file
132
resources/assets/js/bootstrap-table/globalConfig.js
Normal file
@ -0,0 +1,132 @@
|
||||
const appRoutesElement = document.getElementById('app-routes');
|
||||
|
||||
export const routes = appRoutesElement ? JSON.parse(appRoutesElement.textContent) : {};
|
||||
|
||||
export const booleanStatusCatalog = {
|
||||
activo: {
|
||||
trueText: 'Activo',
|
||||
falseText: 'Inactivo',
|
||||
trueClass: 'badge bg-label-success',
|
||||
falseClass: 'badge bg-label-danger',
|
||||
},
|
||||
habilitado: {
|
||||
trueText: 'Habilitado',
|
||||
falseText: 'Deshabilitado',
|
||||
trueClass: 'badge bg-label-success',
|
||||
falseClass: 'badge bg-label-danger',
|
||||
trueIcon: 'ti ti-checkup-list',
|
||||
falseIcon: 'ti ti-ban',
|
||||
},
|
||||
checkSI: {
|
||||
trueText: 'SI',
|
||||
falseIcon: '',
|
||||
trueClass: 'badge bg-label-info',
|
||||
falseText: '',
|
||||
},
|
||||
check: {
|
||||
trueIcon: 'ti ti-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
checkbox: {
|
||||
trueIcon: 'ti ti-checkbox',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
checklist: {
|
||||
trueIcon: 'ti ti-checklist',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
phone_done: {
|
||||
trueIcon: 'ti ti-phone-done',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
checkup_list: {
|
||||
trueIcon: 'ti ti-checkup-list',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
list_check: {
|
||||
trueIcon: 'ti ti-list-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
camera_check: {
|
||||
trueIcon: 'ti ti-camera-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
mail_check: {
|
||||
trueIcon: 'ti ti-mail-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
clock_check: {
|
||||
trueIcon: 'ti ti-clock-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
user_check: {
|
||||
trueIcon: 'ti ti-user-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
circle_check: {
|
||||
trueIcon: 'ti ti-circle-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
shield_check: {
|
||||
trueIcon: 'ti ti-shield-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
},
|
||||
calendar_check: {
|
||||
trueIcon: 'ti ti-calendar-check',
|
||||
falseIcon: '',
|
||||
trueClass: 'text-green-800',
|
||||
falseClass: '',
|
||||
}
|
||||
};
|
||||
|
||||
export const badgeColorCatalog = {
|
||||
primary: { color: 'primary' },
|
||||
secondary: { color: 'secondary' },
|
||||
success: { color: 'success' },
|
||||
danger: { color: 'danger' },
|
||||
warning: { color: 'warning' },
|
||||
info: { color: 'info' },
|
||||
dark: { color: 'dark' },
|
||||
light: { color: 'light', textColor: 'text-dark' }
|
||||
};
|
||||
|
||||
export const statusIntBadgeBgCatalogCss = {
|
||||
1: 'warning',
|
||||
2: 'info',
|
||||
10: 'success',
|
||||
12: 'danger',
|
||||
11: 'warning'
|
||||
};
|
||||
|
||||
export const statusIntBadgeBgCatalog = {
|
||||
1: 'Inactivo',
|
||||
2: 'En proceso',
|
||||
10: 'Activo',
|
||||
11: 'Archivado',
|
||||
12: 'Cancelado',
|
||||
};
|
||||
|
193
resources/assets/js/bootstrap-table/globalFormatters.js
Normal file
193
resources/assets/js/bootstrap-table/globalFormatters.js
Normal file
@ -0,0 +1,193 @@
|
||||
import { booleanStatusCatalog, statusIntBadgeBgCatalogCss, statusIntBadgeBgCatalog } from './globalConfig';
|
||||
import {routes} from '../../../../../laravel-vuexy-admin/resources/assets/js/bootstrap-table/globalConfig.js';
|
||||
|
||||
export const userActionFormatter = (value, row, index) => {
|
||||
if (!row.id) return '';
|
||||
|
||||
const showUrl = routes['admin.user.show'].replace(':id', row.id);
|
||||
const editUrl = routes['admin.user.edit'].replace(':id', row.id);
|
||||
const deleteUrl = routes['admin.user.delete'].replace(':id', row.id);
|
||||
|
||||
return `
|
||||
<div class="flex space-x-2">
|
||||
<a href="${editUrl}" title="Editar" class="icon-button hover:text-slate-700">
|
||||
<i class="ti ti-edit"></i>
|
||||
</a>
|
||||
<a href="${deleteUrl}" title="Eliminar" class="icon-button hover:text-slate-700">
|
||||
<i class="ti ti-trash"></i>
|
||||
</a>
|
||||
<a href="${showUrl}" title="Ver" class="icon-button hover:text-slate-700">
|
||||
<i class="ti ti-eye"></i>
|
||||
</a>
|
||||
</div>
|
||||
`.trim();
|
||||
};
|
||||
|
||||
export const dynamicBooleanFormatter = (value, row, index, options = {}) => {
|
||||
const { tag = 'default', customOptions = {} } = options;
|
||||
const catalogConfig = booleanStatusCatalog[tag] || {};
|
||||
|
||||
const finalOptions = {
|
||||
...catalogConfig,
|
||||
...customOptions, // Permite sobreescribir la configuración predeterminada
|
||||
...options // Permite pasar opciones rápidas
|
||||
};
|
||||
|
||||
const {
|
||||
trueIcon = '',
|
||||
falseIcon = '',
|
||||
trueText = 'Sí',
|
||||
falseText = 'No',
|
||||
trueClass = 'badge bg-label-success',
|
||||
falseClass = 'badge bg-label-danger',
|
||||
iconClass = 'text-green-800'
|
||||
} = finalOptions;
|
||||
|
||||
const trueElement = !trueIcon && !trueText ? '' : `<span class="${trueClass}">${trueIcon ? `<i class="${trueIcon} ${iconClass}"></i> ` : ''}${trueText}</span>`;
|
||||
const falseElement = !falseIcon && !falseText ? '' : `<span class="${falseClass}">${falseIcon ? `<i class="${falseIcon}"></i> ` : ''}${falseText}</span>`;
|
||||
|
||||
return value? trueElement : falseElement;
|
||||
};
|
||||
|
||||
export const dynamicBadgeFormatter = (value, row, index, options = {}) => {
|
||||
const {
|
||||
color = 'primary', // Valor por defecto
|
||||
textColor = '', // Permite agregar color de texto si es necesario
|
||||
additionalClass = '' // Permite añadir clases adicionales
|
||||
} = options;
|
||||
|
||||
return `<span class="badge bg-${color} ${textColor} ${additionalClass}">${value}</span>`;
|
||||
};
|
||||
|
||||
export const statusIntBadgeBgFormatter = (value, row, index) => {
|
||||
return value
|
||||
? `<span class="badge bg-label-${statusIntBadgeBgCatalogCss[value]}">${statusIntBadgeBgCatalog[value]}</span>`
|
||||
: '';
|
||||
}
|
||||
|
||||
export const textNowrapFormatter = (value, row, index) => {
|
||||
if (!value) return '';
|
||||
return `<span class="text-nowrap">${value}</span>`;
|
||||
}
|
||||
|
||||
|
||||
export const toCurrencyFormatter = (value, row, index) => {
|
||||
return isNaN(value) ? '' : Number(value).toCurrency();
|
||||
}
|
||||
|
||||
export const numberFormatter = (value, row, index) => {
|
||||
return isNaN(value) ? '' : Number(value);
|
||||
}
|
||||
|
||||
export const monthFormatter = (value, row, index) => {
|
||||
switch (parseInt(value)) {
|
||||
case 1:
|
||||
return 'Enero';
|
||||
case 2:
|
||||
return 'Febrero';
|
||||
case 3:
|
||||
return 'Marzo';
|
||||
case 4:
|
||||
return 'Abril';
|
||||
case 5:
|
||||
return 'Mayo';
|
||||
case 6:
|
||||
return 'Junio';
|
||||
case 7:
|
||||
return 'Julio';
|
||||
case 8:
|
||||
return 'Agosto';
|
||||
case 9:
|
||||
return 'Septiembre';
|
||||
case 10:
|
||||
return 'Octubre';
|
||||
case 11:
|
||||
return 'Noviembre';
|
||||
case 12:
|
||||
return 'Diciembre';
|
||||
}
|
||||
}
|
||||
|
||||
export const humaneTimeFormatter = (value, row, index) => {
|
||||
return isNaN(value) ? '' : Number(value).humaneTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera la URL del avatar basado en iniciales o devuelve la foto de perfil si está disponible.
|
||||
* @param {string} fullName - Nombre completo del usuario.
|
||||
* @param {string|null} profilePhoto - Ruta de la foto de perfil.
|
||||
* @returns {string} - URL del avatar.
|
||||
*/
|
||||
function getAvatarUrl(fullName, profilePhoto) {
|
||||
const baseUrl = window.baseUrl || '';
|
||||
|
||||
if (profilePhoto) {
|
||||
return `${baseUrl}storage/profile-photos/${profilePhoto}`;
|
||||
}
|
||||
|
||||
return `${baseUrl}admin/usuario/avatar/?name=${fullName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea la columna del perfil de usuario con avatar, nombre y correo.
|
||||
*/
|
||||
export const userProfileFormatter = (value, row, index) => {
|
||||
if (!row.id) return '';
|
||||
|
||||
const profileUrl = routes['admin.user.show'].replace(':id', row.id);
|
||||
const avatar = getAvatarUrl(row.full_name, row.profile_photo_path);
|
||||
const email = row.email ? row.email : 'Sin correo';
|
||||
|
||||
return `
|
||||
<div class="flex items-center space-x-3" style="min-width: 240px">
|
||||
<a href="${profileUrl}" class="flex-shrink-0">
|
||||
<img src="${avatar}" alt="Avatar" class="w-10 h-10 rounded-full border border-gray-300 shadow-sm hover:scale-105 transition-transform">
|
||||
</a>
|
||||
<div class="truncate">
|
||||
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.full_name}</a>
|
||||
<small class="text-muted block truncate">${email}</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatea la columna del perfil de contacto con avatar, nombre y correo.
|
||||
*/
|
||||
export const contactProfileFormatter = (value, row, index) => {
|
||||
if (!row.id) return '';
|
||||
|
||||
const profileUrl = routes['admin.contact.show'].replace(':id', row.id);
|
||||
const avatar = getAvatarUrl(row.full_name, row.profile_photo_path);
|
||||
const email = row.email ? row.email : 'Sin correo';
|
||||
|
||||
return `
|
||||
<div class="flex items-center space-x-3" style="min-width: 240px">
|
||||
<a href="${profileUrl}" class="flex-shrink-0">
|
||||
<img src="${avatar}" alt="Avatar" class="w-10 h-10 rounded-full border border-gray-300 shadow-sm hover:scale-105 transition-transform">
|
||||
</a>
|
||||
<div class="truncate">
|
||||
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.full_name}</a>
|
||||
<small class="text-muted block truncate">${email}</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const creatorFormatter = (value, row, index) => {
|
||||
if (!row.creator) return '';
|
||||
|
||||
const email = row.creator_email || 'Sin correo';
|
||||
const showUrl = routes['admin.user.show'].replace(':id', row.id);
|
||||
|
||||
|
||||
return `
|
||||
<div class="flex flex-col">
|
||||
<a href="${showUrl}" class="font-medium text-slate-600 hover:underline block text-wrap">${row.creator}</a>
|
||||
<small class="text-muted">${email}</small>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user