first commit

This commit is contained in:
Arturo Corro 2025-03-05 20:28:54 -06:00
parent f54ca8e341
commit 68ca619829
570 changed files with 111124 additions and 175 deletions

18
.editorconfig Normal file
View 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
View 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
View File

@ -0,0 +1,10 @@
/node_modules
/vendor
/.vscode
/.nova
/.fleet
/.phpactor.json
/.phpunit.cache
/.phpunit.result.cache
/.zed
/.idea

16
.prettierignore Normal file
View 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
View 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
}
}
]
}

View 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']),
]);
}
}

View 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'];
}
}

View 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();
}
}

View 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();
}
}

View 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
View 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
View 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**.

View 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.');
}
}

View 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
View 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
View 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);
}
}
}
}
}

View 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');
}
}

View 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]);
}
*/
}

View 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'));
}
}

View 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'));
}
}

View 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();
}
}

View 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');
}
}

View 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]);
}
}

View 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']);
}
}

View 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'
]);
}
}
}

View 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'));
}
}

View 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'
]);
}
}
}

View 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);
}
}

View 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();
}
}
}

View 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));
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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());
}
}

View 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());
}
}

View 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');
}
}

View 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()
]);
}
}

View 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');
}
}

View 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)
]);
}
}

View 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;
}
}

View 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
View 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';
}
}

View 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),
]);
}
}

View 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';
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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'
];
}

View 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.');
}
}
}

View 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();
}
}

View 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]);
});
}
}

View 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);
}
}

View 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,
]);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Koneko\VuexyAdmin\Queries;
class GenericQueryBuilder extends BootstrapTableQueryBuilder
{
// Custom query builder
}

223
README.md
View File

@ -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
View 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.');
}
}
}

View 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();
}
}
}
}

View 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");
}
}

View 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();
}
}
}

View 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)];
}
}

View 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'),
]);
}
}

View 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);
}
}

View 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
View 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']);
}
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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',
],
]
],
];

View 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
View 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
1 name email role password
2 Administrador Web webadmin@concierge.test Administrador Web LAdmin123
3 Productos y servicios productos@concierge.test Productos y servicios LAdmin123
4 Recursos humanos rrhh@concierge.test Recursos humanos LAdmin123
5 Nómina nomina@concierge.test Nómina LAdmin123
6 Activos fijos activos@concierge.test Activos fijos LAdmin123
7 Compras y gastos compras@concierge.test Compras y gastos LAdmin123
8 CRM crm@concierge.test CRM LAdmin123
9 Vendedor vendedor@concierge.test Vendedor LAdmin123
10 Gerente gerente@concierge.test Gerente LAdmin123
11 Facturación facturacion@concierge.test Facturación LAdmin123
12 Facturación avanzado facturacion_avanzado@concierge.test Facturación avanzado LAdmin123
13 Finanzas finanzas@concierge.test Finanzas LAdmin123
14 Auditor auditor@concierge.test Auditor LAdmin123

View 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,
]);
}
}

View 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']);
});
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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']);
}
};

View File

@ -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',
] : []));
});
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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);
}
};

View 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();
}
}

View 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,
]);
};
}
}

View 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);
}
}

View 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;
}

View 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;

View 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',
};

View 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