Testing Alpha

This commit is contained in:
Arturo Corro 2025-05-11 14:14:50 -06:00
parent 988b86a33d
commit a7002701f5
1903 changed files with 77534 additions and 36485 deletions

52
.gitattributes vendored
View File

@ -1,24 +1,36 @@
# Normaliza los saltos de línea en diferentes SO
# Normaliza los saltos de línea para todos los sistemas operativos
* text=auto eol=lf
# Reglas para archivos específicos
# Reglas de diferencia por tipo de archivo
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
*.json diff=json
*.yml diff=yaml
*.yaml diff=yaml
*.stub diff=php
# Evitar que estos archivos se exporten con Composer create-project
/.github export-ignore
/.gitignore export-ignore
/.git export-ignore
.gitattributes export-ignore
.editorconfig export-ignore
.prettierrc.json export-ignore
.prettierignore export-ignore
.eslintrc.json export-ignore
CHANGELOG.md export-ignore
CONTRIBUTING.md export-ignore
README.md export-ignore
composer.lock export-ignore
package-lock.json export-ignore
# Archivos que NO deben exportarse con Composer create-project
/.github export-ignore
/.gitignore export-ignore
/.git export-ignore
.gitattributes export-ignore
.editorconfig export-ignore
.prettierrc.json export-ignore
.prettierignore export-ignore
.eslintrc.json export-ignore
CHANGELOG.md export-ignore
CONTRIBUTING.md export-ignore
README.md export-ignore
phpunit.xml export-ignore
phpunit.xml.dist export-ignore
composer.lock export-ignore
package-lock.json export-ignore
vite.config.js export-ignore
tailwind.config.js export-ignore
tests export-ignore
tests/ export-ignore
resources/assets export-ignore
resources/sass export-ignore

68
.gitignore vendored
View File

@ -1,11 +1,59 @@
/node_modules
/vendor
/.vscode
/.nova
/.fleet
/.phpactor.json
# ⚙️ Laravel Package Defaults
/vendor/
composer.lock
# 🧪 PHPUnit
.phpunit.result.cache
/.phpunit.cache
/.phpunit.result.cache
/.zed
/.idea
composer.lock
phpunit.xml
phpunit.xml.dist
# 🧹 Cache y logs
/.cache
/storage/*.key
/storage/pail
*.log
*.dump
*.bak
*.tmp
*.swp
# 🔐 Entornos y configuraciones
.env
.env.*
auth.json
.phpactor.json
.php-cs-fixer.cache
phpstan.neon.local
homestead.yaml
Homestead.json
# 🧱 Compilación frontend (Vite, Mix, Webpack, Tailwind)
/node_modules/
public/build/
public/hot/
public/storage/
.vite
# 🧪 Tests y mocks (solo si generas temporalmente)
/coverage/
*.test.*
*.spec.*
# 🛠️ IDEs y herramientas de desarrollo
/.idea/
/.vscode/
/.nova/
/.zed/
/.fleet/
*.sublime-workspace
*.sublime-project
# 📦 Archivos del sistema
.DS_Store
Thumbs.db
# 🚀 Entornos de staging / producción
*.local.*
*.production.*
*.staging.*

99
CONVENTIONS.md Normal file
View File

@ -0,0 +1,99 @@
# ![Koneko ERP](https://git.koneko.mx/koneko-st/koneko-st/raw/branch/main/logo-images/horizontal-05.png) Convenciones de Estructura de Componentes
📅 *Última actualización:* 2025-04-03
🔧 *Aplicable a todos los módulos Composer de Koneko ERP*
---
## 📁 Estructura General de un Componente
```plaintext
component-root/
├── config/ ← Configuraciones del módulo
├── Database/
│ ├── data/ ← Archivos JSON, CSV, XLSX
│ ├── factories/ ← Factories para testing y seeders
│ ├── migrations/ ← Migraciones del esquema del módulo
│ └── Seeders/ ← Seeders base y de datos fake
├── Enums/ ← Enums (PSR-4) usados por el módulo
├── Events/ ← Eventos del módulo
├── Http/
│ ├── Controllers/ ← Controladores
│ └── Middleware/ ← Middlewares específicos del módulo
├── Livewire/ ← Componentes Livewire organizados por dominio
├── Models/ ← Modelos Eloquent
├── Notifications/ ← Notificaciones personalizadas
├── Providers/ ← Service Providers del módulo
├── Services/ ← Servicios (lógica de negocio)
├── Support/
│ ├── Base/ ← Clases base abstractas
│ ├── Builders/ ← Configuradores de vistas tipo índice
│ ├── Macros/ ← Macros de Str, Collection, etc.
│ ├── Queries/ ← Query Builders avanzados
│ ├── Registries/ ← Registro dinámico de configuración
│ └── Validation/ ← Validaciones personalizadas
├── Traits/
│ ├── Audit/ ← Traits para auditoría y tracking
│ ├── Metadata/ ← Traits para metadatos del modelo
│ ├── Users/ ← Traits relacionados con usuarios
│ └── Indexing/ ← Traits usados por configuradores de índices
├── resources/
│ ├── assets/ ← JS, SCSS, íconos o fuentes específicos
│ ├── faker-images/ ← Imágenes utilizadas en datos de prueba
│ ├── lang/ ← Archivos de traducción
│ └── views/ ← Vistas Blade
├── routes/
│ └── admin.php ← Rutas internas del módulo
├── storage/ ← Recursos adicionales (ej. fuentes)
└── README.md ← Documentación del componente
```
---
## 🧠 Convenciones Generales
- Todos los módulos deben seguir PSR-4.
- Los archivos deben nombrarse en *PascalCase* excepto `config/*.php` y rutas.
- Los `Seeder` deben ser agrupados por módulo si el componente los agrupa (ej. `vuexyAdmin`, `vuexyWarehouse`).
- Los `Factory` deben ser compatibles con `SeederWithFakeImages`.
---
## 🖼️ Imágenes Faker
- Carpeta: `resources/faker-images/<dominio>`
- Subcarpetas válidas: `users/`, `stores/`, `products/`, etc.
- Las imágenes se usan exclusivamente para entornos de testing/demostración.
- Nunca se publican al frontend ni se exponen directamente.
---
## 🧪 Factories
- Todas las `factories` deben estar en `Database/factories/`.
- Si se extiende un modelo (`Koneko\VuexyAdmin\Models\User`), usar `new (User::class)` dinámico.
- Compatible con `SeederOrchestrator` y `config/seeder.php`.
---
## 📊 Configuradores de Índice
- Los index deben implementar `BaseModelIndexConfig` o su extensión.
- Pueden usar Traits como `HandlesFactory`, `HandlesIndexColumns`, `HandlesQueryBuilder`, etc.
- Se recomienda usar `Support/Builders/` para los configuradores y `Support/Registries/` si son extendibles.
---
## 📚 Traducciones
- Usar `resources/lang/es/` con archivos separados por dominio (`auth.php`, `validation.php`, etc.).
- `es_MX.json` puede usarse para traducciones inline.
---
## 📌 Tips
- Si un componente tiene `Service`, `Seeder`, `Factory` y `Livewire`, deben estar todos organizados en sus carpetas respectivas.
- La estructura del componente debe ser lo suficientemente clara para no depender de documentación externa.
---

View File

@ -1,43 +0,0 @@
<?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

@ -1,26 +0,0 @@
<?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".');
}
}
}

View File

@ -1,42 +0,0 @@
<?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 index(CacheConfigService $cacheConfigService)
{
$configCache = $cacheConfigService->getConfig();
return view('vuexy-admin::cache-manager.index', compact('configCache'));
}
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);
}
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
use Koneko\VuexyAdmin\Queries\GenericQueryBuilder;
class GlobalSettingsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if ($request->ajax()) {
$bootstrapTableIndexConfig = [
'table' => 'settings',
'columns' => [
'settings.id',
'settings.key',
'settings.category',
'settings.user_id',
DB::raw("CONCAT_WS(' ', users.name, users.last_name) AS user_fullname"),
'settings.value_string',
'settings.value_integer',
'settings.value_boolean',
'settings.value_float',
DB::raw("IF(LENGTH(settings.value_text) > 60, CONCAT(LEFT(settings.value_text, 60), '..'), settings.value_text) AS value_text"),
DB::raw("IF(settings.value_binary, '-BINARY-', '') AS value_binary"),
'settings.mime_type',
'settings.file_name',
'settings.created_at',
'settings.updated_at',
'settings.updated_by',
DB::raw("CONCAT_WS(' ', creator.name, creator.last_name) AS creator_name"),
],
'joins' => [
[
'table' => 'users',
'first' => 'settings.user_id',
'second' => 'users.id',
'type' => 'leftJoin',
],
[
'table' => 'users',
'first' => 'settings.updated_by',
'second' => 'creator.id',
'type' => 'leftJoin',
'alias' => 'creator',
],
],
'filters' => [
'search' => [
'settings.key',
'settings.category',
'users.name',
'users.last_name',
'creator.name',
'creator.last_name',
],
],
'sort_column' => 'settings.key',
'default_sort_order' => 'asc',
];
return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson();
}
return view('vuexy-admin::global-settings.index');
}
}

View File

@ -1,21 +0,0 @@
<?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

@ -1,43 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Koneko\VuexyAdmin\Queries\GenericQueryBuilder;
class PermissionController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if ($request->ajax()) {
$bootstrapTableIndexConfig = [
'table' => 'permissions',
'columns' => [
'permissions.id',
'permissions.name',
'permissions.group_name',
'permissions.sub_group_name',
'permissions.action',
'permissions.guard_name',
'permissions.created_at',
'permissions.updated_at',
],
'filters' => [
'search' => ['permissions.name', 'permissions.group_name', 'permissions.sub_group_name', 'permissions.action'],
],
'sort_column' => 'permissions.name',
'default_sort_order' => 'asc',
];
return (new GenericQueryBuilder($request, $bootstrapTableIndexConfig))->getJson();
}
return view('vuexy-admin::permissions.index');
}
}

View File

@ -1,76 +0,0 @@
<?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

@ -1,153 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\{Auth,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.name AS full_name',
'users.email',
'users.email_verified_at',
'users.profile_photo_path',
'users.status',
'users.created_by',
'users.created_at',
'users.updated_at',
],
'filters' => [
'search' => ['users.code', 'users.full_name', 'users.email', 'parent_name'],
],
'sort_column' => 'users.full_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')){
app(AvatarImageService::class)->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')){
app(AvatarImageService::class)->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

@ -1,51 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Koneko\VuexyAdmin\Services\AvatarInitialsService;
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);
try {
return app(AvatarInitialsService::class)->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

@ -1,100 +0,0 @@
<?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\SettingsService;
use Koneko\VuexyAdmin\Services\VuexyAdminService;
/**
* Controlador para la gestión de funcionalidades administrativas de Vuexy
*/
class VuexyAdminController extends Controller
{
/**
* Muestra la vista de configuraciones generales
*
* @return \Illuminate\View\View
*/
public function GeneralSettings()
{
return view('vuexy-admin::general-settings.index');
}
/**
* Muestra la vista de configuraciones SMTP
*
* @return \Illuminate\View\View
*/
public function smtpSettings()
{
return view('vuexy-admin::sendmail-settings.index');
}
/**
* Muestra la vista de configuraciones de interfaz
*
* @return \Illuminate\View\View
*/
public function VuexyInterfaceSettings()
{
return view('vuexy-admin::interface-settings.index');
}
/**
* Realiza búsqueda en la barra de navegación
*
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Http\Exceptions\HttpResponseException
*/
public function searchNavbar()
{
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
return response()->json(app(VuexyAdminService::class)->getVuexySearchData());
}
/**
* Actualiza los enlaces rápidos del usuario
*
* @param Request $request Datos de la solicitud
* @return void
* @throws \Illuminate\Http\Exceptions\HttpResponseException
* @throws \Illuminate\Validation\ValidationException
*/
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('key', 'quicklinks')
->where('user_id', Auth::user()->id)
->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'];
});
}
app(SettingsService::class)->set('quicklinks', json_encode($quickLinks), Auth::user()->id, 'vuexy-admin');
VuexyAdminService::clearQuickLinksCache();
}
}

View File

@ -1,37 +0,0 @@
<?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

@ -1,25 +0,0 @@
<?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

@ -1,26 +0,0 @@
<?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

@ -1,515 +0,0 @@
<?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

@ -1,686 +0,0 @@
<?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 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;
/**
* Retorna la ruta de la vista asociada al formulario.
*
* @return string Ruta de la vista Blade.
*/
abstract protected function viewPath(): string;
// ===================== CONFIGURACIÓN =====================
/**
* Define los campos del formulario.
*
* @return array<string, mixed>
*/
protected function fields(): array
{
return (new ($this->model()))->getFillable();
}
/**
* Retorna los valores por defecto para los campos del formulario.
*
* @return array<string, mixed> Valores predeterminados.
*/
protected function defaults(): array
{
return [];
}
/**
* Campo que se debe enfocar cuando se abra el formulario.
*
* @return string
*/
protected function focusOnOpen(): string
{
return '';
}
// ===================== OPCIONES =====================
/**
* Devuelve las opciones que se mostrarán en los selectores del formulario.
*
* @return array<string, mixed> Opciones para los campos del formulario.
*/
protected function options(): array
{
return [];
}
// ===================== 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 = Str::camel($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) {
dd($this->fields());
$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

@ -1,98 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Permissions;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
use Spatie\Permission\Models\Permission;
use Illuminate\Support\Facades\DB;
/**
* Listado de Permisos, extiende de la clase base AbstractIndexComponent
* para reutilizar la lógica de configuración y renderizado de tablas.
*/
class PermissionsIndex extends AbstractIndexComponent
{
/**
* Define la clase del modelo a usar en este Index.
*
* @return string
*/
protected function model(): string
{
return Permission::class;
}
/**
* Retorna las columnas de la tabla.
*
* @return array
*/
protected function columns(): array
{
return [
'action' => 'Acciones',
'name' => 'Nombre del Permiso',
'group_name' => 'Grupo',
'sub_group_name' => 'Subgrupo',
'action' => 'Acción',
'guard_name' => 'Guard',
'created_at' => 'Creado',
'updated_at' => 'Modificado',
];
}
/**
* Retorna el formato para cada columna.
*
* @return array
*/
protected function format(): array
{
return [
'action' => [
'formatter' => 'storeActionFormatter',
'onlyFormatter' => true,
],
'name' => [
'switchable' => false,
],
'created_at' => [
'formatter' => 'whitespaceNowrapFormatter',
'align' => 'center',
'visible' => false,
],
'updated_at' => [
'formatter' => 'whitespaceNowrapFormatter',
'align' => 'center',
'visible' => false,
],
];
}
/**
* Sobrescribe la configuración base de la tabla.
*
* @return array
*/
protected function bootstraptableConfig(): array
{
return array_merge(parent::bootstraptableConfig(), [
'sortName' => 'name',
'exportFileName' => 'Permisos',
'showFullscreen' => false,
'showPaginationSwitch'=> false,
'showRefresh' => false,
'pagination' => false,
]);
}
/**
* Retorna la vista a renderizar por este componente.
*
* @return string
*/
protected function viewPath(): string
{
return 'vuexy-admin::livewire.permissions.index';
}
}

View File

@ -1,174 +0,0 @@
<?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

@ -1,306 +0,0 @@
<?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

@ -1,129 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Users;
use Koneko\VuexyAdmin\Livewire\Form\AbstractFormOffCanvasComponent;
use Koneko\VuexyAdmin\Models\User;
/**
* 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
{
/**
* Propiedades del formulario relacionadas con el usuario.
*/
public $code,
$name,
$last_name,
$email,
$status;
/**
* 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;
}
/**
* Campo que se debe enfocar cuando se abra el formulario.
*
* @return string
*/
protected function focusOnOpen(): string
{
return 'name';
}
// ===================== VALIDACIONES =====================
/**
* 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 [
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'name' => ['required', 'string', 'max:96'],
];
case 'delete':
return [
'confirmDeletion' => 'accepted', // Asegura que el usuario confirme la eliminación
];
default:
return [];
}
}
/**
* 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 [
'name.required' => 'El nombre del usuario es obligatorio.',
];
}
/**
* Ruta de la vista asociada con este formulario.
*
* @return string
*/
protected function viewPath(): string
{
return 'vuexy-admin::livewire.users.offcanvas-form';
}
}

View File

@ -1,232 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\Users;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
class UsersIndex extends AbstractIndexComponent
{
/**
* 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',
'full_name' => 'Nombre completo',
'email' => 'Correo electrónico',
'email_verified_at' => 'Correo verificado',
'created_by' => 'Creado Por',
'status' => 'Estatus',
'created_at' => 'Fecha 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,
],
'email_verified_at' => [
'visible' => false,
],
'parent_id' => [
'formatter' => 'parentProfileFormatter',
'visible' => false,
],
'agent_id' => [
'formatter' => 'agentProfileFormatter',
'visible' => false,
],
'phone' => [
'formatter' => 'telFormatter',
'visible' => false,
],
'mobile' => [
'formatter' => 'telFormatter',
],
'whatsapp' => [
'formatter' => 'whatsappFormatter',
],
'company' => [
'formatter' => 'textNowrapFormatter',
],
'curp' => [
'visible' => false,
],
'nss' => [
'visible' => false,
],
'license_number' => [
'visible' => false,
],
'job_position' => [
'formatter' => 'textNowrapFormatter',
'visible' => false,
],
'pais' => [
'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_supplier' => [
'formatter' => [
'name' => 'dynamicBooleanFormatter',
'params' => ['tag' => 'checkSI'],
],
'align' => 'center',
],
'is_carrier' => [
'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,
],
];
}
/**
* 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

@ -1,66 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\SettingsService;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
class AppDescriptionSettings extends Component
{
private $targetNotify = "#app-description-settings-card .notification-container";
public $app_name,
$title,
$description;
public function mount()
{
$this->resetForm();
}
public function save()
{
$this->validate([
'app_name' => 'required|string|max:255',
'title' => 'required|string|max:255',
'description' => 'nullable|string|max:255',
]);
// Guardar título del sitio en configuraciones
$SettingsService = app(SettingsService::class);
$SettingsService->set('admin.app_name', $this->app_name, null, 'vuexy-admin');
$SettingsService->set('admin.title', $this->title, null, 'vuexy-admin');
$SettingsService->set('admin.description', $this->description, null, 'vuexy-admin');
// Limpiar cache de plantilla
app(AdminTemplateService::class)->clearAdminVarsCache();
// Recargamos el formulario
$this->resetForm();
// Notificación de éxito
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.'
);
}
public function resetForm()
{
// Obtener los valores de las configuraciones de la base de datos
$settings = app(AdminTemplateService::class)->getAdminVars();
$this->app_name = $settings['app_name'];
$this->title = $settings['title'];
$this->description = $settings['description'];
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.app-description-settings');
}
}

View File

@ -1,103 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;;
use Koneko\VuexyAdmin\Livewire\Table\AbstractIndexComponent;
use Koneko\VuexyAdmin\Models\Setting;
/**
* Listado de Configuraciones (settings), extiende la clase base AbstractIndexComponent
* para reutilizar la lógica de configuración y renderizado de tablas.
*/
class GlobalSettingsIndex extends AbstractIndexComponent
{
/**
* Define la clase o instancia del modelo a usar.
*
* @return string
*/
protected function model(): string
{
return Setting::class;
}
/**
* Retorna las columnas (header) de la tabla.
* Se eligen las columnas más relevantes para mantener una interfaz mobile-first.
*
* @return array
*/
protected function columns(): array
{
return [
'action' => 'Acciones',
'key' => 'Clave',
'category' => 'Categoría',
'user_fullname' => 'Usuario',
'created_at' => 'Creado',
];
}
/**
* Retorna el formato (formatter) para cada columna.
* Se aplican formatters para resaltar la información y se establecen propiedades de alineación y visibilidad.
*
* @return array
*/
protected function format(): array
{
return [
'action' => [
'formatter' => 'settingActionFormatter',
'onlyFormatter' => true,
],
'key' => [
'formatter' => [
'name' => 'dynamicBadgeFormatter',
'params' => ['color' => 'primary'],
],
'align' => 'center',
'switchable' => false,
],
'category' => [
'switchable' => false,
],
'user_fullname' => [
'switchable' => false,
],
'created_at' => [
'formatter' => 'whitespaceNowrapFormatter',
'align' => 'center',
'visible' => false,
],
];
}
/**
* Sobrescribe la configuración base de la tabla para ajustar
* la vista y funcionalidades específicas del catálogo.
*
* @return array
*/
protected function bootstraptableConfig(): array
{
return array_merge(parent::bootstraptableConfig(), [
'sortName' => 'key',
'exportFileName' => 'Configuración',
'showFullscreen' => false,
'showPaginationSwitch' => false,
'showRefresh' => false,
'pagination' => false,
]);
}
/**
* Retorna la vista a renderizar para este componente.
*
* @return string
*/
protected function viewPath(): string
{
return 'vuexy-admin::livewire.global-settings.index';
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
class QuickAccessWidget extends Component
{
public $quickAccessItems = [];
public function mount()
{
$menuConfig = config('vuexy_menu');
$this->quickAccessItems = $this->processMenu($menuConfig);
}
private function processMenu(array $menu): array
{
$user = Auth::user();
$accessItems = [];
foreach ($menu as $section => $items) {
if (!isset($items['submenu']) || !is_array($items['submenu'])) {
continue;
}
$categoryData = [
'title' => $section,
'icon' => $items['icon'] ?? 'ti ti-folder',
'description' => $items['description'] ?? '',
'submenu' => []
];
$this->processSubmenu($items['submenu'], $categoryData['submenu'], $user);
if (!empty($categoryData['submenu'])) {
$accessItems[] = $categoryData;
}
}
return $accessItems;
}
private function processSubmenu(array $submenu, array &$categorySubmenu, $user)
{
foreach ($submenu as $title => $item) {
// Si el elemento NO tiene 'route' ni 'url' y SOLO contiene un submenu, no lo mostramos como acceso directo
if (!isset($item['route']) && !isset($item['url']) && isset($item['submenu'])) {
// Procesamos los submenús de este elemento sin agregarlo directamente a la lista
$this->processSubmenu($item['submenu'], $categorySubmenu, $user);
continue;
}
// Validar si el usuario tiene permiso
$can = $item['can'] ?? null;
if (!$can || $user->can($can)) {
// Si tiene ruta y existe en Laravel, usarla; si no, usar url, y si tampoco hay, usar 'javascript:;'
$routeExists = isset($item['route']) && Route::has($item['route']);
$url = $routeExists ? route($item['route']) : ($item['url'] ?? 'javascript:;');
// Agregar elemento al submenu si tiene un destino válido
$categorySubmenu[] = [
'title' => $title,
'icon' => $item['icon'] ?? 'ti ti-circle',
'url' => $url,
];
}
// Si el elemento tiene un submenu, también lo procesamos
if (isset($item['submenu']) && is_array($item['submenu'])) {
$this->processSubmenu($item['submenu'], $categorySubmenu, $user);
}
}
}
public function render()
{
return view('vuexy-admin::livewire.vuexy.quick-access-widget', [
'quickAccessItems' => $this->quickAccessItems,
]);
}
}

View File

@ -1,121 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Livewire\VuexyAdmin;
use Livewire\Component;
use Koneko\VuexyAdmin\Services\AdminTemplateService;
use Koneko\VuexyAdmin\Services\GlobalSettingsService;
use Koneko\VuexyAdmin\Services\SettingsService;
class VuexyInterfaceSettings extends Component
{
private $targetNotify = "#interface-settings-card .notification-container";
public $uniqueId;
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->uniqueId = uniqid();
$this->resetForm();
}
public function save()
{
$this->validate([
'vuexy_maxQuickLinks' => 'required|integer|min:2|max:20',
]);
// Guardar configuraciones utilizando SettingsService
$SettingsService = app(SettingsService::class);
$SettingsService->set('config.vuexy.custom.myLayout', $this->vuexy_myLayout, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.myTheme', $this->vuexy_myTheme, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.myStyle', $this->vuexy_myStyle, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.hasCustomizer', $this->vuexy_hasCustomizer, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.displayCustomizer', ($this->vuexy_hasCustomizer? $this->vuexy_displayCustomizer: false), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.contentLayout', $this->vuexy_contentLayout, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.navbarType', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_navbarType: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.footerFixed', $this->vuexy_footerFixed, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.menuFixed', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_menuFixed: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.menuCollapsed', ($this->vuexy_myLayout == 'vertical' ? $this->vuexy_menuCollapsed: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.headerType', ($this->vuexy_myLayout == 'horizontal' ? $this->vuexy_headerType: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.showDropdownOnHover', ($this->vuexy_myLayout == 'horizontal' ? $this->vuexy_showDropdownOnHover: null), null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.authViewMode', $this->vuexy_authViewMode, null, 'vuexy-admin');
$SettingsService->set('config.vuexy.custom.maxQuickLinks', $this->vuexy_maxQuickLinks, null, 'vuexy-admin');
// Elimina la Cache de Configuraciones
app(GlobalSettingsService::class)->clearSystemConfigCache();
// Refrescar el componente actual
$this->dispatch('clearLocalStoregeTemplateCustomizer');
// Notificación de éxito
$this->dispatch(
'notification',
target: $this->targetNotify,
type: 'success',
message: 'Se han guardado los cambios en las configuraciones.',
deferReload: true
);
}
public function clearCustomConfig()
{
// Elimina las claves config.vuexy.* para cargar los valores por defecto
app(GlobalSettingsService::class)->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 resetForm()
{
// Obtener los valores de las configuraciones de la base de datos
$settings = app(AdminTemplateService::class)->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 render()
{
return view('vuexy-admin::livewire.vuexy.interface-settings');
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Setting extends Model
{
use HasFactory;
// ─────────────────────────────────────────────
// Configuración del modelo
// ─────────────────────────────────────────────
protected $table = 'settings';
protected $fillable = [
'key',
'category',
'user_id',
'value_string',
'value_integer',
'value_boolean',
'value_float',
'value_text',
'value_binary',
'mime_type',
'file_name',
'updated_by',
];
protected $casts = [
'user_id' => 'integer',
'value_integer' => 'integer',
'value_boolean' => 'boolean',
'value_float' => 'float',
'updated_by' => 'integer',
];
// ─────────────────────────────────────────────
// Metadatos personalizados para el generador de componentes
// ─────────────────────────────────────────────
public string $tagName = 'setting';
public string $columnNameLabel = 'key';
public string $singularName = 'Configuración';
public string $pluralName = 'Configuraciones';
// ─────────────────────────────────────────────
// Relaciones
// ─────────────────────────────────────────────
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function updatedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
// ─────────────────────────────────────────────
// Scopes
// ─────────────────────────────────────────────
/**
* Configuraciones para un usuario específico.
*/
public function scopeForUser($query, int $userId)
{
return $query->where('user_id', $userId);
}
/**
* Configuraciones globales (sin usuario).
*/
public function scopeGlobal($query)
{
return $query->whereNull('user_id');
}
/**
* Incluir columna virtual `value` en la consulta.
*/
public function scopeWithVirtualValue($query)
{
return $query->select(['key', 'value']);
}
}

View File

@ -1,225 +0,0 @@
<?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 INITIAL_MAX_LENGTH = 3;
/**
* 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 URL for the user's profile photo.
*
* @return string
*/
public function getProfilePhotoUrlAttribute()
{
if ($this->profile_photo_path) {
return asset('storage/profile-photos/' . $this->profile_photo_path);
}
return $this->defaultProfilePhotoUrl();
}
/**
* Get the default profile photo URL if no profile photo has been uploaded.
*
* @return string
*/
protected function defaultProfilePhotoUrl()
{
return route('admin.core.user-profile.avatar', ['name' => $this->fullname]);
}
/**
* 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));
}
/**
* 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));
}
/**
* 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;
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Model;
class UserLogin extends Model
{
protected $fillable = [
'user_id',
'ip_address',
'user_agent'
];
}

View File

@ -1,27 +0,0 @@
<?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 a través del servicio
app(GlobalSettingsService::class)->loadSystemConfig();
}
}

View File

@ -1,184 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Providers;
use Koneko\VuexyAdmin\Console\Commands\CleanInitialAvatars;
use Koneko\VuexyAdmin\Helpers\VuexyHelper;
use Koneko\VuexyAdmin\Http\Middleware\AdminTemplateMiddleware;
use Illuminate\Auth\Events\{Login,Logout};
use Illuminate\Foundation\AliasLoader;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\{URL,Event,Blade};
use Illuminate\Support\ServiceProvider;
use Koneko\VuexyAdmin\Listeners\{ClearUserCache,HandleUserLogin};
use Koneko\VuexyAdmin\Livewire\Cache\{CacheFunctions,CacheStats,SessionStats,MemcachedStats,RedisStats};
use Koneko\VuexyAdmin\Livewire\Permissions\{PermissionsIndex,PermissionOffCanvasForm};
use Koneko\VuexyAdmin\Livewire\Profile\{UpdateProfileInformationForm,UpdatePasswordForm,TwoFactorAuthenticationForm,LogoutOtherBrowser,DeleteUserForm};
use Koneko\VuexyAdmin\Livewire\Roles\{RolesIndex,RoleCards};
use Koneko\VuexyAdmin\Livewire\Users\{UsersIndex,UsersCount,UserForm,UserOffCanvasForm};
use Koneko\VuexyAdmin\Livewire\VuexyAdmin\{LogoOnLightBgSettings,LogoOnDarkBgSettings,AppDescriptionSettings,AppFaviconSettings};
use Koneko\VuexyAdmin\Livewire\VuexyAdmin\SendmailSettings;
use Koneko\VuexyAdmin\Livewire\VuexyAdmin\{VuexyInterfaceSettings};
use Koneko\VuexyAdmin\Livewire\VuexyAdmin\{GlobalSettingsIndex,GlobalSettingOffCanvasForm};
use Koneko\VuexyAdmin\Livewire\VuexyAdmin\QuickAccessWidget;
use Koneko\VuexyAdmin\Models\User;
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('TRUST_PROXY', false)) {
Request::setTrustedProxies(
explode(',', env('TRUST_PROXY_IPS', '*')), // admite múltiples IPs separadas por coma
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_PREFIX
);
}
if (env('FORCE_HTTPS', false) || request()->header('X-Forwarded-Proto') === 'https') {
URL::forceScheme('https');
app('request')->server->set('HTTPS', 'on');
}
// 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');
// Register the migrations
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
// 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');
// 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 = [
// Usuarios
'vuexy-admin::users-index' => UsersIndex::class,
'vuexy-admin::users-count' => UsersCount::class,
'vuexy-admin::user-form' => UserForm::class,
'vuexy-admin::user-offcanvas-form' => UserOffCanvasForm::class,
// Perfil del usuario
'vuexy-admin::update-profile-information-form' => UpdateProfileInformationForm::class,
'vuexy-admin::update-password-form' => UpdatePasswordForm::class,
'vuexy-admin::two-factor-authentication' => TwoFactorAuthenticationForm::class,
'vuexy-admin::logout-other-browser' => LogoutOtherBrowser::class,
'vuexy-admin::delete-user-form' => DeleteUserForm::class,
// Roles y Permisos
'vuexy-admin::roles-index' => RolesIndex::class,
'vuexy-admin::role-cards' => RoleCards::class,
'vuexy-admin::permissions-index' => PermissionsIndex::class,
'vuexy-admin::permission-offcanvas-form' => PermissionOffCanvasForm::class,
// Identidad de aplicación
'vuexy-admin::app-description-settings' => AppDescriptionSettings::class,
'vuexy-admin::app-favicon-settings' => AppFaviconSettings::class,
'vuexy-admin::logo-on-light-bg-settings' => LogoOnLightBgSettings::class,
'vuexy-admin::logo-on-dark-bg-settings' => LogoOnDarkBgSettings::class,
// Ajustes de interfaz
'vuexy-admin::interface-settings' => VuexyInterfaceSettings::class,
// Cache
'vuexy-admin::cache-stats' => CacheStats::class,
'vuexy-admin::session-stats' => SessionStats::class,
'vuexy-admin::redis-stats' => RedisStats::class,
'vuexy-admin::memcached-stats' => MemcachedStats::class,
'vuexy-admin::cache-functions' => CacheFunctions::class,
// Configuración de correo saliente
'vuexy-admin::sendmail-settings' => SendmailSettings::class,
// Configuraciones globales
'vuexy-admin::global-settings-index' => GlobalSettingsIndex::class,
'vuexy-admin::global-setting-offcanvas-form' => GlobalSettingOffCanvasForm::class,
// Accesos rápidos de la barra de menú
'vuexy-admin::quick-access-widget' => QuickAccessWidget::class,
];
foreach ($components as $alias => $component) {
Livewire::component($alias, $component);
}
// Registrar auditoría en usuarios
User::observe(AuditableObserver::class);
}
}

View File

@ -1,109 +0,0 @@
<?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';
// Soporte para alias
$table = $join['table'];
$alias = $join['alias'] ?? null;
$tableWithAlias = $alias ? DB::raw("{$table} as {$alias}") : $table;
$this->query->{$type}($tableWithAlias, function ($joinObj) use ($join, $alias) {
$first = $join['first'];
$second = $join['second'];
$joinObj->on($first, '=', $second);
// Soporte para condiciones adicionales tipo AND
if (!empty($join['and'])) {
foreach ((array) $join['and'] as $andCondition) {
$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

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

View File

@ -9,7 +9,7 @@
<a href="https://packagist.org/packages/koneko/laravel-vuexy-admin"><img src="https://img.shields.io/packagist/l/koneko/laravel-vuexy-admin" alt="License"></a>
<a href="https://git.koneko.mx/koneko"><img src="https://img.shields.io/badge/Git%20Server-Koneko%20Git-orange" alt="Servidor Git"></a>
<a href="https://github.com/koneko-mx/laravel-vuexy-admin/actions/workflows/tests.yml"><img src="https://github.com/koneko-mx/laravel-vuexy-admin/actions/workflows/tests.yml/badge.svg" alt="Build Status"></a>
<a href="https://github.com/koneko-mx/laravel-vuexy-admin/issues"><img src="https://img.shields.io/github/issues/koneko/laravel-vuexy-admin" alt="Issues"></a>
<a href="https://github.com/koneko-mx/laravel-vuexy-admin/issues"><img src="https://img.shields.io/github/issues/koneko-mx/laravel-vuexy-admin" alt="Issues"></a>
</p>
---
@ -30,19 +30,37 @@
## 📦 Instalación
Instalar vía **Composer**:
### 🔹 Opción 1: Desde Packagist (Recomendado)
Instala el paquete desde [Packagist](https://packagist.org/packages/koneko/laravel-vuexy-admin):
```bash
composer require koneko/laravel-vuexy-admin
```
Publicar archivos de configuración y migraciones:
> Asegúrate de tener habilitado Packagist en tu `composer.json`.
### 🔹 Opción 2: Desde repositorio Git (GitHub o Tea)
También puedes instalarlo como repositorio privado en desarrollo:
```json
"repositories": {
"koneko/laravel-vuexy-admin": {
"type": "vcs",
"url": "https://github.com/koneko-mx/laravel-vuexy-admin"
}
}
```
Luego ejecuta:
```bash
php artisan vendor:publish --tag=vuexy-admin-config
php artisan migrate
composer require koneko/laravel-vuexy-admin:@dev
```
> Puedes cambiar la URL por `https://git.koneko.mx/koneko/laravel-vuexy-admin` si usas Tea como servidor Git.
---
## 🚀 Uso básico
@ -124,7 +142,6 @@ Este paquete es de código abierto bajo la licencia [MIT](LICENSE).
---
<p align="center">
Hecho con ❤️ por <a href="https://koneko.mx">Koneko Soluciones Tecnológicas</a>
</p>

View File

@ -1,289 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Services;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Koneko\VuexyAdmin\Models\Setting;
/**
* Servicio para gestionar la configuración administrativa de VuexyAdmin
*
* Este servicio maneja el procesamiento y almacenamiento de imágenes del favicon
* y logos del panel administrativo, incluyendo diferentes versiones y tamaños.
*
* @package Koneko\VuexyAdmin\Services
*/
class AdminSettingsService
{
/** @var string Driver de procesamiento de imágenes */
private $driver;
/** @var string Disco de almacenamiento para imágenes */
private $imageDisk = 'public';
/** @var string Ruta base para favicons */
private $favicon_basePath = 'favicon/';
/** @var string Ruta base para logos */
private $image_logo_basePath = 'images/logo/';
/** @var array<string,array<int>> Tamaños predefinidos para favicons */
private $faviconsSizes = [
'180x180' => [180, 180],
'192x192' => [192, 192],
'152x152' => [152, 152],
'120x120' => [120, 120],
'76x76' => [76, 76],
'16x16' => [16, 16],
];
/** @var int Área máxima en píxeles para la primera versión del logo */
private $imageLogoMaxPixels1 = 22500;
/** @var int Área máxima en píxeles para la segunda versión del logo */
private $imageLogoMaxPixels2 = 75625;
/** @var int Área máxima en píxeles para la tercera versión del logo */
private $imageLogoMaxPixels3 = 262144;
/** @var int Área máxima en píxeles para la versión base64 del logo */
private $imageLogoMaxPixels4 = 230400;
/** @var int Tiempo de vida en caché en minutos */
protected $cacheTTL = 60 * 24 * 30;
/**
* Constructor del servicio
*
* Inicializa el driver de procesamiento de imágenes desde la configuración
*/
public function __construct()
{
$this->driver = config('image.driver', 'gd');
}
/**
* Procesa y guarda un nuevo favicon
*
* Genera múltiples versiones del favicon en diferentes tamaños predefinidos,
* elimina las versiones anteriores y actualiza la configuración.
*
* @param \Illuminate\Http\UploadedFile $image Archivo de imagen subido
* @return void
*/
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));
}
// Actualizar configuración utilizando SettingService
$SettingsService = app(SettingsService::class);
$SettingsService->set('admin.favicon_ns', $this->favicon_basePath . $imageName, null, 'vuexy-admin');
}
/**
* Elimina los favicons antiguos del almacenamiento
*
* @return void
*/
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);
}
}
}
}
/**
* Procesa y guarda un nuevo logo
*
* Genera múltiples versiones del logo con diferentes tamaños máximos,
* incluyendo una versión en base64, y actualiza la configuración.
*
* @param \Illuminate\Http\UploadedFile $image Archivo de imagen subido
* @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal)
* @return void
*/
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
}
/**
* Genera y guarda una versión del logo
*
* @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a procesar
* @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal)
* @param int $maxPixels Área máxima en píxeles
* @param string $suffix Sufijo para el nombre del archivo
* @return void
*/
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' : '');
$keyValue = '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
$SettingsService = app(SettingsService::class);
$SettingsService->set($keyValue, $resizedPath, null, 'vuexy-admin');
}
/**
* Redimensiona una imagen manteniendo su proporción
*
* @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a redimensionar
* @param int $maxPixels Área máxima en píxeles
* @return \Intervention\Image\Interfaces\ImageInterface
*/
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;
}
/**
* Genera y guarda una versión del logo en formato base64
*
* @param \Intervention\Image\Interfaces\ImageInterface $image Imagen a procesar
* @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal)
* @param int $maxPixels Área máxima en píxeles
* @return void
*/
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
$SettingsService = app(SettingsService::class);
$SettingsService->set("admin.image.logo_base64" . ($type === 'dark' ? '_dark' : ''), $base64Image, null, 'vuexy-admin');
}
/**
* Elimina las imágenes antiguas del logo
*
* @param string $type Tipo de logo ('dark' para modo oscuro, '' para normal)
* @return void
*/
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

@ -1,195 +0,0 @@
<?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;
/**
* Servicio para gestionar la configuración y personalización del template administrativo.
*
* Esta clase maneja las configuraciones del template VuexyAdmin, incluyendo variables
* de personalización, logos, favicons y otras configuraciones de la interfaz.
* Implementa un sistema de caché para optimizar el rendimiento.
*/
class AdminTemplateService
{
/** @var int Tiempo de vida del caché en minutos (60 * 24 * 30 = 30 días) */
protected $cacheTTL = 60 * 24 * 30;
/**
* Obtiene las variables de configuración del admin.
*
* @param string $setting Clave específica de configuración a obtener
* @return array Configuraciones del admin o valor específico si se proporciona $setting
*/
public function getAdminVars(string $setting = ''): array
{
try {
// Verificar si el sistema está inicializado (la tabla `migrations` existe)
if (!Schema::hasTable('migrations')) {
return $this->getDefaultAdminVars($setting);
}
// Cargar desde el caché o la base de datos si está disponible
$adminVars = Cache::remember('admin_settings', $this->cacheTTL, function () {
$settings = Setting::withVirtualValue()
->where('key', 'LIKE', 'admin.%')
->pluck('value', 'key')
->toArray();
return $this->buildAdminVarsArray($settings);
});
return $setting ? ($adminVars[$setting] ?? []) : $adminVars;
} catch (\Exception $e) {
// En caso de error, devolver valores predeterminados
return $this->getDefaultAdminVars($setting);
}
}
/**
* Obtiene las variables predeterminadas del admin.
*
* @param string $setting Clave específica de configuración a obtener
* @return array Configuraciones predeterminadas o valor específico si se proporciona $setting
*/
private function getDefaultAdminVars(string $setting = ''): 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 $setting
? $defaultSettings[$setting] ?? null
: $defaultSettings;
}
/**
* Construye el array de variables del admin a partir de las configuraciones.
*
* @param array $settings Array asociativo de configuraciones
* @return array Array estructurado con las variables del admin
*/
private function buildAdminVarsArray(array $settings): array
{
return [
'title' => $settings['admin.title'] ?? config('koneko.appTitle'),
'author' => config('koneko.author'),
'description' => $settings['admin.description'] ?? config('koneko.description'),
'favicon' => $this->getFaviconPaths($settings),
'app_name' => $settings['admin.app_name'] ?? config('koneko.appName'),
'image_logo' => $this->getImageLogoPaths($settings),
];
}
/**
* Obtiene las variables de personalización de Vuexy.
*
* Combina las configuraciones predeterminadas con las almacenadas en la base de datos,
* aplicando las transformaciones necesarias para tipos específicos como booleanos.
*
* @return array Array asociativo con las variables de personalización
*/
public function getVuexyCustomizerVars()
{
// Obtener valores de la base de datos
$settings = Setting::withVirtualValue()
->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, ['hasCustomizer', 'displayCustomizer', 'footerFixed', 'menuFixed', 'menuCollapsed', 'showDropdownOnHover'])) {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
return [$key => $value];
})
->toArray();
}
/**
* Genera las rutas para los diferentes tamaños de favicon.
*
* @param array $settings Array asociativo de configuraciones
* @return array Array con las rutas de los favicons en diferentes 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,
];
}
/**
* Genera las rutas para los diferentes tamaños y versiones del logo.
*
* @param array $settings Array asociativo de configuraciones
* @return array Array con las rutas de los logos en diferentes tamaños y modos
*/
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 la ruta de una imagen específica desde las configuraciones.
*
* @param array $settings Array asociativo de configuraciones
* @param string $key Clave de la configuración
* @param string $default Valor predeterminado si no se encuentra la configuración
* @return string Ruta de la imagen
*/
private function getImagePath(array $settings, string $key, string $default): string
{
return $settings[$key] ?? $default;
}
/**
* Limpia el caché de las variables del admin.
*
* @return void
*/
public static function clearAdminVarsCache()
{
Cache::forget("admin_settings");
}
}

View File

@ -1,76 +0,0 @@
<?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

@ -1,124 +0,0 @@
<?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 = [
'#3b82f6',
'#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

@ -1,255 +0,0 @@
<?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;
/**
* Servicio para gestionar la configuración global del sistema.
*
* Esta clase maneja las configuraciones globales del sistema, incluyendo servicios
* externos (Facebook, Google), configuración de Vuexy y sistema de correo.
* Implementa un sistema de caché para optimizar el rendimiento y proporciona
* valores predeterminados cuando es necesario.
*/
class GlobalSettingsService
{
/** @var int Tiempo de vida del caché en minutos (60 * 24 * 30 = 30 días) */
private $cacheTTL = 60 * 24 * 30;
/**
* Carga la configuración del sistema desde la base de datos o caché.
*
* Gestiona la carga de configuraciones para servicios externos y Vuexy.
* Si la base de datos no está inicializada, utiliza valores predeterminados.
*
* @return void
*/
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::withVirtualValue()
->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', []));
}
}
/**
* Obtiene la configuración predeterminada del sistema.
*
* @return array Configuración predeterminada para servicios y Vuexy
*/
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 existe configuración para un bloque específico.
*
* @param array $settings Array de configuraciones
* @param string $blockPrefix Prefijo del bloque a verificar
* @return bool True si existe configuración para el bloque
*/
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 para un servicio específico.
*
* @param array $settings Array de configuraciones
* @param string $blockPrefix Prefijo del bloque de configuración
* @param string $defaultConfigKey Clave de configuración predeterminada
* @return array Configuración del servicio
*/
protected function buildServiceConfig(array $settings, string $blockPrefix, string $defaultConfigKey): array
{
if (!$this->hasBlockConfig($settings, $blockPrefix)) {
return config($defaultConfigKey)?? [];
}
return [
'client_id' => $settings["{$blockPrefix}client_id"] ?? '',
'client_secret' => $settings["{$blockPrefix}client_secret"] ?? '',
'redirect' => $settings["{$blockPrefix}redirect"] ?? '',
];
}
/**
* Construye la configuración de Vuexy.
*
* Combina la configuración predeterminada con los valores almacenados
* en la base de datos y normaliza los campos booleanos.
*
* @param array $settings Array de configuraciones
* @return array Configuración de Vuexy normalizada
*/
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 en la configuración.
*
* @param array $config Configuración a normalizar
* @return array Configuración con campos booleanos normalizados
*/
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 la caché de configuración del sistema.
*
* @return void
*/
public static function clearSystemConfigCache(): void
{
Cache::forget('global_system_config');
}
/**
* Limpia la configuración de Vuexy de la base de datos y caché.
*
* @return void
*/
public static function clearVuexyConfig(): void
{
Setting::where('key', 'LIKE', 'config.vuexy.%')->delete();
Cache::forget('global_system_config');
}
/**
* Obtiene la configuración del sistema de correo.
*
* Recupera y estructura la configuración de correo incluyendo
* configuración SMTP, direcciones de envío y respuesta.
*
* @return array Configuración completa del sistema de correo
*/
public function getMailSystemConfig(): array
{
return Cache::remember('mail_system_config', $this->cacheTTL, function () {
$settings = Setting::withVirtualValue()
->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 la caché de configuración del sistema de correo.
*
* @return void
*/
public static function clearMailSystemConfigCache(): void
{
Cache::forget('mail_system_config');
}
}

View File

@ -1,28 +0,0 @@
<?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

@ -1,190 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Services;
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Models\Setting;
use Illuminate\Support\Facades\Cache;
class SettingsService
{
/**
* Obtiene una configuración con opciones avanzadas de caché.
*
* @param string $key
* @param int|null $userId
* @param string|null $category
* @param bool $useCache
* @param bool $storeInCache
* @param int|null $cacheTtl
* @return mixed|null
*/
public function get(
string $key,
?int $userId = null,
?string $category = null,
bool $useCache = false,
bool $storeInCache = true,
?int $cacheTtl = 120
) {
$cacheKey = $this->generateCacheKey($key, $userId, $category);
if ($useCache && Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
$value = $this->retrieveSetting($key, $userId, $category);
if ($storeInCache && $value !== null) {
Cache::put($cacheKey, $value, now()->addMinutes($cacheTtl));
}
return $value;
}
/**
* Guarda o actualiza una configuración con control de caché.
*
* @param string $key
* @param mixed $value
* @param int|null $userId
* @param string|null $category
* @param string|null $mimeType
* @param string|null $fileName
* @param bool $updateCache
* @param int|null $cacheTtl
* @return Setting|null
*/
public function set(
string $key,
mixed $value,
?int $userId = null,
?string $category = null,
?string $mimeType = null,
?string $fileName = null,
bool $updateCache = false,
?int $cacheTtl = 120
): ?Setting {
$data = [
'user_id' => $userId,
'category' => $category,
'mime_type' => $mimeType,
'file_name' => $fileName,
// Inicializar todos los campos de valor como null
'value_string' => null,
'value_integer' => null,
'value_boolean' => null,
'value_float' => null,
'value_text' => null,
'value_binary' => null,
];
// Detectar tipo de valor
if (is_string($value)) {
// Evaluamos la longitud de la cadena
$threshold = 250;
if (strlen($value) > $threshold) {
$data['value_text'] = $value;
} else {
$data['value_string'] = $value;
}
} elseif (is_int($value)) {
$data['value_integer'] = $value;
} elseif (is_bool($value)) {
$data['value_boolean'] = $value;
} elseif (is_float($value)) {
$data['value_float'] = $value;
} elseif (is_resource($value) || $value instanceof \SplFileInfo) {
$data['value_binary'] = is_resource($value)
? stream_get_contents($value)
: file_get_contents($value->getRealPath());
} elseif (is_array($value) || is_object($value)) {
$data['value_text'] = json_encode($value);
}
// Se registra usuario que realiza la acción
if (Auth::check()) {
$data['updated_by'] = Auth::id();
}
$setting = Setting::updateOrCreate(
['key' => $key, 'user_id' => $userId, 'category' => $category],
$data
);
if ($updateCache) {
$cacheKey = $this->generateCacheKey($key, $userId, $category);
Cache::put($cacheKey, $setting->value, now()->addMinutes($cacheTtl));
}
return $setting;
}
/**
* Elimina una configuración.
*
* @param string $key
* @param int|null $userId
* @param string|null $category
* @return Setting|null La configuración eliminada o null si no existía
*/
public function delete(string $key, ?int $userId = null, ?string $category = null): ?Setting
{
$setting = Setting::where('key', $key);
if ($userId !== null) {
$setting->where('user_id', $userId);
}
if ($category !== null) {
$setting->where('category', $category);
}
$setting = $setting->first();
if ($setting) {
$cacheKey = $this->generateCacheKey($key, $userId, $category);
Cache::forget($cacheKey);
$setting->delete();
}
return $setting;
}
/**
* Recupera una configuración de la base de datos.
*
* @param string $key
* @param integer|null $userId
* @param string|null $category
* @return void
*/
protected function retrieveSetting(string $key, ?int $userId, ?string $category)
{
$query = Setting::where('key', $key);
if ($userId !== null) {
$query->where('user_id', $userId);
}
if ($category !== null) {
$query->where('category', $category);
}
return $query->first()?->value;
}
/**
* Genera una clave de caché para una configuración.
*
* @param string $key
* @param integer|null $userId
* @param string|null $category
* @return string
*/
protected function generateCacheKey(string $key, ?int $userId, ?string $category): string
{
return 'settings:' . md5($key . '|' . $userId . '|' . $category);
}
}

View File

@ -1,416 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Services;
use Illuminate\Support\Facades\{Auth,Cache,Route,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;
}
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);
}
private function getGuestMenu()
{
return Cache::remember('vuexy_menu_guest', now()->addDays(7), function () {
return $this->getMenuArray();
});
}
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;
}
public static function clearUserMenuCache()
{
$user = Auth::user();
if ($user !== null)
Cache::forget("vuexy_menu_user_{$user->id}");
}
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, mixed $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
);
}
}
}
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>
</a>
</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,4 +1,5 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "koneko/laravel-vuexy-admin",
"description": "Laravel Vuexy Admin, un modulo de administracion optimizado para México.",
"keywords": ["laravel", "koneko", "framework", "vuexy", "admin", "mexico"],
@ -6,20 +7,36 @@
"license": "MIT",
"require": {
"php": "^8.2",
"intervention/image-laravel": "^1.4",
"geoip2/geoip2": "^3.1",
"intervention/image-laravel": "^1.5",
"jenssegers/agent": "^2.6",
"laravel/framework": "^11.31",
"laravel/fortify": "^1.25",
"laravel/sanctum": "^4.0",
"league/csv": "^9.23.0",
"livewire/livewire": "^3.5",
"owen-it/laravel-auditing": "^13.6",
"spatie/laravel-permission": "^6.10"
},
"prefer-stable": true,
"require-dev": {
"orchestra/testbench": "^9.12"
},
"autoload": {
"psr-4": {
"Koneko\\VuexyAdmin\\": "./"
"Koneko\\VuexyAdmin\\": "src/"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Koneko\\VuexyAdmin\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit"
},
"extra": {
"laravel": {
"providers": [
@ -36,5 +53,6 @@
"support": {
"source": "https://github.com/koneko-mx/laravel-vuexy-admin",
"issues": "https://github.com/koneko-mx/laravel-vuexy-admin/issues"
}
},
"prefer-stable": true
}

20
config/keyvault_db.php Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
return [
'connections' => [
'keyvault' => [
'driver' => 'mysql',
'host' => env('KEYVAULT_DB_HOST', '127.0.0.1'),
'database' => env('KEYVAULT_DB_DATABASE', 'key_vault'),
'username' => env('KEYVAULT_DB_USERNAME', 'vault_user'),
'password' => env('KEYVAULT_DB_PASSWORD', 'secret'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => env('KEYVAULT_DB_PREFIX', ''),
'strict' => true,
'engine' => null,
],
],
];

View File

@ -1,14 +1,10 @@
<?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",
"title" => "Koneko Soluciones Tecnológicas",
"description" => "Koneko Soluciones Tecnológicas ofrece desarrollo de sistemas empresariales, sitios web profesionales, inteligencia artificial, infraestructura y soluciones digitales avanzadas para negocios en México.",
"author" => "arturo@koneko.mx",
"app_name" => "koneko.mx",
"app_logo" => "../vendor/vuexy-admin/img/logo/koneko-04.png",
"favicon" => "../vendor/vuexy-admin/img/logo/koneko-04.png",
];

107
config/koneko_admin.php Normal file
View File

@ -0,0 +1,107 @@
<?php
return [
// Personalización de interfaz
'vuexy' => [
'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]: 8(default), 6, 8, 10
'customizerControls' => [
'style',
'headerType',
'contentLayout',
'layoutCollapsed',
'layoutNavbarOptions',
'themes',
], // To show/hide customizer options
],
// HTTPS y proxies
'security' => [
'force_https' => (bool) env('FORCE_HTTPS', false),
'trust_proxy' => (bool) env('TRUST_PROXY', false),
'trust_proxy_ips' => env('TRUST_PROXY_IPS', '*'),
// Key Vault & Encryption Management
'key_vault' => [
'driver' => env('VUEXY_KEY_VAULT_DRIVER', 'laravel'), // Options: laravel, sqlite, mysql, mariadb, go_service
'enabled' => (bool) env('VUEXY_KEY_VAULT_ENABLED', true),
// Laravel Default Encryption (APP_KEY)
'laravel' => [
'key' => env('APP_KEY'),
'algorithm' => 'AES-256-CBC',
],
// Second DB Configuration (Requires separate connection)
'database' => [
'connection' => env('VUEXY_KEY_VAULT_DB_CONNECTION', 'vault'),
'table' => env('VUEXY_KEY_VAULT_DB_TABLE', 'vault_keys'),
'algorithm' => env('VUEXY_KEY_VAULT_DB_ALGORITHM', 'AES-256-CBC'),
],
// External Go Microservice
'service' => [
'base_url' => env('VUEXY_KEY_VAULT_SERVICE_URL'),
'api_token' => env('VUEXY_KEY_VAULT_SERVICE_TOKEN'),
'timeout' => (int) env('VUEXY_KEY_VAULT_SERVICE_TIMEOUT', 5),
],
],
],
// Cache
'cache' => [
'enabled' => (bool) env('VUEXY_CACHE_ENABLED', true),
'ttl' => (int) env('VUEXY_CACHE_TTL', 20 * 24 * 60),
],
// Avatar
'avatar' => [
'initials' => [
'max_length' => (int) env('VUEXY_AVATAR_INITIALS_MAX_LENGTH', 2),
'disk' => env('VUEXY_AVATAR_INITIALS_DISK', 'public'),
'directory' => env('VUEXY_AVATAR_INITIALS_DIRECTORY', 'initial-avatars'),
'size' => (int) env('VUEXY_AVATAR_INITIALS_SIZE', 512),
'background' => env('VUEXY_AVATAR_INITIALS_BACKGROUND', '#EBF4FF'),
'colors' => json_decode(env('VUEXY_AVATAR_INITIALS_COLORS', json_encode(['#3b82f6', '#808390', '#28c76f', '#ff4c51', '#ff9f43', '#00bad1', '#4b4b4b'])), true),
'font_size_ratio' => (float) env('VUEXY_AVATAR_INITIALS_FONT_SIZE_RATIO', 0.4),
'fallback_text' => env('VUEXY_AVATAR_INITIALS_FALLBACK_TEXT', 'NA'),
'cache' => [
'ttl' => (int) env('VUEXY_AVATAR_INITIALS_CACHE_TTL', 30 * 24 * 60),
],
],
'image' => [
'disk' => env('VUEXY_AVATAR_IMAGE_DISK', 'public'),
'directory' => env('VUEXY_AVATAR_IMAGE_DIRECTORY', 'profile-photos'),
'width' => (int) env('VUEXY_AVATAR_IMAGE_WIDTH', 512),
'height' => (int) env('VUEXY_AVATAR_IMAGE_HEIGHT', 512),
'fit_method' => env('VUEXY_AVATAR_IMAGE_FIT_METHOD', 'cover'),
],
],
// Menú
'menu' => [
'cache' => [
'enabled' => (bool) env('VUEXY_MENU_CACHE_ENABLED', true),
'ttl' => (int) env('VUEXY_MENU_CACHE_TTL', 2 * 24 * 60),
],
'debug' => [
'show_broken_routers' => (bool) env('VUEXY_MENU_DEBUG_SHOW_BROKEN_ROUTES', false),
'show_disallowed_links' => (bool) env('VUEXY_MENU_DEBUG_SHOW_DISALLOWED_LINKS', false),
'show_hidden_items' => (bool) env('VUEXY_MENU_DEBUG_SHOW_HIDDEN_ITEMS', false),
],
],
];

12
config/logging.php Normal file
View File

@ -0,0 +1,12 @@
<?php
return [
'channels' => [
'vuexy' => [
'driver' => 'daily',
'path' => storage_path('logs/vuexy.log'),
'level' => env('VUEXY_LOG_LEVEL', 'debug'),
'days' => 14,
],
],
];

View File

@ -1,37 +0,0 @@
<?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
],
'force_https' => env('FORCE_HTTPS', false),
];

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,4 @@
name,email,password,roles,avatar_path
Koneko Admin,sadmin@koneko.mx,LAdmin123,"[""SuperAdmin""]",vendor/vuexy-admin/img/logo/koneko-02.png
Administrador,admin@koneko.mx,LAdmin123,"[""Admin""]",vendor/vuexy-admin/img/logo/koneko-03.png
Auditor,auditor@koneko.mx,LAdmin123,"[""Auditor""]",vendor/vuexy-admin/img/logo/koneko-03.png
1 name email password roles avatar_path
2 Koneko Admin sadmin@koneko.mx LAdmin123 ["SuperAdmin"] vendor/vuexy-admin/img/logo/koneko-02.png
3 Administrador admin@koneko.mx LAdmin123 ["Admin"] vendor/vuexy-admin/img/logo/koneko-03.png
4 Auditor auditor@koneko.mx LAdmin123 ["Auditor"] vendor/vuexy-admin/img/logo/koneko-03.png

View File

@ -1,510 +0,0 @@
{
"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.products.products.view",
"admin.products.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.purchase-orders.orders.view",
"admin.purchase-orders.reception.view",
"admin.purchase-orders.materials.view",
"admin.inventory.warehouse.view",
"admin.inventory.stock.view",
"admin.inventory.movements.view",
"admin.inventory.transfers.view",
"admin.shipping.orders.view",
"admin.shipping.tracking.view",
"admin.shipping.carriers.view",
"admin.shipping.rates.view",
"admin.assets.assets.view",
"admin.assets.maintenance.view",
"admin.assets.lifecycle.view",
"admin.assets.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.products.products.view",
"admin.products.products.create",
"admin.contacts.suppliers.view",
"admin.contacts.suppliers.create",
"admin.inventory.warehouse.view",
"admin.purchase-orders.orders.view",
"admin.purchase-orders.reception.view",
"admin.purchase-orders.materials.view",
"admin.inventory.stock.view",
"admin.inventory.movements.view",
"admin.inventory.transfers.view",
"admin.assets.assets.view",
"admin.assets.maintenance.view",
"admin.assets.lifecycle.view",
"admin.assets.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.products.products.view",
"admin.products.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.products.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.purchase-orders.orders.view",
"admin.purchase-orders.reception.view",
"admin.purchase-orders.materials.view",
"admin.inventory.warehouse.view",
"admin.inventory.stock.view",
"admin.inventory.movements.view",
"admin.inventory.transfers.view",
"admin.shipping.orders.view",
"admin.shipping.tracking.view",
"admin.shipping.carriers.view",
"admin.shipping.rates.view",
"admin.assets.assets.view",
"admin.assets.maintenance.view",
"admin.assets.lifecycle.view",
"admin.assets.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.products.products.view",
"admin.products.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.purchase-orders.orders.view",
"admin.purchase-orders.reception.view",
"admin.purchase-orders.materials.view",
"admin.inventory.warehouse.view",
"admin.inventory.stock.view",
"admin.inventory.movements.view",
"admin.inventory.transfers.view",
"admin.shipping.orders.view",
"admin.shipping.tracking.view",
"admin.shipping.carriers.view",
"admin.shipping.rates.view",
"admin.assets.assets.view",
"admin.assets.maintenance.view",
"admin.assets.lifecycle.view",
"admin.assets.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"
]
}

View File

@ -0,0 +1,892 @@
{
"module": "admin.core",
"name": {
"es": "Koneko Vuexy Admin",
"en": "Koneko Vuexy Admin"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de ajustes de sistema",
"en": "Permissions for managing system settings"
},
"icon": "ti ti-adjustments-alt"
},
"priority": "first",
"groups": {
"system-settings":{
"name": {
"es": "Ajustes de sistema",
"en": "System settings"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de ajustes de sistema",
"en": "Permissions for managing system settings"
},
"icon": "ti ti-adjustments-alt"
},
"priority": "first",
"sub_groups": {
"web-interface": {
"name": {
"es": "Interfaz Web",
"en": "Web Interface"
},
"_meta": {
"description": {
"es": "Permisos para la configuración de la interfaz web",
"en": "Permissions for configuring the web interface"
},
"icon": "ti ti-device-desktop-cog"
},
"priority": 100,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver configuración de interfaz web",
"en": "View web interface settings"
},
"key": "settings.web-interface.view"
},
{
"action": "update",
"label": {
"es": "Modificar configuración de interfaz web",
"en": "Update web interface settings"
},
"key": "settings.web-interface.update"
}
]
},
"vuexy-interface": {
"name": {
"es": "Interfaz Vuexy",
"en": "Vuexy Interface"
},
"_meta": {
"description": {
"es": "Permisos para la configuración de la interfaz Vuexy",
"en": "Permissions for configuring the Vuexy interface"
},
"icon": "ti ti-template"
},
"priority": 200,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver configuración de interfaz Vuexy",
"en": "View Vuexy interface settings"
},
"key": "settings.vuexy-interface.view"
},
{
"action": "update",
"label": {
"es": "Modificar configuración de interfaz Vuexy",
"en": "Update Vuexy interface settings"
},
"key": "settings.vuexy-interface.update"
}
]
},
"smtp": {
"name": {
"es": "Servidor SMTP",
"en": "SMTP Server"
},
"_meta": {
"description": {
"es": "Permisos para la configuración del servidor SMTP",
"en": "Permissions for configuring the SMTP server"
},
"icon": "ti ti-mail-cog"
},
"priority": 300,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver configuración de servidor SMTP",
"en": "View SMTP server settings"
},
"key": "settings.smtp.view"
},
{
"action": "update",
"label": {
"es": "Modificar configuración de servidor SMTP",
"en": "Update SMTP server settings"
},
"key": "settings.smtp.update"
}
]
},
"apis": {
"name": {
"es": "APIs",
"en": "APIs"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de APIs e integraciones",
"en": "Permissions for managing APIs and integrations"
},
"icon": "ti ti-plug-connected"
},
"priority": 400,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver configuración de APIs",
"en": "View APIs configuration"
},
"key": "settings.apis.view"
},
{
"action": "update",
"label": {
"es": "Modificar configuración de APIs",
"en": "Update APIs configuration"
},
"key": "settings.apis.update"
}
]
},
"env": {
"name": {
"es": "Variables de entorno",
"en": "Environment Variables"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de variables de entorno",
"en": "Permissions for managing environment variables"
},
"icon": "ti ti-settings-code"
},
"priority": 500,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver variables de entorno",
"en": "View environment variables"
},
"key": "settings.env.view"
},
{
"action": "update",
"label": {
"es": "Modificar variables de entorno",
"en": "Update environment variables"
},
"key": "settings.env.update"
}
]
}
}
},
"users-rbac": {
"name": {
"es": "Usuarios y control de Acceso",
"en": "Users and Access Control"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de usuarios y permisos de control de acceso (RBAC)",
"en": "Permissions for managing users and access control (RBAC)"
},
"icon": "ti ti-lock-access"
},
"priority": 100,
"sub_groups": {
"users": {
"name": {
"es": "Usuarios de sistema",
"en": "System users"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de usuarios de sistema",
"en": "Permissions for managing system users"
},
"icon": "__MENU__"
},
"priority": 100,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver listado de usuarios",
"en": "View user list"
},
"key": "users.users.view"
},
{
"action": "create",
"label": {
"es": "Crear usuario",
"en": "Create user"
},
"key": "users.users.create"
},
{
"action": "update",
"label": {
"es": "Editar usuario",
"en": "Update user"
},
"key": "users.users.update"
},
{
"action": "delete",
"label": {
"es": "Eliminar usuario",
"en": "Delete user"
},
"key": "users.users.delete"
},
{
"action": "export",
"label": {
"es": "Exportar listado de usuarios",
"en": "Export userlist"
},
"key": "users.users.export"
},
{
"action": "assign",
"label": {
"es": "Asignar roles",
"en": "Assign roles"
},
"key": "users.users-role.assign"
}
]
},
"roles": {
"name": {
"es": "Roles",
"en": "Roles"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de roles",
"en": "Permissions for managing roles"
},
"icon": "ti ti-lock-access"
},
"priority": 200,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver roles",
"en": "View roles"
},
"key": "rbac.roles.view"
},
{
"action": "create",
"label": {
"es": "Crear rol",
"en": "Create role"
},
"key": "rbac.roles.create"
},
{
"action": "update",
"label": {
"es": "Editar rol",
"en": "Update role"
},
"key": "rbac.roles.update"
},
{
"action": "delete",
"label": {
"es": "Eliminar rol",
"en": "Delete role"
},
"key": "rbac.roles.delete"
},
{
"action": "duplicate",
"label": {
"es": "Duplicar rol",
"en": "Duplicate role"
},
"key": "rbac.roles.duplicate"
}
]
},
"permissions": {
"name": {
"es": "Permisos",
"en": "Permissions"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de permisos",
"en": "Permissions for managing permissions"
},
"icon": "ti ti-lock-access",
"flags": {
"is_development": true
}
},
"priority": 300,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver permisos",
"en": "View permissions"
},
"key": "rbac.permissions.view"
},
{
"action": "create",
"label": {
"es": "Crear permiso",
"en": "Create permission"
},
"key": "rbac.permissions.create"
},
{
"action": "update",
"label": {
"es": "Editar permiso",
"en": "Update permission"
},
"key": "rbac.permissions.update"
},
{
"action": "delete",
"label": {
"es": "Eliminar permiso",
"en": "Delete permission"
},
"key": "rbac.permissions.delete"
}
]
}
}
},
"tools": {
"name": {
"es": "Herramientas de sistema",
"en": "System Tools"
},
"_meta": {
"description": {
"es": "Permisos para tareas programadas, caché, monitoreo y notificaciones del sistema",
"en": "Permissions for scheduled tasks, cache, monitoring and system notifications"
},
"icon": "ti ti-tool"
},
"priority": 200,
"sub_groups": {
"scheduler": {
"name": {
"es": "Tareas programadas",
"en": "Scheduled Tasks"
},
"_meta": {
"description": {
"es": "Supervisa y gestiona tareas periódicas, workers y ejecución en cola.",
"en": "Monitor and manage periodic tasks, workers, and queued execution."
},
"icon": "ti ti-clock"
},
"priority": 100,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver panel general",
"en": "View scheduler dashboard"
},
"key": "scheduler.dashboard.view"
},
{
"action": "view",
"label": {
"es": "Ver tareas programadas",
"en": "View scheduled tasks"
},
"key": "scheduler.cron.view"
},
{
"action": "view",
"label": {
"es": "Ver jobs en cola",
"en": "View queued jobs"
},
"key": "scheduler.queued-jobs.view"
},
{
"action": "view",
"label": {
"es": "Ver historial de ejecución",
"en": "View execution history"
},
"key": "scheduler.history.view"
},
{
"action": "configure",
"label": {
"es": "Configurar scheduler",
"en": "Configure scheduler"
},
"key": "scheduler.settings.view"
}
]
},
"cache": {
"name": {
"es": "Gestión de Caché",
"en": "Cache Management"
},
"_meta": {
"description": {
"es": "Limpieza y configuración de caché del sistema",
"en": "System cache cleaning and configuration"
},
"icon": "ti ti-cpu"
},
"priority": 200,
"permissions": [
{
"action": "clean",
"label": {
"es": "Limpiar caché Redis",
"en": "Clean Redis cache"
},
"key": "cache.redis.view"
},
{
"action": "clean",
"label": {
"es": "Limpiar caché Memcache",
"en": "Clean Memcache cache"
},
"key": "cache.memcache.view"
},
{
"action": "clean",
"label": {
"es": "Limpiar sesiones",
"en": "Clear sessions"
},
"key": "cache.sessions.view"
},
{
"action": "clean",
"label": {
"es": "Limpiar caché Laravel",
"en": "Clean Laravel cache"
},
"key": "cache.laravel.view"
},
{
"action": "clean",
"label": {
"es": "Limpiar caché Vuexy",
"en": "Clean Vuexy cache"
},
"key": "cache.vuexy.view"
},
{
"action": "clean",
"label": {
"es": "Limpiar assets generados",
"en": "Clean Vite assets"
},
"key": "cache.vite-assets.view"
},
{
"action": "configure",
"label": {
"es": "Ajustar TTLs",
"en": "Adjust TTLs"
},
"key": "cache.ttls.view"
}
]
},
"notifications": {
"name": {
"es": "Centro de Notificaciones",
"en": "Notification Center"
},
"_meta": {
"description": {
"es": "Notificaciones globales, personales y configuración del centro",
"en": "Global, personal notifications and alert center settings"
},
"icon": "ti ti-bell"
},
"priority": 300,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver notificaciones globales",
"en": "View global notifications"
},
"key": "notifications.system.view"
},
{
"action": "view",
"label": {
"es": "Ver notificaciones personales",
"en": "View personal notifications"
},
"key": "notifications.personal.view"
},
{
"action": "configure",
"label": {
"es": "Configurar centro de alertas",
"en": "Configure alert center"
},
"key": "notifications.settings.view"
}
]
},
"monitoring": {
"name": {
"es": "Monitoreo del Sistema",
"en": "System Monitoring"
},
"_meta": {
"description": {
"es": "Supervisión de sesiones activas y uso del sistema",
"en": "Active session and system usage monitoring"
},
"icon": "ti ti-heart-rate-monitor"
},
"priority": 400,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver sesiones activas",
"en": "View active sessions"
},
"key": "monitor.sessions.view"
}
]
}
}
},
"vuexy-admin": {
"name": {
"es": "Koneko Vuexy Admin",
"en": "Koneko Vuexy Admin"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de Koneko Vuexy Admin",
"en": "Permissions for managing Koneko Vuexy Admin"
},
"icon": "ti ti-lock-access"
},
"priority": 300,
"sub_groups": {
"plugins": {
"name": {
"es": "Librerías y plugins",
"en": "Libraries and plugins"
},
"_meta": {
"description": {
"es": "Gestiona las librerías y plugins del módulo Vuexy Admin.",
"en": "Manage libraries and plugins in the Vuexy Admin module."
},
"icon": "ti ti-plug"
},
"priority": 100,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver plugins",
"en": "View plugins"
},
"key": "modules.plugins.view"
},
{
"action": "install",
"label": {
"es": "Instalar plugins",
"en": "Install plugins"
},
"key": "modules.plugins.install"
},
{
"action": "update",
"label": {
"es": "Actualizar plugins",
"en": "Update plugins"
},
"key": "modules.plugins.update"
},
{
"action": "delete",
"label": {
"es": "Eliminar plugins",
"en": "Delete plugins"
},
"key": "modules.plugins.delete"
}
]
},
"config": {
"name": {
"es": "Configuración de módulos",
"en": "Modules configuration"
},
"_meta": {
"description": {
"es": "Administra la configuración avanzada de módulos y paquetes instalados.",
"en": "Manage advanced configuration of installed modules and packages."
},
"icon": "ti ti-puzzle"
},
"priority": 200,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver configuración",
"en": "View configuration"
},
"key": "modules.config.view"
},
{
"action": "update",
"label": {
"es": "Editar configuración",
"en": "Edit configuration"
},
"key": "modules.config.update"
}
]
}
}
},
"audit": {
"name": {
"es": "Auditoría",
"en": "Audit"
},
"_meta": {
"description": {
"es": "Permisos para la gestión de auditoría",
"en": "Permissions for managing audit"
},
"icon": "ti ti-bell"
},
"priority": 400,
"sub_groups": {
"access": {
"name": {
"es": "Eventos de Acceso",
"en": "Access Events"
},
"_meta": {
"description": {
"es": "Historial de inicios de sesión y cierres por usuario.",
"en": "Login and logout history per user."
},
"icon": "ti ti-user-shield"
},
"priority": 100,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver logs de acceso",
"en": "View access logs"
},
"key": "audit.access.view"
}
]
},
"security-events": {
"name": {
"es": "Eventos de Seguridad",
"en": "Security Events"
},
"_meta": {
"description": {
"es": "Registros enriquecidos con geolocalización, IP, dispositivos y actividad sospechosa.",
"en": "Logs enriched with geolocation, IP, devices, and suspicious activity."
},
"icon": "ti ti-shield"
},
"priority": 200,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver eventos de seguridad",
"en": "View security events"
},
"key": "audit.security-events.view"
}
]
},
"user-interactions": {
"name": {
"es": "Interacciones de Usuario",
"en": "User Interactions"
},
"_meta": {
"description": {
"es": "Registro detallado de acciones ejecutadas por usuarios en la interfaz.",
"en": "Detailed log of user interface actions."
},
"icon": "ti ti-user-check"
},
"priority": 300,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver interacciones de usuario",
"en": "View user interactions"
},
"key": "audit.user-interactions.view"
}
]
},
"file-logs": {
"name": {
"es": "Logs del Sistema",
"en": "System Logs"
},
"_meta": {
"description": {
"es": "Visualiza logs generados por Laravel u otros sistemas locales.",
"en": "View logs generated by Laravel or other local systems."
},
"icon": "ti ti-file-text"
},
"priority": 400,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver logs del sistema",
"en": "View system logs"
},
"key": "audit.file-logs.view"
}
]
},
"db-logs": {
"name": {
"es": "Logs de Base de Datos",
"en": "Database Logs"
},
"_meta": {
"description": {
"es": "Consulta los logs persistidos en la base de datos estructurados por tipo y nivel.",
"en": "Query logs stored in the database by type and level."
},
"icon": "ti ti-database-search"
},
"priority": 500,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver logs de base de datos",
"en": "View database logs"
},
"key": "audit.db-logs.view"
}
]
},
"modules": {
"name": {
"es": "Logs por Módulo",
"en": "Module Logs"
},
"_meta": {
"description": {
"es": "Visualiza logs agrupados por componente o módulo.",
"en": "View logs grouped by component or module."
},
"icon": "ti ti-box-multiple"
},
"priority": 600,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver logs por módulo",
"en": "View module logs"
},
"key": "audit.modules.view"
}
]
},
"alerts": {
"name": {
"es": "Alertas y Reportes",
"en": "Alerts and Reports"
},
"_meta": {
"description": {
"es": "Configura alertas automáticas, condiciones críticas y reportes periódicos.",
"en": "Configure automatic alerts, critical conditions, and periodic reports."
},
"icon": "ti ti-bell"
},
"priority": 950,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver alertas y reportes",
"en": "View alerts and reports"
},
"key": "audit.alerts.view"
}
]
},
"logging-settings": {
"name": {
"es": "Configuración de Logging",
"en": "Logging Configuration"
},
"_meta": {
"description": {
"es": "Configuración avanzada del sistema de logging y auditoría.",
"en": "Advanced configuration of the logging and audit system."
},
"icon": "ti ti-settings"
},
"priority": 999,
"permissions": [
{
"action": "view",
"label": {
"es": "Ver configuración de logging",
"en": "View logging configuration"
},
"key": "audit.logging-settings.view"
}
]
}
}
}
}
}

View File

@ -0,0 +1,205 @@
{
"SuperAdmin": {
"_meta": {
"description": {
"es": "Rol con acceso total a todo el sistema, configuración, seguridad y módulos.",
"en": "Role with full access to the system, configuration, security, and modules."
},
"icon": "ti ti-shield-lock",
"style": "dark"
},
"permissions" : [
"admin.core.settings.web-interface.view",
"admin.core.settings.web-interface.update",
"admin.core.settings.vuexy-interface.view",
"admin.core.settings.vuexy-interface.update",
"admin.core.settings.smtp.view",
"admin.core.settings.smtp.update",
"admin.core.settings.apis.view",
"admin.core.settings.apis.update",
"admin.core.settings.env.view",
"admin.core.settings.env.update",
"admin.core.users.users.view",
"admin.core.users.users.create",
"admin.core.users.users.update",
"admin.core.users.users.delete",
"admin.core.users.users.export",
"admin.core.users.users-role.assign",
"admin.core.rbac.roles.view",
"admin.core.rbac.roles.create",
"admin.core.rbac.roles.update",
"admin.core.rbac.roles.delete",
"admin.core.rbac.roles.duplicate",
"admin.core.rbac.permissions.view",
"admin.core.rbac.permissions.create",
"admin.core.rbac.permissions.update",
"admin.core.rbac.permissions.delete",
"admin.core.scheduler.dashboard.view",
"admin.core.scheduler.cron.view",
"admin.core.scheduler.queued-jobs.view",
"admin.core.scheduler.history.view",
"admin.core.scheduler.settings.view",
"admin.core.cache.redis.view",
"admin.core.cache.memcache.view",
"admin.core.cache.sessions.view",
"admin.core.cache.laravel.view",
"admin.core.cache.vuexy.view",
"admin.core.cache.vite-assets.view",
"admin.core.cache.ttls.view",
"admin.core.notifications.system.view",
"admin.core.notifications.personal.view",
"admin.core.notifications.settings.view",
"admin.core.monitor.sessions.view",
"admin.core.modules.plugins.view",
"admin.core.modules.plugins.install",
"admin.core.modules.plugins.update",
"admin.core.modules.plugins.delete",
"admin.core.modules.config.view",
"admin.core.modules.config.update",
"admin.core.audit.access.view",
"admin.core.audit.security-events.view",
"admin.core.audit.user-interactions.view",
"admin.core.audit.file-logs.view",
"admin.core.audit.db-logs.view",
"admin.core.audit.modules.view",
"admin.core.audit.alerts.view",
"admin.core.audit.logging-settings.view"
]
},
"Admin": {
"_meta": {
"description": {
"es": "Acceso total a configuración del sistema, usuarios, módulos y caché.",
"en": "Full access to system configuration, users, modules, and cache."
},
"icon": "ti ti-settings",
"style": "dark"
},
"permissions" : [
"admin.core.settings.web-interface.view",
"admin.core.settings.web-interface.update",
"admin.core.settings.vuexy-interface.view",
"admin.core.settings.vuexy-interface.update",
"admin.core.settings.smtp.view",
"admin.core.settings.smtp.update",
"admin.core.settings.apis.view",
"admin.core.settings.apis.update",
"admin.core.settings.env.view",
"admin.core.settings.env.update",
"admin.core.users.users.view",
"admin.core.users.users.create",
"admin.core.users.users.update",
"admin.core.users.users.delete",
"admin.core.users.users.export",
"admin.core.users.users-role.assign",
"admin.core.rbac.roles.view",
"admin.core.rbac.roles.create",
"admin.core.rbac.roles.update",
"admin.core.rbac.roles.delete",
"admin.core.rbac.roles.duplicate",
"admin.core.scheduler.dashboard.view",
"admin.core.scheduler.cron.view",
"admin.core.scheduler.queued-jobs.view",
"admin.core.scheduler.history.view",
"admin.core.scheduler.settings.view",
"admin.core.cache.redis.view",
"admin.core.cache.memcache.view",
"admin.core.cache.sessions.view",
"admin.core.cache.laravel.view",
"admin.core.cache.vuexy.view",
"admin.core.cache.vite-assets.view",
"admin.core.cache.ttls.view",
"admin.core.notifications.system.view",
"admin.core.notifications.personal.view",
"admin.core.notifications.settings.view",
"admin.core.monitor.sessions.view",
"admin.core.modules.plugins.view",
"admin.core.modules.plugins.install",
"admin.core.modules.plugins.update",
"admin.core.modules.plugins.delete",
"admin.core.modules.config.view",
"admin.core.modules.config.update",
"admin.core.audit.access.view",
"admin.core.audit.security-events.view",
"admin.core.audit.user-interactions.view",
"admin.core.audit.file-logs.view",
"admin.core.audit.db-logs.view",
"admin.core.audit.modules.view",
"admin.core.audit.alerts.view",
"admin.core.audit.logging-settings.view"
]
},
"UserAdmin": {
"_meta": {
"description": {
"es": "Gestiona usuarios, roles y permisos del sistema.",
"en": "Manages system users, roles, and permissions."
},
"icon": "ti ti-users",
"style": "secondary"
},
"permissions": [
"admin.core.users.users.view",
"admin.core.users.users.create",
"admin.core.users.users.update",
"admin.core.users.users.delete",
"admin.core.users.users.export",
"admin.core.users.users-role.assign",
"admin.core.rbac.roles.view",
"admin.core.rbac.roles.create",
"admin.core.rbac.roles.update",
"admin.core.rbac.roles.delete",
"admin.core.rbac.roles.duplicate",
"admin.core.rbac.permissions.view",
"admin.core.rbac.permissions.create",
"admin.core.rbac.permissions.update",
"admin.core.rbac.permissions.delete"
]
},
"Auditor": {
"_meta": {
"description": {
"es": "Visualiza logs, interacciones y auditorías del sistema.",
"en": "Views logs, interactions, and system audits."
},
"icon": "ti ti-report-analytics",
"style": "info"
},
"permissions" : [
"admin.core.settings.web-interface.view",
"admin.core.settings.vuexy-interface.view",
"admin.core.settings.smtp.view",
"admin.core.settings.apis.view",
"admin.core.settings.env.view",
"admin.core.users.users.view",
"admin.core.rbac.roles.view",
"admin.core.rbac.permissions.view",
"admin.core.scheduler.dashboard.view",
"admin.core.scheduler.cron.view",
"admin.core.scheduler.queued-jobs.view",
"admin.core.scheduler.history.view",
"admin.core.scheduler.settings.view",
"admin.core.cache.redis.view",
"admin.core.cache.memcache.view",
"admin.core.cache.sessions.view",
"admin.core.cache.laravel.view",
"admin.core.cache.vuexy.view",
"admin.core.cache.vite-assets.view",
"admin.core.cache.ttls.view",
"admin.core.notifications.system.view",
"admin.core.notifications.personal.view",
"admin.core.notifications.settings.view",
"admin.core.monitor.sessions.view",
"admin.core.modules.plugins.view",
"admin.core.modules.config.view",
"admin.core.audit.access.view",
"admin.core.audit.security-events.view",
"admin.core.audit.user-interactions.view",
"admin.core.audit.file-logs.view",
"admin.core.audit.db-logs.view",
"admin.core.audit.modules.view",
"admin.core.audit.alerts.view",
"admin.core.audit.logging-settings.view"
]
}
}

View File

@ -0,0 +1,4 @@
name,email,password,roles,avatar_path
Koneko Admin,sadmin@koneko.mx,LAdmin123,"[""SuperAdmin""]",vendor/vuexy-admin/img/logo/koneko-02.png
Administrador,admin@koneko.mx,LAdmin123,"[""Admin""]",vendor/vuexy-admin/img/logo/koneko-03.png
Auditor,auditor@koneko.mx,LAdmin123,"[""Auditor""]",vendor/vuexy-admin/img/logo/koneko-03.png
1 name email password roles avatar_path
2 Koneko Admin sadmin@koneko.mx LAdmin123 ["SuperAdmin"] vendor/vuexy-admin/img/logo/koneko-02.png
3 Administrador admin@koneko.mx LAdmin123 ["Admin"] vendor/vuexy-admin/img/logo/koneko-03.png
4 Auditor auditor@koneko.mx LAdmin123 ["Auditor"] vendor/vuexy-admin/img/logo/koneko-03.png

View File

@ -0,0 +1,45 @@
[
{
"name": "Cliente de prueba",
"email": "cliente@koneko.mx",
"password": "cliente123",
"rfc": "XAXX010101000",
"roles": ["SuperAdmin"]
},
{
"name": "Proveedor Koneko",
"email": "proveedor@koneko.mx",
"password": "proveedor123",
"tipo_persona": 2,
"curp": "GOML850927MOCSRN09",
"roles": ["SuperAdmin", "Auditor"]
},
{
"code": "U002",
"parent_id": 1,
"agent_id": 2,
"name": "Usuario Completo",
"last_name": "Pérez López",
"email": "usuario.completo@example.com",
"company": "Empresa de Prueba S.A. de C.V.",
"c_pais": "MEX",
"birth_date": "1990-06-15",
"hire_date": "2022-01-10",
"curp": "LOPJ900615HDFRNS03",
"nss": "12345678901",
"rfc": "LOPJ900615ABC",
"nombre_fiscal": "López Pérez Juan",
"tipo_persona": 1,
"c_regimen_fiscal": 601,
"domicilio_fiscal": 64000,
"enable_credit": 1,
"credit_days": 30,
"credit_limit": "15000.00",
"license_number": "A123456789",
"policy_format": "NOM035",
"special_requirements": "Ninguno",
"password": "proveedor123",
"notes": "Este usuario tiene todos los campos completados.",
"status": 1
}
]

View File

@ -1,14 +0,0 @@
name,email,role,password
Administrador Web,webadmin@koneko.test,Administrador Web,LAdmin123
Productos y servicios,productos@koneko.test,Productos y servicios,LAdmin123
Recursos humanos,rrhh@koneko.test,Recursos humanos,LAdmin123
Nómina,nomina@koneko.test,Nómina,LAdmin123
Activos fijos,activos@koneko.test,Activos fijos,LAdmin123
Compras y gastos,compras@koneko.test,Compras y gastos,LAdmin123
CRM,crm@koneko.test,CRM,LAdmin123
Vendedor,vendedor@koneko.test,Vendedor,LAdmin123
Gerente,gerente@koneko.test,Gerente,LAdmin123
Facturación,facturacion@koneko.test,Facturación,LAdmin123
Facturación avanzado,facturacion_avanzado@koneko.test,Facturación avanzado,LAdmin123
Finanzas,finanzas@koneko.test,Finanzas,LAdmin123
Almacenista,almacenista@koneko.test,Almacenista,LAdmin123
1 name email role password
2 Administrador Web webadmin@koneko.test Administrador Web LAdmin123
3 Productos y servicios productos@koneko.test Productos y servicios LAdmin123
4 Recursos humanos rrhh@koneko.test Recursos humanos LAdmin123
5 Nómina nomina@koneko.test Nómina LAdmin123
6 Activos fijos activos@koneko.test Activos fijos LAdmin123
7 Compras y gastos compras@koneko.test Compras y gastos LAdmin123
8 CRM crm@koneko.test CRM LAdmin123
9 Vendedor vendedor@koneko.test Vendedor LAdmin123
10 Gerente gerente@koneko.test Gerente LAdmin123
11 Facturación facturacion@koneko.test Facturación LAdmin123
12 Facturación avanzado facturacion_avanzado@koneko.test Facturación avanzado LAdmin123
13 Finanzas finanzas@koneko.test Finanzas LAdmin123
14 Almacenista almacenista@koneko.test Almacenista LAdmin123

View File

@ -1,49 +0,0 @@
<?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

@ -1,9 +1,8 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Schema\Blueprint;
use Koneko\VuexyAdmin\Models\User;
use Illuminate\Support\Facades\{DB,Schema};
return new class extends Migration
{
@ -19,9 +18,12 @@ return new class extends Migration
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->boolean('status')->default(1)->after('profile_photo_path');
$table->unsignedMediumInteger('created_by')->nullable()->index()->after('status');
// Auditoria
$table->softDeletes();
// Definir la relación con created_by
$table->foreign('created_by')->references('id')->on('users')->onUpdate('restrict')->onDelete('restrict');
});
@ -35,10 +37,5 @@ return new class extends Migration
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,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
use Koneko\VuexyAdmin\Application\Enums\User\UserBaseFlags;
use Koneko\VuexyAdmin\Support\Traits\Migrations\HandlesGeneratedColumns;
return new class extends Migration
{
use HandlesGeneratedColumns;
public function up()
{
// Añadir columna contenedora si no existe
if (!Schema::hasColumn('users', 'flags')) {
Schema::table('users', function ($table) {
$table->json('flags')
->nullable()
->after('profile_photo_path')
->comment('Dynamic flags storage');
});
}
// Añadir columnas generadas
$this->addGeneratedColumns('users', UserBaseFlags::cases());
}
public function down()
{
// Eliminar columnas generadas
$this->dropGeneratedColumns('users', UserBaseFlags::cases());
// Eliminar columna flags (opcional)
Schema::table('users', function ($table) {
$table->dropColumnIfExists('flags');
});
}
};

View File

@ -1,36 +0,0 @@
<?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,45 @@
<?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('permission_groups', function (Blueprint $table) {
$table->smallIncrements('id');
$table->string("module_register")->index(); // Nombre del módulo registrado
$table->unsignedSmallInteger('parent_id')->nullable()->index(); // ID del grupo padre
$table->string('type', 16)->default('root_group'); // Enum: root_group, sub_group, external_group
$table->string("module", 32)->index();
$table->string("grupo", 32)->nullable()->index();
$table->string("sub_grupo", 32)->nullable()->index();
$table->json('name')->nullable(); // Nombre i18n
$table->json('ui_metadata')->nullable(); // icon, description i18n, flags, ...
$table->string('priority', 16)->nullable()->index();
// Auditoría
$table->timestamps();
// Relaciones
$table->foreign('parent_id')->references('id')->on('permission_groups')->restrictOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('permission_groups');
}
};

View File

@ -27,49 +27,69 @@ return new class extends Migration
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->smallIncrements('id');
// Permiso Spatie
$table->string('name')->unique(); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
// Metadata
$table->unsignedSmallInteger('group_id')->nullable()->index();
$table->json('label')->nullable(); // Nombre del permiso i18n
$table->json('ui_metadata')->nullable(); // helperText, floatLabel
// Acción
$table->string('action', 16)->nullable()->index(); // enum PermissionAction: view, create, update, delete, install, clean
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
// Auditoría
$table->timestamps();
$table->unique(['name', 'guard_name']);
$table->unique(['group_name', 'sub_group_name', 'action', 'guard_name']);
// Relaciones
$table->foreign('group_id')
->references('id')
->on('permission_groups')
->restrictOnDelete()
->cascadeOnUpdate();
});
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
//$table->engine('InnoDB');
$table->bigIncrements('id'); // role id
$table->smallIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->unsignedSmallInteger($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->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->json('ui_metadata')->nullable(); // Tailwind classes, icon, helperText, floatLabel
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
// Auditoría
$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->unsignedSmallInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->unsignedSmallInteger($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->unsignedSmallInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary(
@ -85,18 +105,19 @@ return new class extends Migration
});
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->unsignedSmallInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->unsignedSmallInteger($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->unsignedSmallInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary(
@ -112,8 +133,8 @@ return new class extends Migration
});
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->unsignedSmallInteger($pivotPermission);
$table->unsignedSmallInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id

View File

@ -13,16 +13,35 @@ return new class extends Migration
public function up(): void
{
Schema::create('settings', function (Blueprint $table) {
$table->smallIncrements('id');
$table->mediumIncrements('id');
// Clave del setting
$table->string('key')->index();
$table->string('key')->index(); // Clave del setting
// Categoría (opcional pero recomendable)
$table->string('category')->nullable()->index();
$table->string('namespace', 8)->index(); // Namespace del setting
$table->string('environment', 7)->default('prod')->index(); // Entorno de aplicación (prod, dev, test, staging), permite sobrescribir valores según ambiente.
$table->string('scope', 6)->default('global')->index(); // Define el alcance: global, tenant, branch, user, etc. Útil en arquitecturas multicliente.
// Usuario (null para globales)
$table->unsignedMediumInteger('user_id')->nullable()->index();
$table->string('component', 16)->index(); // Nombre de Componente o proyecto
$table->string('module')->nullable()->index(); // composerName de módulo Autocalculado
$table->string('group', 16)->index(); // Grupo de configuraciones
$table->string('sub_group', 16)->index(); // Sub grupo de configuraciones
$table->string('key_name', 24)->index(); // Nombre de la clave de configuraciones
$table->unsignedMediumInteger('user_id')->nullable()->index(); // Usuario (null para globales)
$table->boolean('is_system')->default(false)->index(); // Indica si es un setting de sistema
$table->boolean('is_encrypted')->default(false)->index(); // Si el valor está cifrado (para secretos, tokens, passwords).
$table->boolean('is_sensitive')->default(false)->index(); // Marca datos sensibles (ej. datos personales, claves API). Puede ocultarse en UI o logs.
$table->boolean('is_editable')->default(true)->index(); // Permite o bloquea edición desde la UI (útil para settings de solo lectura).
$table->boolean('is_active')->default(true)->index(); // Permite activar/desactivar la aplicación de un setting sin eliminarlo.
$table->string('encryption_key', 64)->nullable()->index(); // Identificador de la clave usada (ej. 'ssl_cert_2025')
$table->string('encryption_algorithm', 16)->nullable(); // Ej. 'AES-256-CBC'
$table->timestamp('encryption_rotated_at')->nullable(); // Fecha de última rotación de clave
$table->string('description')->nullable();
$table->string('hint')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->integer('usage_count')->default(0)->index();
// Valores segmentados por tipo para mejor rendimiento
$table->string('value_string')->nullable();
@ -35,28 +54,47 @@ return new class extends Migration
$table->string('file_name')->nullable();
// Auditoría
$table->timestamps();
$table->unsignedMediumInteger('updated_by')->nullable();
$table->unsignedMediumInteger('created_by')->nullable()->index();
$table->unsignedMediumInteger('updated_by')->nullable()->index();
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
// Unique constraint para evitar duplicados
$table->unique(['key', 'user_id', 'category']);
$table->timestamps();
$table->softDeletes(); // THIS ONE
// Índice de Unicidad Principal (para consultas y updates rápidos)
$table->unique(
['namespace', 'environment', 'scope', 'component', 'group', 'sub_group', 'key_name', 'user_id'],
'uniq_settings_full_context'
);
// Búsqueda rápida por componente
$table->index(['namespace', 'component'], 'idx_settings_ns_component');
// Listar grupos de un componente por scope
$table->index(['namespace', 'scope', 'component', 'group'], 'idx_settings_ns_scope_component_group');
// Listar subgrupos por grupo y componente en un scope
$table->index(['namespace', 'scope', 'component', 'group', 'sub_group'], 'idx_settings_ns_scope_component_sg');
// Consultas por entorno y usuario
$table->index(['namespace', 'environment', 'user_id'], 'idx_settings_ns_env_user');
// Consultas por scope y usuario
$table->index(['namespace', 'scope', 'user_id'], 'idx_settings_ns_scope_user');
// Consultas por estado de actividad o sistema
$table->index(['namespace', 'is_active'], 'idx_settings_ns_is_active');
$table->index(['namespace', 'is_system'], 'idx_settings_ns_is_system');
// Consultas por estado de cifrado y usuario
$table->index(['namespace', 'is_encrypted', 'user_id'], 'idx_settings_encrypted_user');
// Relaciones
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
$table->foreign('user_id')->references('id')->on('users')->restrictOnDelete();
$table->foreign('created_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('deleted_by')->references('id')->on('users')->restrictOnDelete();
});
// Agregar columna virtual unificada
DB::statement("ALTER TABLE settings ADD COLUMN value VARCHAR(255) GENERATED ALWAYS AS (
CASE
WHEN value_string IS NOT NULL THEN value_string
WHEN value_integer IS NOT NULL THEN CAST(value_integer AS CHAR)
WHEN value_boolean IS NOT NULL THEN IF(value_boolean, 'true', 'false')
WHEN value_float IS NOT NULL THEN CAST(value_float AS CHAR)
WHEN value_text IS NOT NULL THEN LEFT(value_text, 255)
WHEN value_binary IS NOT NULL THEN '[binary_data]'
ELSE NULL
END
) VIRTUAL");
}
/**

View File

@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::connection('vault')->create('vault_keys', function (Blueprint $table) {
$table->bigIncrements('id');
// Contexto de clave
$table->string('alias', 64)->unique()->index();
$table->string('owner_project', 64)->index();
$table->string('environment', 10)->default('prod')->index(); // prod, dev, staging
$table->string('namespace', 32)->default('core')->index();
$table->string('scope', 16)->default('global')->index(); // global, tenant, user
// Datos de la clave
$table->string('algorithm', 32)->default('AES-256-CBC');
$table->binary('key_material');
$table->boolean('is_active')->default(true);
$table->boolean('is_sensitive')->default(true);
// Control de rotación
$table->timestamp('rotated_at')->nullable();
$table->unsignedInteger('rotation_count')->default(0);
// Auditoría
$table->unsignedBigInteger('created_by')->nullable()->index();
$table->unsignedBigInteger('updated_by')->nullable()->index();
$table->unsignedBigInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Índices adicionales
$table->index(['owner_project', 'environment', 'namespace', 'scope', 'is_active'], 'idx_full_context');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::connection('vault')->dropIfExists('settings');
}
};

View File

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('external_apis', function (Blueprint $table) {
$table->id();
// Identidad y relación
$table->string('slug')->unique(); // api-google-analytics
$table->string('name'); // Google Analytics
$table->string('module')->index(); // vuexy-website-admin
$table->string('provider')->nullable()->index(); // Google, Twitter, Banxico
// Autenticación y alcance
$table->string('auth_type', 16)->nullable()->index(); // api_key, oauth2, jwt, none
$table->json('credentials')->nullable(); // api_key, secret, token, etc.
$table->json('scopes')->nullable(); // ['read', 'write', 'analytics']
// Conectividad
$table->string('base_url')->nullable(); // https://api.example.com
$table->string('doc_url')->nullable(); // https://docs.example.com
// Estado y entorno
$table->string('environment')->default('production')->index(); // dev, staging, prod
$table->boolean('is_active')->default(true)->index(); // toggle global
// Extensión dinámica
$table->json('metadata')->nullable(); // libre: headers extra, tags, categorías
$table->json('config')->nullable(); // libre: UI params, rate limits, etc.
// Auditoría
$table->timestamps();
$table->unsignedMediumInteger('created_by')->nullable()->index();
$table->unsignedMediumInteger('updated_by')->nullable()->index();
// Relaciones
$table->foreign('created_by')->references('id')->on('users')->nullOnDelete();
$table->foreign('updated_by')->references('id')->on('users')->nullOnDelete();
});
}
public function down(): void
{
Schema::dropIfExists('external_apis');
}
};

View File

@ -0,0 +1,58 @@
<?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()->index();
$table->string('user_agent')->nullable();
$table->string('device_type')->nullable();
$table->string('browser')->nullable();
$table->string('browser_version')->nullable();
$table->string('os')->nullable();
$table->string('os_version')->nullable();
$table->string('country')->nullable()->index();
$table->string('region')->nullable();
$table->string('city')->nullable();
$table->decimal('lat', 10, 8)->nullable();
$table->decimal('lng', 11, 8)->nullable();
$table->boolean('is_proxy')->default(false)->index();
$table->boolean('login_success')->default(true)->index();
$table->timestamp('logout_at')->nullable();
$table->string('logout_reason')->nullable();
$table->json('additional_info')->nullable();
// Auditoría
$table->timestamps();
// Indices
$table->index(['user_id', 'login_success']);
$table->index(['user_id', 'logout_at']);
$table->index(['user_id', 'logout_at', 'login_success']);
// 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,73 @@
<?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('security_events', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('module')->nullable()->index(); // Modulo (opcional pero recomendable)
$table->unsignedMediumInteger('user_id')->nullable()->index(); // Usuario
// Información básica del evento
$table->string('event_type')->index();
$table->string('status')->default('new')->index();
// Información de acceso
$table->ipAddress('ip_address')->nullable()->index();
$table->string('user_agent')->nullable();
$table->string('device_type', 100)->nullable();
$table->string('browser', 100)->nullable();
$table->string('browser_version')->nullable();
$table->string('os', 100)->nullable();
$table->string('os_version', 100)->nullable();
// Información GeoIP (geoip2)
$table->string('country')->nullable()->index();
$table->string('region')->nullable();
$table->string('city')->nullable();
$table->decimal('lat', 10, 8)->nullable();
$table->decimal('lng', 11, 8)->nullable();
// Información adicional del evento
$table->boolean('is_proxy')->default(false)->index();
$table->string('url')->nullable();
$table->string('http_method', 10)->nullable();
// JSON payload del evento para análisis avanzado
$table->json('payload')->nullable();
// Auditoría
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Indices
$table->index(['event_type', 'status', 'user_id']);
$table->index(['user_id', 'event_type']);
$table->index(['user_id', 'event_type', 'status']);
// Relaciones
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('deleted_by')->references('id')->on('users')->restrictOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Elimina tablas solo si existen
Schema::dropIfExists('user_logins');
}
};

View File

@ -0,0 +1,61 @@
<?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('system_logs', function (Blueprint $table) {
$table->integerIncrements('id');
// Relación polimórfica: puede ser un pedido, una factura, etc.
$table->unsignedMediumInteger('loggable_id')->index();
$table->string('loggable_type')->index();
$table->string('module')->nullable()->index(); // Modulo (opcional pero recomendable)
$table->unsignedMediumInteger('user_id')->nullable()->index(); // Usuario
$table->string('level', 16)->index(); // info, warning, error
$table->text('message');
$table->json('context')->nullable(); // datos estructurados
$table->string('trigger_type', 16)->index(); // user, cronjob, webhook, etc.
$table->unsignedMediumInteger('trigger_id')->nullable()->index(); // user_id, job_id, etc.
// Auditoría
$table->unsignedMediumInteger('updated_by')->nullable()->index();
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Índices
$table->index(['loggable_id', 'loggable_type']);
$table->index(['loggable_type', 'user_id']);
$table->index(['loggable_type', 'level']);
$table->index(['trigger_type', 'level']);
$table->index(['loggable_type', 'user_id', 'level']);
$table->index(['trigger_type', 'user_id', 'level']);
// Relaciones
$table->foreign('user_id')->references('id')->on('users')->onDelete('restrict');
$table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('deleted_by')->references('id')->on('users')->restrictOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Elimina tablas solo si existen
Schema::dropIfExists('system_logs');
}
};

View File

@ -0,0 +1,72 @@
<?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_interactions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('module')->nullable()->index(); // Modulo (opcional pero recomendable)
$table->unsignedMediumInteger('user_id')->index(); // Usuario
$table->string('livewire_component')->nullable()->index(); // ejemplo: inventory.products
$table->string('action')->index(); // ejemplo: view, update, export
$table->string('security_level')->default('normal')->index(); // normal, sensible, crítico
$table->ipAddress('ip_address')->nullable();
$table->string('user_agent')->nullable();
$table->json('context')->nullable();
// 🛡️ Flags de auditoría administrativa
$table->json('user_flags')->nullable(); // snapshot de is_admin, is_client, etc.
$table->json('user_roles')->nullable(); // ["admin", "manager"]
$table->mediumText('notes')->nullable(); // comentarios internos de admin
$table->json('chat_thread')->nullable(); // [{user_id:1, msg:"...", at:"..."}, ...]
$table->boolean('is_reviewed')->default(false)->index();
$table->boolean('is_flagged')->default(false)->index();
$table->boolean('is_escalated')->default(false)->index();
$table->unsignedMediumInteger('reviewed_by')->nullable()->index();
$table->unsignedMediumInteger('flagged_by')->nullable()->index();
$table->unsignedMediumInteger('escalated_by')->nullable()->index();
// Auditoria
$table->unsignedMediumInteger('updated_by')->nullable()->index();
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Indices
$table->index(['user_id', 'is_reviewed', 'security_level']);
$table->index(['user_id', 'is_flagged', 'security_level']);
$table->index(['user_id', 'is_escalated', 'security_level']);
$table->index(['user_id', 'is_reviewed', 'is_flagged', 'is_escalated', 'security_level'])->name('user_interactions_user_id_is_reviewed_flagged_escalated_sec_lev_index');
// Relaciones
$table->foreign('user_id')->references('id')->on('users')->onDelete('restrict');
$table->foreign('reviewed_by')->references('id')->on('users')->onDelete('restrict');
$table->foreign('flagged_by')->references('id')->on('users')->onDelete('restrict');
$table->foreign('escalated_by')->references('id')->on('users')->onDelete('restrict');
$table->foreign('updated_by')->references('id')->on('users')->onDelete('restrict');
$table->foreign('deleted_by')->references('id')->on('users')->onDelete('restrict');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_interactions');
}
};

View File

@ -0,0 +1,57 @@
<?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(): void
{
Schema::create('device_tokens', function (Blueprint $table) {
$table->id();
$table->unsignedMediumInteger('user_id')->nullable()->index();
$table->string('token')->unique(); // Token del dispositivo
$table->string('platform', 32)->nullable()->index(); // Plataforma: ios, android, web, desktop, etc.
$table->string('client')->nullable(); // Navegador o cliente usado
$table->mediumText('device_info')->nullable(); // Información extendida del dispositivo
$table->string('location', 128)->nullable(); // Ubicación del dispositivo (opcional)
$table->timestamp('last_used_at')->nullable(); // Último uso
$table->boolean('is_active')->default(true);
// Auditoria
$table->unsignedMediumInteger('created_by')->nullable()->index();
$table->unsignedMediumInteger('updated_by')->nullable()->index();
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Indices
$table->index(['user_id', 'platform']);
$table->index(['user_id', 'is_active']);
$table->index(['user_id', 'is_active', 'platform']);
// Relaciones
$table->foreign('user_id')->references('id')->on('users')->restrictOnDelete();
$table->foreign('created_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('deleted_by')->references('id')->on('users')->restrictOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('device_tokens');
}
};

View File

@ -0,0 +1,58 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('system_notifications', function (Blueprint $table) {
$table->mediumIncrements('id');
$table->string('scope', 16)->default('both')->index(); // enum 'admin', 'frontend', 'both'
$table->string('type', 16)->default('info')->index(); // enum 'info', 'success', 'warning', 'danger', 'promo'
$table->string('title')->index();
$table->text('message');
$table->string('channel', 32)->default('toast')->index(); // toast | push | websocket | etc.
$table->string('style', 16)->default('banner')->index(); // enum 'toast', 'banner', 'modal', 'inline'
$table->string('priority', 16)->default('medium')->index(); // Enum 'low', 'medium', 'high', 'critical'
$table->boolean('requires_confirmation')->default(false)->index();
$table->string('target_area', 32)->nullable()->comment('Ej: header, sidebar, checkout, etc.')->index();
$table->json('tags')->nullable();
$table->json('roles')->nullable();
$table->json('user_flags')->nullable();
$table->boolean('is_active')->default(true)->index();
$table->timestamp('starts_at')->nullable();
$table->timestamp('ends_at')->nullable();
// Auditoría
$table->unsignedMediumInteger('created_by')->nullable()->index();
$table->unsignedMediumInteger('updated_by')->nullable()->index();
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Indices
$table->index(['scope', 'type']);
$table->index(['is_active', 'starts_at', 'ends_at']);
// Relaciones
$table->foreign('created_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('deleted_by')->references('id')->on('users')->restrictOnDelete();
});
}
public function down(): void
{
Schema::dropIfExists('system_notifications');
}
};

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('system_notification_user', function (Blueprint $table) {
$table->id();
$table->unsignedMediumInteger('system_notification_id')->index();
$table->unsignedMediumInteger('user_id')->index();
$table->string('channel', 32)->default('toast')->index(); // toast | push | websocket | etc.
$table->boolean('is_read')->default(false)->index();
$table->timestamp('read_at')->nullable()->index();
$table->boolean('is_dismissed')->default(false)->index();
$table->timestamp('dismissed_at')->nullable()->index();
$table->boolean('is_confirmed')->default(false)->comment('Confirmación explícita si se requiere')->index();
$table->timestamp('confirmed_at')->nullable()->index();
$table->text('confirmation_notes')->nullable();
// Auditoria
$table->timestamps();
// Indices
$table->unique(['system_notification_id', 'user_id']);
// Relaciones
$table->foreign('system_notification_id')->references('id')->on('system_notifications')->cascadeOnDelete();
$table->foreign('user_id')->references('id')->on('users')->restrictOnDelete();
});
}
public function down(): void
{
Schema::dropIfExists('system_notification_user');
}
};

View File

@ -0,0 +1,59 @@
<?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(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->id();
$table->string('module')->nullable()->index(); // Modulo (opcional pero recomendable)
$table->unsignedMediumInteger('user_id')->index(); // Usuario
$table->string('channel', 32)->default('toast')->index(); // toast | push | websocket | etc.
$table->string('type', 16)->default('info')->index(); // Enum: info, success, danger, warning, system
$table->string('title')->index();
$table->text('body')->nullable();
$table->json('data')->nullable();
$table->string('action_url')->nullable();
$table->boolean('is_read')->default(false)->index();
$table->timestamp('read_at')->nullable();
// Auditoria
$table->timestamps();
$table->boolean('is_dismissed')->default(false)->index();
$table->boolean('is_deleted')->default(false)->index();
$table->unsignedMediumInteger('emitted_by')->nullable()->index();
$table->softDeletes();
// Indices
$table->index(['type', 'title']);
$table->index(['is_read', 'user_id']);
$table->index(['user_id', 'is_read', 'is_deleted']);
// Relaciones
$table->foreign('user_id')->references('id')->on('users')->restrictOnDelete();
$table->foreign('emitted_by')->references('id')->on('users')->restrictOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notifications');
}
};

View File

@ -0,0 +1,66 @@
<?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('module_packages', function (Blueprint $table) {
$table->smallIncrements('id');
$table->string('name')->unique();
$table->string('display_name');
$table->text('description')->nullable();
$table->jsonb('keywords')->nullable();
$table->string('author_name')->nullable();
$table->string('author_email')->nullable();
$table->string('source_url', 500)->nullable();
$table->string('composer_url', 500)->nullable();
$table->string('cover_image', 500)->nullable();
$table->string('readme_path', 500)->nullable();
$table->string('source_type', 16)->default(false); // Enum
$table->boolean('zip_available')->default(false);
$table->json('composer')->nullable(); // dump completo del composer.json
$table->string('repository_type', 16)->default('public')->index();
$table->boolean('active')->default(true);
// Auditoría
$table->unsignedMediumInteger('created_by')->nullable()->index();
$table->unsignedMediumInteger('updated_by')->nullable()->index();
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Indices
$table->index(['name', 'display_name']);
$table->index(['repository_type', 'active']);
// Relaciones
$table->foreign('created_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('deleted_by')->references('id')->on('users')->restrictOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Elimina tablas solo si existen
Schema::dropIfExists('module_packages');
}
};

View File

@ -0,0 +1,54 @@
<?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('installed_modules', function (Blueprint $table) {
$table->smallIncrements('id');
$table->unsignedSmallInteger('module_package_id')->index(); // Relación con module_packages
$table->string('slug')->unique(); // Ej: vuexy-website-admin
$table->string('name'); // Ej: koneko/laravel-vuexy-website-admin
$table->string('version')->nullable(); // Versión instalada
$table->string('install_path')->nullable(); // Path en vendor/ o custom
$table->boolean('enabled')->default(true); // Activado para el sistema
// Control
$table->json('install_options')->nullable(); // flags de instalación zip, git, etc.
// Auditoría y estado
$table->timestamp('installed_at')->nullable();
$table->timestamp('last_checked_at')->nullable(); // Sincronización con fuente
// Auditoría
$table->unsignedMediumInteger('created_by')->nullable()->index();
$table->unsignedMediumInteger('updated_by')->nullable()->index();
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Indices
$table->index(['module_package_id', 'enabled']);
// Relaciones
$table->foreign('module_package_id')->references('id')->on('module_packages')->onDelete('cascade');
$table->foreign('created_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('deleted_by')->references('id')->on('users')->restrictOnDelete();
});
}
public function down(): void
{
Schema::dropIfExists('installed_modules');
}
};

View File

@ -0,0 +1,57 @@
<?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('vuexy_modules', function (Blueprint $table) {
$table->string('slug')->primary(); // Ej. koneko-vuexy-contacts
$table->string('name'); // Nombre completo del módulo
$table->string('vendor')->nullable(); // koneko-st, vendor de composer
$table->unsignedSmallInteger('installed_module_id')->nullable()->index();
$table->string('version')->nullable(); // 1.0.0
$table->string('build_version')->nullable(); // 20250429223531
$table->string('type')->default('plugin'); // core, plugin, theme, etc.
$table->string('provider')->nullable();
$table->json('tags')->nullable();
$table->json('metadata')->nullable(); // json libre para UI, etc.
// Auditoria
$table->boolean('is_enabled')->default(true)->index(); // Para activar/desactivar
$table->boolean('is_installed')->default(false)->index(); // Para trackear instalación
$table->unsignedMediumInteger('created_by')->nullable()->index();
$table->unsignedMediumInteger('updated_by')->nullable()->index();
$table->unsignedMediumInteger('deleted_by')->nullable()->index();
$table->timestamps();
$table->softDeletes();
// Indices
$table->index(['is_enabled', 'is_installed']);
// Relaciones
$table->foreign('installed_module_id')->references('id')->on('installed_modules')->onDelete('set null');
$table->foreign('created_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('updated_by')->references('id')->on('users')->restrictOnDelete();
$table->foreign('deleted_by')->references('id')->on('users')->restrictOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Elimina tablas solo si existen
Schema::dropIfExists('vuexy_modules');
}
};

View File

@ -1,14 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Koneko\VuexyAdmin\Services\RBACService;
class PermissionSeeder extends Seeder
{
public function run()
{
RBACService::loadRolesAndPermissions();
}
}

View File

@ -1,97 +0,0 @@
<?php
namespace Database\Seeders;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Services\AvatarImageService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Define el disco y la carpeta
$disk = 'public';
$directory = 'profile-photos';
// Verifica si la carpeta existe
if (Storage::disk($disk)->exists($directory))
Storage::disk($disk)->deleteDirectory($directory);
//
$avatarImageService = new AvatarImageService();
// Super admin
$user = User::create([
'name' => 'Koneko Admin',
'email' => 'sadmin@koneko.mx',
'email_verified_at' => now(),
'password' => bcrypt('LAdmin123'),
'status' => User::STATUS_ENABLED,
])->assignRole('SuperAdmin');
// Actualizamos la foto
$avatarImageService->updateProfilePhoto($user, new UploadedFile(
'public/vendor/vuexy-admin/img/logo/koneko-02.png',
'koneko-02.png'
));
// admin
$user = User::create([
'name' => 'Admin',
'email' => 'admin@koneko.mx',
'email_verified_at' => now(),
'password' => bcrypt('LAdmin123'),
'status' => User::STATUS_ENABLED,
])->assignRole('Admin');
$avatarImageService->updateProfilePhoto($user, new UploadedFile(
'public/vendor/vuexy-admin/img/logo/koneko-03.png',
'koneko-03.png'
));
// Auditor
$user = User::create([
'name' => 'Auditor',
'email' => 'auditor@koneko.mx',
'email_verified_at' => now(),
'password' => bcrypt('LAdmin123'),
'status' => User::STATUS_ENABLED,
])->assignRole('Auditor');
$avatarImageService->updateProfilePhoto($user, new UploadedFile(
'public/vendor/vuexy-admin/img/logo/koneko-03.png',
'koneko-03.png'
));
// Usuarios CSV
$csvFile = fopen(base_path("database/data/users.csv"), "r");
$firstline = true;
while (($data = fgetcsv($csvFile, 2000, ",")) !== FALSE) {
if (!$firstline) {
User::create([
'name' => $data['0'],
'email' => $data['1'],
'email_verified_at' => now(),
'password' => bcrypt($data['3']),
'status' => User::STATUS_ENABLED,
])->assignRole($data['2']);
}
$firstline = false;
}
fclose($csvFile);
}
}

68
docs/<!DOCTYPE html>.html Normal file
View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="{{ dynamic_lang }}" prefix="og: http://ogp.me/ns#">
<head>
<!-- Metadatos Esenciales -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Title Tag Optimizado -->
<title>{{ page_title }} | {{ site_name }}</title>
<!-- Meta Description Dinámica -->
<meta name="description" content="{{ meta_description|truncate:160 }}">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="{{ og_type }}">
<meta property="og:url" content="{{ canonical_url }}">
<meta property="og:title" content="{{ og_title|truncate:60 }}">
<meta property="og:description" content="{{ og_description|truncate:160 }}">
<meta property="og:image" content="{{ og_image_url }}">
<meta property="og:site_name" content="{{ site_name }}">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:creator" content="{{ twitter_handle }}">
<!-- ... otros meta twitter ... -->
<!-- Robots Directives -->
<meta name="robots" content="{{ index_status }}, {{ follow_status }}, max-image-preview:large">
<!-- Idioma y Geolocalización -->
<meta name="language" content="{{ main_language }}">
<meta name="geo.region" content="{{ geo_region }}">
<meta name="geo.placename" content="{{ geo_placename }}">
<!-- Enlaces Canónicos y Alternativos -->
<link rel="canonical" href="{{ canonical_url }}">
{% for alternate in alternate_langs %}
<link rel="alternate" hreflang="{{ alternate.code }}" href="{{ alternate.url }}">
{% endfor %}
<!-- Preconexiones y Preloads -->
<link rel="preconnect" href="https://www.googletagmanager.com">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<!-- Favicon Moderno (SVG + PNG fallback) -->
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<!-- CSS Crítico Inline -->
<style>/* CSS mínimo para above-the-fold */</style>
<!-- Schema.org JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "{{ site_name }}",
"url": "{{ base_url }}",
"potentialAction": {
"@type": "SearchAction",
"target": "{{ search_url }}?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
</head>
<body itemscope itemtype="http://schema.org/WebPage">

View File

@ -0,0 +1,117 @@
# Koneko ERP - Cache Helper Guide
> ✨ Esta guía detalla el uso correcto del sistema de cache en Koneko ERP, basado completamente en el helper `cache_manager()`, sin necesidad de interactuar con las clases internas como `KonekoCacheManager` o `LaravelCacheManager`.
---
## 🔎 Filosofía
* Toda interacción de componentes con el sistema de cache debe realizarse exclusivamente mediante el helper `cache_manager()`.
* Las clases internas son consideradas **@internal**, y no deben ser accedidas directamente por desarrolladores de componentes.
* El sistema permite configuración jerárquica basada en namespace del componente, grupo lógico de datos y claves individuales.
---
## 🔍 Sintaxis del Helper
```php
cache_manager(string $component = 'admin', string $group = 'cache')
```
Retorna una instancia segura del gestor para el componente y grupo indicados. Ejemplos:
```php
cache_manager('admin', 'avatar')->enabled();
cache_manager('website', 'menu')->ttl();
cache_manager('website', 'html')->flush();
```
---
## ⚖️ Jerarquía de Configuración
Las operaciones de cache respetan la siguiente jerarquía al determinar configuraciones:
1. `koneko.{componente}.{grupo}.ttl` / `enabled`
2. `koneko.{componente}.cache.ttl` / `enabled`
3. `koneko.cache.ttl` / `enabled`
Esto permite granularidad sin perder coherencia global.
---
## 📃 Métodos Disponibles
| Método | Descripción |
| ------------------------ | ------------------------------------------------------------- |
| `key(string $suffix)` | Genera clave de cache completa con prefijos |
| `config($key, $default)` | Accede a configuraciones con prefijo aplicado |
| `ttl()` | TTL efectivo (en segundos) |
| `enabled()` | Determina si el cache está habilitado para el contexto actual |
| `flush()` | Limpia el grupo de cache completo si el driver lo permite |
| `driver()` | Devuelve el driver de cache actual |
| `shouldDebug()` | Evalúa si se encuentra en modo debug para cache |
| `registerDefaults()` | Registra valores default para ttl y enabled si no existen |
---
## 🚀 Ejemplos de Uso
### Validar TTL efectivo
```php
$ttl = cache_manager('website', 'menu')->ttl();
```
### Verificar si está habilitado
```php
if (cache_manager('admin', 'avatar')->enabled()) {
// Proceder con cache
}
```
### Limpiar cache con soporte para etiquetas
```php
cache_manager('website', 'html')->flush();
```
---
## ⚠️ Buenas Prácticas
* **No** uses directamente la clase `KonekoCacheManager`.
* **No** accedas directamente al sistema `Cache::` sin pasar por el helper.
* **No** asumas estructura de clave, usa `->key()`.
* **Sí** configura `koneko.{componente}.{grupo}.ttl` en tu archivo `config()` si tu componente lo necesita.
---
## 🔍 Debug y Diagnóstico
Ejecuta:
```bash
php artisan koneko:cache --component=website --group=html --ttl
php artisan koneko:cache --component=admin --group=avatar --flush
```
El comando mostrará información relevante para depuración sin exponer clases internas.
---
## 🚧 Futura Expansión
* Registro de tags
* TTLs dinámicos
* Integración con eventos y auditoría
* Modo "read-only" para ambientes cacheados
---
## 🌟 Conclusión
Este sistema garantiza modularidad, extensibilidad y seguridad. El helper `cache_manager()` es la única puerta de entrada para desarrolladores y debe usarse exclusivamente para mantener la integridad del ecosistema.
> ✅ Si necesitas agregar un nuevo grupo de cache, simplemente define su configuración y comienza a usar el helper, sin necesidad de modificar clases o contratos.

View File

@ -0,0 +1,68 @@
# Koneko ERP - Component Access & Exposure Policy
Este documento define las reglas de exposición y acceso para los servicios técnicos clave dentro del ecosistema **Koneko ERP**. Establece los principios para mantener una arquitectura limpia, segura y coherente, facilitando su extensión y documentación.
---
## ✅ Componentes y su Exposición
| Clase Técnica | Expuesta al Usuario | Acceso Recomendado | Notas |
| --------------------------------- | ------------------- | -------------------------- | ---------------------------------------------------------------- |
| `KonekoSettingManager` | ❌ No | `settings()` helper | Configuración modular con soporte de namespaces. |
| `KonekoCacheManager` | ❌ No | `cache_manager()` helper | Acceso al sistema de cache multi-driver y con TTL configurables. |
| `KonekoSystemLogger` | ❌ No | `log_system()` helper | Logger morfable con niveles y contexto extendido. |
| `KonekoSecurityLogger` | ❌ No | `log_security()` helper | Eventos de seguridad con GeoIP y proxy detection. |
| `KonekoUserInteractionLogger` | ❌ No | `log_interaction()` helper | Auditoría de componentes y acciones sensibles. |
| `KonekoComponentContextRegistrar` | ❌ No | ❄ Solo Bootstrap | Registra el `namespace` del componente y su `slug` actual. |
---
## ⚖️ Principios de Acceso
* **Acceso exclusivo por helpers**: Los helpers representan la única interfaz pública soportada para operaciones de configuración, cache y logs.
* **Encapsulamiento interno**: Ningún componente debería invocar directamente servicios como `KonekoSettingManager` o `KonekoSystemLogger`.
* **UI y CLI desacoplados**: Tanto los comandos artisan como el panel administrativo utilizan los helpers, nunca instancias directas.
---
## 💡 Buenas prácticas para desarrolladores
* Usa `settings()` para acceder o escribir configuraciones modulares.
* Usa `cache_manager()` para obtener TTL, flush o debug por componente.
* Usa `log_system()` para registrar eventos de sistema de forma morfable.
* Usa `log_security()` para eventos como logins fallidos o IP sospechosas.
* Usa `log_interaction()` para acciones en Livewire, eventos UI o tracking avanzado.
---
## 🔹 Ejemplo práctico
```php
// Correcto
settings()->in('website')->get('general.site_name');
cache_manager('admin', 'menu')->ttl();
log_system('info', 'Menú regenerado');
```
```php
// Incorrecto ❌
app(KonekoSettingManager::class)->get(...);
new KonekoCacheManager(...);
(new KonekoSystemLogger)->log(...);
```
---
## 📃 Futuras extensiones
* Se agregará soporte en este esquema para:
* APIManager: `api_manager()`
* Catalogs: `catalog()`
* Event Dispatchers: `event_dispatcher()`
Cada uno seguirá el mismo patrón: clase técnica interna, helper de acceso documentado, y uso controlado por contexto registrado.
---
> Este documento forma parte del conjunto `docs/architecture/components/`. Asegúrese de mantenerlo actualizado ante cualquier refactor o nuevo helper público.

View File

@ -0,0 +1,120 @@
# Koneko Security Logger Helper Guide
> ⚖️ Auditoría de eventos de seguridad en tiempo real y bajo demanda.
El logger `log_security()` forma parte de la infraestructura central de seguridad de Koneko Vuexy Admin y te permite registrar eventos sensibles como accesos, fallos de login, intentos sospechosos, detecciones de VPN, entre otros.
---
## ✨ Ventajas
* No requiere instanciar clases.
* Autoobtiene `Request`, `IP`, `UserAgent`, `Geolocalización`, etc.
* Flexible para ejecutarse desde controladores, middleware, jobs o Livewire.
* Compatible con `SecurityEvent` (modelo auditado).
* Almacena contexto extendido con `payload`, `is_proxy`, `user_id`, etc.
---
## ✨ Uso básico
```php
log_security('login_failed');
```
Esto registra un fallo de login con todos los metadatos de seguridad: IP, ciudad, dispositivo, agente, URL y más.
---
## ⚖️ Sintaxis Completa
```php
log_security(
string $type, // Tipo de evento (ej: login_failed, login_success)
?Request $request = null, // Puede inyectarse manualmente
?int $userId = null, // Usuario relacionado (opcional)
array $payload = [], // Datos adicionales como intentos, cabeceras, etc.
bool $isProxy = false // ¿Fue detectado uso de proxy/VPN?
);
```
Ejemplo completo:
```php
log_security(
'login_failed',
request(),
$user?->id,
['intentos' => 3, 'via' => 'formulario'],
detectaVpnProxy(request()->ip())
);
```
---
## 🔍 Eventos soportados por defecto
Puedes definirlos como constantes estáticas en tu código:
```php
SecurityEvent::EVENT_LOGIN_FAILED
SecurityEvent::EVENT_LOGIN_SUCCESS
```
O usar strings arbitrarios con sentido:
```php
'password_reset_attempt'
'blocked_login_from_blacklist'
'csrf_token_fail'
'geolocation_warning'
```
---
## ⌚ Modelo generado: `SecurityEvent`
El evento registrado se almacena en la tabla `security_events`, con campos como:
* `ip_address`
* `user_id`
* `event_type`
* `payload`
* `region`, `city`, `country`
* `is_proxy`, `device_type`, etc.
---
## 🌐 Geolocalización Automática
Si tienes habilitado el trait `HasGeolocation`, el sistema hace *GeoIP Lookup* por IP:
```php
use Koneko\VuexyAdmin\Support\Traits\Helpers\HasGeolocation;
```
---
## 🔐 Buenas prácticas
* Usa tipos semánticos: `login_success`, `vpn_blocked`, `csrf_fail`, etc.
* Agrega contexto extra en `payload` para debugging posterior.
* Detecta IPs sospechosas con `isProxy` (ideal para usar junto con sistemas de listas negras).
---
## ✨ Extras
Puedes extender la lógica de logging desde:
```php
Koneko\VuexyAdmin\Support\Logger\KonekoSecurityLogger
```
Este servicio puede adaptarse o sustituirse por otro para logs distribuidos o externos (ej: Sentry, Elastic, etc).
---
## ✅ Conclusión
El helper `log_security()` es una herramienta robusta, flexible y elegante para capturar eventos sensibles en tu ERP. Evita trabajar directamente con el modelo, mantén la consistencia de tu registro, y delega la trazabilidad a una capa preparada para el contexto empresarial.

View File

@ -0,0 +1,161 @@
# Koneko Settings Helper Guide
Guía oficial de uso para `settings()` dentro del ecosistema Koneko ERP.
---
## 📁 Filosofía General
KonekoSettingManager es una clase interna (@internal) encargada de gestionar configuraciones de componentes y módulos del ecosistema de forma centralizada, modular y cacheable. No debe ser usada directamente.
### Acceso siempre mediante el helper global:
```php
settings()->get('clave');
settings()->set('clave', 'valor');
```
---
## ⚖️ Jerarquía de Namespace (Prioridad de Resolución)
```text
componentNamespace (ej: admin, website)
└️ module_slug (ej: avatar, seo, menu)
└️ key
```
Ejemplo real:
```text
koneko.admin.avatar.cache.ttl
```
---
## ✨ Métodos Disponibles
### `self(?string $subNamespace = null)`
Se vincula automáticamente al componente activo según el contexto del módulo actual.
```php
settings()->self()->get('cache.ttl');
```
### `in(string $namespace)`
Permite establecer un namespace manual.
```php
settings()->in('admin.avatar')->get('enabled');
```
### `get(string $key, ...$args)`
Obtiene el valor de un setting.
```php
$ttl = settings()->get('cache.ttl');
```
### `set(string $key, mixed $value, ?int $userId = null)`
Guarda o actualiza un setting.
```php
settings()->set('menu.visible', true);
```
### `currentNamespace()`
Devuelve el namespace actual del contexto.
### `listGroups()`
Devuelve una colección de todos los grupos de configuración disponibles en el componente.
---
## 🚀 Comportamiento Avanzado
### ❄️ Cache Automática
* Al consultar un setting se usa el `KonekoCacheManager` de fondo si está habilitado.
* Puedes controlar el TTL y activación por:
```php
koneko.cache.enabled
koneko.admin.cache.ttl
koneko.admin.avatar.cache.enabled
```
### ⚖️ Tipos automáticos
* Si el valor del setting es JSON, se decodifica automáticamente.
### 🔨 Almacenamiento estructurado
Los datos se almacenan en la base de datos en la tabla `settings`:
* `namespace`
* `key`
* `value`
* `user_id`
---
## 🔧 Casos de Uso Comunes
```php
// 1. TTL de avatar (modo simple)
$ttl = settings()->in('admin.avatar')->get('cache.ttl');
// 2. Cambiar visibilidad del menú del website
settings()->in('website.menu')->set('visible', true);
// 3. Desde un componente que ya registró su namespace:
settings()->get('enabled');
// 4. En test con namespace explícito:
settings()->in('admin.seo')->set('json_ld.enabled', true);
```
---
## ⚡ Recomendaciones
* Nunca accedas directamente a `KonekoSettingManager`
* Usa `settings()` siempre que necesites configuración
* El namespace se debe registrar automáticamente con `KonekoComponentContextRegistrar`
* Puedes definir valores por `.env` o `config()` que serán sobreescritos si existen en la base de datos
---
## 🚩 Diagnóstico Rápido
```php
settings()->in('website.menu')->get('debug');
settings()->in('admin.avatar')->currentNamespace();
```
---
## ✨ Integración UI
Todos los valores son consultables y modificables desde:
* Panel de administrador
* Vistas Livewire
* Componentes Vue o Blade mediante API
---
## 🙏 Agradecimientos
Sistema inspirado por la necesidad de centralizar configuraciones por módulo en entornos escalables y cacheables. Diseñado con amor para el ERP Koneko Vuexy Admin México.
---
\#❤️ ¿Aportaciones?
Si tienes sugerencias, no dudes en compartirlas en el repositorio oficial Koneko.

View File

@ -0,0 +1,126 @@
# Koneko System Logger Helper Guide
El sistema de logs de sistema en Koneko ERP permite registrar eventos importantes de forma estructurada, segura y extensible. A través del helper `log_system()` se abstrae la lógica de registro para facilitar su uso sin exponer la clase subyacente `KonekoSystemLogger`.
## ✅ ¿Cuándo usar `log_system()`?
Este helper debe utilizarse para registrar:
- Operaciones del sistema (módulos, configuración, instalación de paquetes)
- Procesos técnicos (errores, warnings, notificaciones internas)
- Eventos relevantes relacionados con lógica de negocio o flujos administrativos
---
## 📦 Helper: `log_system()`
```php
log_system(
string|LogLevel $level,
string $message,
array $context = [],
?Model $related = null
): SystemLog
```
### Parámetros
| Nombre | Tipo | Descripción |
|--------------|----------------------------------|-------------|
| `$level` | `string\|LogLevel` | Nivel del log (`info`, `warning`, `error`, `critical`, etc.) |
| `$message` | `string` | Mensaje a registrar |
| `$context` | `array` | Datos adicionales relevantes al evento |
| `$related` | `Model|null` | Modelo relacionado (opcional, se guarda en `related_model`) |
---
## 🎯 Ejemplos de uso
### Log simple con nivel:
```php
log_system('info', 'Inicio del proceso de sincronización');
```
### Log con contexto personalizado:
```php
log_system('error', 'Error al procesar factura CFDI', [
'cfdi_id' => 3345,
'error' => $exception->getMessage(),
]);
```
### Log vinculado a un modelo:
```php
log_system(
'warning',
'El producto fue modificado manualmente',
['user' => auth()->id()],
$product
);
```
---
## 🧱 Internamente...
- Se utiliza el modelo `Koneko\VuexyAdmin\Models\SystemLog`
- Soporta `morphTo()` para asociar cualquier modelo relacionado (via `related_model`)
- Se castea `level` como Enum `LogLevel`
- Se incluye automáticamente el componente si está registrado vía `KonekoComponentContextRegistrar`
---
## 🛡️ Buenas prácticas
- Usa niveles correctos (`info`, `debug`, `warning`, `error`, `critical`) según la gravedad
- Agrega siempre contexto útil que facilite auditoría
- Usa `related` cuando el evento está directamente vinculado a un modelo (como un `Pedido`, `Producto`, etc.)
- Si estás en un módulo registrado, el helper asocia automáticamente el `componentNamespace`
---
## 🔐 Soporte a auditoría
`log_system()` es parte fundamental del sistema de trazabilidad técnica del ERP. Todos los registros quedan disponibles para consulta por el módulo de Auditoría o Seguridad Avanzada si está habilitado.
---
## 📍 Registro automático de módulo
Si el componente actual fue registrado mediante:
```php
KonekoComponentContextRegistrar::registerComponent('admin');
```
El log quedará asociado a `module = 'admin'`, sin necesidad de especificarlo manualmente.
---
## 📚 Relación con otros loggers
| Helper | Propósito |
|---------------------|-----------------------------------|
| `log_system()` | Logs técnicos y operativos |
| `log_security()` | Eventos de seguridad (auth, IP) |
| `log_interaction()` | Interacciones del usuario final |
---
## 🧪 Testing y ambiente local
En `local` o `staging`, es común agregar logs temporales para diagnóstico:
```php
log_system('debug', 'Revisando flujo de pago', ['step' => 3]);
```
Recuerda que estos deben eliminarse o ajustarse a `info` en producción.
---
## 🧭 Ubicación del modelo
```php
Koneko\VuexyAdmin\Models\SystemLog
```
Puedes extender la funcionalidad desde el modelo si se requiere una visualización especial para auditoría o tablas administrativas.
---
> Este helper está diseñado para desarrolladores del ecosistema Koneko. Evita el uso directo de `KonekoSystemLogger` salvo en integraciones muy especializadas.

View File

@ -0,0 +1,110 @@
# Koneko User Interaction Logger Helper Guide
El sistema de interacciones de usuario de Koneko permite registrar acciones relevantes dentro de la interfaz de usuario (UI) o backend, con fines de auditoría, trazabilidad, y seguridad.
> Este mecanismo está diseñado para ser invocado desde componentes Livewire, controladores o procesos sensibles que desees monitorear.
---
### ✅ Uso recomendado
Usa el helper `log_interaction()` para registrar cualquier acción de interacción importante:
```php
log_interaction('update_profile', [
'changes' => $changes,
]);
```
Puedes especificar el nivel de seguridad de la acción:
```php
log_interaction('delete_user', [
'user_id' => $user->id,
], 'high');
```
Si estás dentro de Livewire:
```php
log_interaction('submit_form', [], 'normal', 'components.users.create-user');
```
---
### 💡 Sintaxis completa
```php
log_interaction(
string $action,
array $context = [],
InteractionSecurityLevel|string $security = 'normal',
?string $livewireComponent = null
): ?UserInteraction
```
| Parámetro | Descripción |
|---------------------|------------------------------------------------------------------------------|
| `$action` | Nombre de la acción realizada, ejemplo: `create_order`, `login_success` |
| `$context` | Arreglo con datos adicionales relevantes. Puede incluir cambios, payloads...|
| `$security` | Nivel de seguridad: `low`, `normal`, `high`, `critical` |
| `$livewireComponent`| Ruta del componente Livewire, si aplica |
---
### 🔍 Ejemplo realista
```php
log_interaction(
action: 'update_user_permissions',
context: [
'user_id' => $user->id,
'new_roles' => $roles,
],
security: 'high',
livewireComponent: 'components.admin.users.user-edit-panel'
);
```
---
### 📃 Sobre el modelo `UserInteraction`
No necesitas instanciar ni importar la clase `UserInteraction` para registrar interacciones. Sin embargo, si deseas auditar desde base de datos o hacer queries personalizados, puedes usar scopes como:
```php
UserInteraction::byModule('admin')->latest()->get();
```
---
### 🔧 Extendibilidad
Puedes extender `KonekoUserInteractionLogger` para registrar interacciones con fuentes externas, agregar metadata, o enviar notificaciones.
---
### ❌ Buenas prácticas a evitar
- No usar `UserInteraction::create(...)` manualmente.
- No cambiar campos protegidos (`action`, `security_level`, etc.) después del registro.
- No registrar acciones irrelevantes o sin contexto claro.
---
### 📆 Logs relacionados
- `log_system()` — para eventos del sistema.
- `log_security()` — para eventos de seguridad como accesos o bloqueos.
---
### 🚀 Módulos compatibles
Este sistema está disponible para todos los componentes que registren `componentNamespace` en su `KonekoModule.php`, permitiendo trazabilidad completa por módulo.
---
### 🌟 Tip final
Agrégalo a tus componentes Livewire clave para auditar formularios, acciones de administración, y cambios críticos del sistema.

30
docs/factory/index.md Normal file
View File

@ -0,0 +1,30 @@
# 🏭 Koneko ERP - Factory Design Guide
Este documento describe cómo crear y extender `Factories` para modelos en el ecosistema de Koneko ERP.
## 🎯 Objetivo
Facilitar la generación de datos de prueba y semilla utilizando una estructura clara, coherente y extensible para todos los modelos del sistema.
## 🧱 Clase Base: `AbstractModelFactory`
```php
namespace Koneko\VuexyAdmin\Support\Factories;
abstract class AbstractModelFactory extends Factory
{
// Inyecta Faker automáticamente
// Ofrece métodos auxiliares como maybe()
}
```
## 🧬 Traits útiles
- `HasFactorySupport`: `maybe($probabilidad, $valor)`
- `HasContactFakeData`: CURP, RFC, teléfono
- `HasFactoryEnumSupport`: Soporte aleatorio de Enums
- `HasDynamicFactoryExtenders`: para métodos como `definitionXyz()`
## 🚘 Ejemplo: Factory para `Vehicle`
Ver archivo: `VehicleFactory.php`

View File

@ -0,0 +1,181 @@
# Guía de Convenciones para `database/data/` en Koneko Vuexy Admin/ERP
> Esta guía está diseñada para ayudar a desarrolladores a organizar, entender y aprovechar correctamente la estructura de carpetas `database/data/*` en los módulos del ecosistema **Koneko Vuexy Admin/ERP**. Aplica tanto para datos de prueba como para catálogos oficiales, importaciones, plantillas y volcados.
---
## 📂 Estructura base sugerida
Cada componente o módulo deberá tener su propia carpeta bajo `database/data/`, utilizando el nombre del paquete o alias del módulo. Por ejemplo:
```bash
database/data/vuexy-contacts/
database/data/vuexy-sat-catalogs/
database/data/vuexy-ecommerce/
database/data/vuexy-core/
```
---
## 📄 Tipos de carpetas internas
A continuación se detallan los tipos de subcarpetas recomendadas dentro de cada componente:
### 1. `seeders/`
Contiene archivos de datos utilizados para poblar la base de datos con registros reales o iniciales.
* Formatos: `.json`, `.csv`, `.php`
* Uso: Comandos como `php artisan db:seed` o `SeederOrchestrator`
**Ejemplos:**
```bash
database/data/vuexy-contacts/seeders/users.json
database/data/vuexy-ecommerce/seeders/products.csv
```
---
### 2. `fixtures/`
Datos temporales o de prueba. Generalmente usados para QA, testeo, demostraciones o sandbox.
**Ejemplos:**
```bash
database/data/vuexy-ecommerce/fixtures/demo-products.csv
database/data/vuexy-core/fixtures/test-users.json
```
---
### 3. `catalogs/`
Incluye catálogos oficiales o externos (como los del SAT, INEGI, ISO, etc.) ya convertidos a `.csv` o `.json`, listos para importar.
**Ejemplos:**
```bash
database/data/vuexy-sat-catalogs/catalogs/c_RegimenFiscal.csv
database/data/vuexy-sat-catalogs/catalogs/c_UsoCFDI.csv
```
---
### 4. `samples/` o `templates/`
Plantillas de importación para administradores o usuarios finales. Ayudan a estructurar correctamente los datos antes de importar.
**Ejemplos:**
```bash
database/data/vuexy-website-admin/samples/template_users.csv
database/data/vuexy-ecommerce/samples/sample_product.json
```
---
### 5. `dumps/`
Volcados de base de datos completos o parciales, usados como backups, sincronización o testing. No deben usarse en producción directamente.
**Ejemplos:**
```bash
database/data/vuexy-core/dumps/db-export.json
database/data/vuexy-ecommerce/dumps/partial-products.sql
```
---
### 6. `sources/`
Archivos fuente originales que requieren procesamiento, como `.xlsx` del SAT o archivos crudos.
**Ejemplos:**
```bash
database/data/vuexy-sat-catalogs/sources/SAT_Catalogos_2024.xlsx
database/data/vuexy-geo/sources/inegi_cp_2020.xlsx
```
---
### 7. `mappings/`
Mapas de conversión, reglas de transformación, diccionarios de campos, etc. Muy útiles cuando se procesan archivos externos.
**Ejemplos:**
```bash
database/data/vuexy-sat-catalogs/mappings/column_map.json
database/data/vuexy-ecommerce/mappings/type_rules.json
```
---
### 8. `commands/` (opcional)
Scripts de ejemplo para parseo, importación o generación de datos. No se ejecutan automáticamente, son de referencia.
**Ejemplo:**
```bash
database/data/vuexy-sat-catalogs/commands/parse_sat_catalog.php
```
---
## 📊 Publicación de archivos desde Providers
Los `ServiceProvider` de cada módulo pueden publicar estos archivos para que el desarrollador los copie al proyecto:
```php
'publishedFiles' => [
'seeders' => [
'database/data/vuexy-contacts/seeders' => base_path('database/data/vuexy-contacts/seeders'),
],
'catalogs' => [
'database/data/vuexy-sat-catalogs/catalogs' => base_path('database/data/vuexy-sat-catalogs/catalogs'),
],
],
```
---
## ✨ Buenas prácticas
* Usa nombres descriptivos: `products_v2.json`, `c_UsoCFDI_2024.csv`
* No sobreescribas datos productivos desde estas carpetas.
* Prefiere `json` para estructuras complejas, `csv` para tabulares simples.
* Siempre separa datos de prueba (`fixtures`) de datos de producción (`seeders`).
* Versiona si es necesario: `v1/`, `v2/`.
---
## ❓ FAQ
**¿Puedo usar archivos de `samples/` como plantilla para importadores Livewire?**
> Sí, están pensados como guías para el usuario final.
**¿Debo publicar todos los archivos por defecto?**
> No. Publica solo los que consideres necesarios para instalación o personalización.
**¿Puedo incluir múltiples carpetas del mismo tipo?**
> Sí. Puedes tener `catalogs/v3/` y `catalogs/v4/` si soportas distintas versiones.
---
## 🚀 Extensiones futuras
* Agregar validación automática de estructuras CSV.
* Sistema de visualización para cada tipo de archivo desde el admin.
* Comandos de limpieza automática (`vuexy:data:cleanup`)
---
¡Esta convención mejora la modularidad, facilita el mantenimiento y hace tu ERP más robusto y predecible!

View File

@ -0,0 +1,54 @@
# Notificaciones en Koneko ERP
Este documento describe cómo utilizar el sistema de notificaciones centralizado mediante `NotifyChannelService` en el ecosistema Koneko ERP.
## 🧩 Drivers disponibles
- **Toastr** (predeterminado, clásico)
- **Notyf** (UX moderna, ligera)
- **Toastify** (notificaciones mínimas flotantes)
- **SweetAlert2** (modales avanzados, carga bajo demanda)
- **PNotify** (notificaciones avanzadas y persistentes, carga bajo demanda)
- **Custom** (ejecuta `window.VuexyNotify(payload)` si está definido)
## 📦 Uso básico
Puedes disparar una notificación global desde cualquier JS:
```js
import { NotifyChannelService } from '@/vuexy/notifications/notify-channel-service';
NotifyChannelService.notify({
type: 'success',
message: 'Usuario guardado correctamente',
title: 'Éxito',
channel: 'toastr' // 'notyf', 'toastify', 'sweetalert', 'pnotify', 'custom'
});
```
## 🌐 Desde Livewire
Puedes emitir un evento desde tu componente PHP:
```php
$this->dispatchBrowserEvent('notify', [
'type' => 'success',
'message' => 'Operación exitosa',
'channel' => 'toastify'
]);
```
Y en tu JS global:
```js
document.addEventListener('notify', (event) => {
window.NotifyChannelService.notify(event.detail);
});
```
## 📄 Recomendaciones
- **Toastr**, **Toastify** y **Notyf** pueden convivir cargados.
- **SweetAlert2** y **PNotify** se cargan dinámicamente.
- Configura el canal por defecto desde `settings('core.notify.driver')` si lo deseas.

32
docs/seeder/config.md Normal file
View File

@ -0,0 +1,32 @@
# ⚙️ Configuración del sistema de seeders
El archivo `config/seeder.php` permite controlar el comportamiento global de todos los módulos.
## 🔑 Opciones principales
| Opción | Tipo | Descripción |
|----------------|---------|-------------|
| `env` | string | Entorno actual (local, demo, etc.) |
| `modules` | array | Lista de módulos con su configuración individual |
| `defaults` | array | Valores por defecto para todos los seeders |
| `clear_assets` | array | Define qué carpetas se borran antes de iniciar |
## 🧩 Configuración por módulo
Cada módulo acepta:
```php
'users' => [
'enabled' => true,
'seeder' => UserSeeder::class,
'file' => 'users.json',
'depends_on' => ['permissions'],
'truncate' => true,
'faker_only' => false,
'fake' => [
'min' => 5,
'max' => 100,
'images' => [...]
],
],
```

View File

@ -0,0 +1,33 @@
# 🧱 Cómo crear un nuevo Seeder compatible
## 1. Crear el Seeder
```php
class ProductoSeeder extends AbstractDataSeeder
{
use HasSeederFactorySupport;
use HandlesFileSeeders;
public function run(array $config = []): void
{
$this->seedFromJson('productos.json');
}
public function runFake(int $total, array $config = []): void
{
Producto::factory()->count($total)->create();
}
}
```
## 2. Registrar en `config/seeder.php`
```php
'products' => [
'enabled' => true,
'seeder' => ProductoSeeder::class,
'file' => 'products.json',
'faker_only' => false,
'fake' => ['min' => 5, 'max' => 100],
],
```

10
docs/seeder/index.md Normal file
View File

@ -0,0 +1,10 @@
# 🌱 Koneko ERP - Sistema de Seeders Modular
Este sistema permite poblar la base de datos del ERP de forma controlada, desde archivos CSV/JSON o mediante generación con Faker, incluyendo soporte para:
- Seeders con dependencias (`depends_on`)
- Modo `dry-run` (simulación sin afectar BD)
- Reportes en formato Markdown y JSON
- Limpieza automática de assets (ej. avatares)
- Soporte para múltiples entornos (`SEEDER_ENV`)
- Generación de imágenes y datos falsos

30
docs/seeder/usage.md Normal file
View File

@ -0,0 +1,30 @@
# 🚀 Cómo ejecutar los seeders
## 🧪 Modo normal
```bash
php artisan migrate:fresh --seed
```
## 🎯 Ejecutar módulo específico
```bash
php artisan db:seed --class=DatabaseSeeder --only=users
```
## 🧹 Activar limpieza de assets
Controlado por `config/seeder.php > clear_assets`.
## 🧼 Dry run
```php
$orchestrator->run(only: 'users', options: ['dry-run' => true]);
```
## 📁 Reportes
Se generan automáticamente:
- `database/seeders/reports/local/seed-report-*.md`
- `database/seeders/reports/local/seed-report-*.json`

0
docs/structure.md Normal file
View File

View File

@ -1,16 +1,15 @@
import '../../vendor/libs/bootstrap-table/bootstrap-table';
import '../notifications/LivewireNotification';
import '@vuexy-admin/assets/vendor/libs/bootstrap-table/bootstrap-table';
class BootstrapTableManager {
constructor(bootstrapTableWrap, config = {}) {
const defaultConfig = {
header: [],
format: [],
formatters: [],
search_columns: [],
actionColumn: false,
height: 'auto',
minHeight: 300,
bottomMargin : 195,
bottomMargin : 35,
search: true,
showColumns: true,
showColumnsToggleAll: true,
@ -18,7 +17,7 @@ class BootstrapTableManager {
exportfileName: 'datatTable',
exportWithDatetime: true,
showFullscreen: true,
showPaginationSwitch: true,
showPaginationSwitch: false,
showRefresh: true,
showToggle: true,
/*
@ -49,7 +48,7 @@ class BootstrapTableManager {
pageList: [25, 50, 100, 500, 1000],
sortName: 'id',
sortOrder: 'asc',
cookie: false,
cookie: true,
cookieExpire: '365d',
cookieIdTable: 'myTableCookies', // Nombre único para las cookies de la tabla
cookieStorage: 'localStorage',
@ -77,7 +76,10 @@ class BootstrapTableManager {
* Calcula la altura de la tabla.
*/
getTableHeight() {
const btHeight = window.innerHeight - this.$toolbar.height() - this.bottomMargin;
let container = document.querySelector('.container-p-y'),
toolbat = document.querySelector('.bt-toolbar');
let btHeight = container?.offsetHeight - toolbat?.offsetHeight - this.config.bottomMargin;
return btHeight < this.config.minHeight ? this.config.minHeight : btHeight;
}
@ -106,8 +108,7 @@ class BootstrapTableManager {
* Carga los formatters dinámicamente
*/
async loadFormatters() {
//const formattersModules = import.meta.glob('../../../../../**/resources/assets/js/bootstrap-table/*Formatters.js');
const formattersModules = import.meta.glob('/vendor/koneko/laravel-vuexy-admin/resources/assets/js/bootstrap-table/*Formatters.js');
const formattersModules = import.meta.glob('/vendor/koneko/**/resources/assets/js/bootstrap-table/*Formatters.js');
const formatterPromises = Object.entries(formattersModules).map(async ([path, importer]) => {
const module = await importer();
@ -121,7 +122,7 @@ class BootstrapTableManager {
const columns = [];
Object.entries(this.config.header).forEach(([key, value]) => {
const columnFormat = this.config.format[key] || {};
const columnFormat = this.config.formatters[key] || {};
if (typeof columnFormat.formatter === 'object') {
const formatterName = columnFormat.formatter.name;

View File

@ -29,6 +29,13 @@ export const booleanStatusCatalog = {
trueClass: 'text-green-800',
falseClass: '',
},
simpleCheck: {
trueText: '',
trueIcon: 'ti ti-check',
falseIcon: '',
trueClass: 'text-green-900',
falseText: '',
},
checkbox: {
trueIcon: 'ti ti-checkbox',
falseIcon: '',
@ -130,3 +137,20 @@ export const statusIntBadgeBgCatalog = {
12: 'Cancelado',
};
/**
* 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.
*/
export const getAvatarUrl = (fullName, profilePhoto) => {
const baseUrl = window.baseUrl || '';
if (profilePhoto) {
return `${baseUrl}storage/profile-photos/${profilePhoto}`;
}
const name = fullName.replace(/\s+/g, '-').toLowerCase();
return `${baseUrl}admin/usuario/avatar/?name=${fullName}`;
}

View File

@ -1,5 +1,4 @@
import { booleanStatusCatalog, statusIntBadgeBgCatalogCss, statusIntBadgeBgCatalog } from './globalConfig';
import {routes} from '@vuexy-admin/bootstrap-table/globalConfig.js';
import { routes, getAvatarUrl, booleanStatusCatalog, statusIntBadgeBgCatalogCss, statusIntBadgeBgCatalog } from '@vuexy-admin/assets/js/bootstrap-table/globalConfig';
export const userActionFormatter = (value, row, index) => {
if (!row.id) return '';
@ -9,7 +8,7 @@ export const userActionFormatter = (value, row, index) => {
const deleteUrl = routes['admin.user.delete'].replace(':id', row.id);
return `
<div class="flex space-x-2">
<div class="flex justify-center space-x-2">
<a href="${editUrl}" title="Editar" class="icon-button hover:text-slate-700">
<i class="ti ti-edit"></i>
</a>
@ -23,6 +22,25 @@ export const userActionFormatter = (value, row, index) => {
`.trim();
};
export const userLoginActionFormatter = (value, row, index) => {
if (!row.user_id) return '';
//const showUrl = routes['admin.user-login.show'].replace(':id', row.user_id);
const showUrl = '#';
return `
<div class="flex justify-center space-x-2">
<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] || {};
@ -50,6 +68,8 @@ export const dynamicBooleanFormatter = (value, row, index, options = {}) => {
};
export const dynamicBadgeFormatter = (value, row, index, options = {}) => {
if (!value) return '';
const {
color = 'primary', // Valor por defecto
textColor = '', // Permite agregar color de texto si es necesario
@ -59,6 +79,11 @@ export const dynamicBadgeFormatter = (value, row, index, 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>`
@ -71,95 +96,240 @@ export const textNowrapFormatter = (value, row, index) => {
}
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 textXsFormatter = (value, row, index) => {
if (!value) return '';
return `<span class="text-xs">${value}</span>`;
};
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}`;
export const dateClassicFormatter = (value) => {
if (!value) return '';
const date = new Date(value);
if (isNaN(date)) return value;
const fecha = date.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = value.length == 19? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }): false;
let fechaHora = hora
? `<span class="inline-flex items-center gap-1 text-blue-700 text-xs px-2 py-0.5 me-1">
<i class="ti ti-calendar-event text-[14px]"></i> ${fecha}
</span>`
: `<span class="inline-flex items-center gap-1 text-blue-700 px-2 py-1">
<i class="ti ti-calendar-event text-[16px]"></i> ${fecha}
</span>`;
if (hora) {
fechaHora += `
<span class="inline-flex items-center gap-1 text-gray-700 text-xs px-2 py-0.5">
<i class="ti ti-clock text-[14px]"></i> ${hora}
</span>
`.trim();
}
return `${baseUrl}admin/usuario/avatar/?name=${fullName}`;
}
return fechaHora.trim();
};
export const dateLimeFormatter = (value) => {
if (!value) return '';
const date = new Date(value);
if (isNaN(date)) return value;
const fecha = date.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = value.length == 19? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }): false;
let fechaHora = hora
? `<span class="inline-flex items-center gap-1 text-lime-800 text-xs px-2 py-0.5 me-1">
<i class="ti ti-calendar-event text-[14px]"></i> ${fecha}
</span>`
: `<span class="inline-flex items-center gap-1 text-lime-800 px-2 py-1">
<i class="ti ti-calendar-event text-[16px]"></i> ${fecha}
</span>`;
if (hora) {
fechaHora += `
<span class="inline-flex items-center gap-1 text-lime-600 text-xs px-2 py-0.5">
<i class="ti ti-clock text-[14px]"></i> ${hora}
</span>
`.trim();
}
return fechaHora.trim();
};
export const dateBadgeBlueFormatter = (value) => {
if (!value) return '';
const date = new Date(value);
if (isNaN(date)) return value;
const fecha = date.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = value.length == 19? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }): false;
let fechaHora = hora
? `<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 text-blue-700 text-xs px-2 py-0.5 me-1">
<i class="ti ti-calendar-event text-[14px]"></i> ${fecha}
</span>`
: `<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 text-blue-700 px-2 py-1">
<i class="ti ti-calendar-event text-[16px]"></i> ${fecha}
</span>`;
if (hora) {
fechaHora += `
<span class="inline-flex items-center gap-1 rounded-full bg-gray-100 text-gray-700 text-xs px-2 py-0.5">
<i class="ti ti-clock text-[14px]"></i> ${hora}
</span>
`.trim();
}
return fechaHora.trim();
};
export const dateBadgeAmberFormatter = (value) => {
if (!value) return '';
const date = new Date(value);
if (isNaN(date)) return value;
const fecha = date.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = value.length == 19? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }): false;
let fechaHora = hora
? `<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 text-amber-700 text-xs px-2 py-0.5 me-1">
<i class="ti ti-calendar-event text-[14px]"></i> ${fecha}
</span>`
: `<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 text-amber-700 px-2 py-1">
<i class="ti ti-calendar-event text-[16px]"></i> ${fecha}
</span>`;
if (hora) {
fechaHora += `
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 text-amber-600 text-xs px-2 py-0.5">
<i class="ti ti-clock text-[14px]"></i> ${hora}
</span>
`.trim();
}
return fechaHora.trim();
};
export const emailFormatter = (value, row, index) => {
if (!value) return '';
return `
<a href="mailto:${value}" class="flex items-center space-x-2 text-blue-600">
<i class="ti ti-mail-filled"></i>
<span class="whitespace-nowrap hover:underline">${value}</span>
</a>
`.trim();
};
/**
* Formatea la columna del perfil de usuario con avatar, nombre y correo.
* Formatter para mostrar duración de sesión del usuario.
*
* @param {string} row.created_at - Fecha de inicio de sesión.
* @param {string|null} row.logout_at - Fecha de cierre de sesión o null si aún activa.
* @returns {string} Duración de la sesión formateada o estado de sesión activa.
*/
export const userProfileFormatter = (value, row, index) => {
export const sessionDurationFormatter = (value, row, index) => {
if (!row.created_at) return '<span class="text-muted">-</span>';
const start = new Date(row.created_at);
const end = row.logout_at ? new Date(row.logout_at) : new Date();
const diffMs = end - start;
if (diffMs < 0) return '<span class="text-danger">Invalido</span>';
const diffSeconds = Math.floor(diffMs / 1000);
const hours = Math.floor(diffSeconds / 3600);
const minutes = Math.floor((diffSeconds % 3600) / 60);
const seconds = diffSeconds % 60;
const formattedTime = [
hours ? `${hours}h` : '',
minutes ? `${minutes}m` : '',
`${seconds}s`
].filter(Boolean).join(' ');
return row.logout_at
? `<span class="text-xs font-medium text-slate-700">${formattedTime}</span>`
: `<span class="text-xs font-semibold text-success-600">🟢 Sesión activa (${formattedTime})</span>`;
};
/**
* @param {int} row.id - Identificador del usuario
* @param {string} row.full_name - Nombre completo del usuario
* @param {string} row.profile_photo_path - Ruta de la foto de perfil
* @param {string} row.email - Correo electrónico del usuario
* @returns {string} HTML con el perfil del usuario
*/
export const userIdProfileFormatter = (value, row, index) => {
if (!row.id) return '';
const avatar = getAvatarUrl(row.full_name, row.profile_photo_path);
const email = row.email ? row.email : 'Sin correo';
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 formatterProfileElement(row.id, row.full_name, avatar, email, profileUrl);
};
const formatterProfileElement = (id, name, avatar, email, profileUrl) => {
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>
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${name}</a>
<small class="text-muted block truncate">${email}</small>
</div>
</div>
`;
`.trim();
};
/**
* Formatea la columna del perfil de contacto con avatar, nombre y correo.
* @param {int} row.user_id - Identificador del usuario
* @param {string} row.user_name - Nombre del usuario
* @param {string} row.user_profile_photo_path - Ruta de la foto de perfil
* @param {string} row.user_email - Correo electrónico del usuario
* @returns {string} HTML con el perfil del usuario
*/
export const contactProfileFormatter = (value, row, index) => {
if (!row.id) return '';
export const userProfileFormatter = (value, row, index) => {
if (!row.user_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';
const profileUrl = routes['admin.user.show'].replace(':id', row.user_id);
const avatar = getAvatarUrl(row.user_name, row.user_profile_photo_path);
const email = row.user_email ? row.user_email : 'Sin correo';
return `
<div class="flex items-center space-x-3" style="min-width: 240px">
@ -167,27 +337,101 @@ export const contactProfileFormatter = (value, row, index) => {
<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>
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.user_name}</a>
<small class="text-muted block truncate">${email}</small>
</div>
</div>
`;
`.trim();
};
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);
export const creatorProfileFormatter = (value, row, index) => {
if (!row.created_by) return '';
const profileUrl = routes['admin.user.show'].replace(':id', row.created_by);
const avatar = getAvatarUrl(row.creator_name, row.creator_profile_photo_path);
const email = row.creator_email ? row.creator_email : 'Sin correo';
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 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.creator_name}</a>
<small class="text-muted block truncate">${email}</small>
</div>
</div>
`;
`.trim();
};
export const updaterProfileFormatter = (value, row, index) => {
if (!row.updated_by) return '';
const profileUrl = routes['admin.user.show'].replace(':id', row.updated_by);
const avatar = getAvatarUrl(row.updater_name, row.updater_profile_photo_path);
const email = row.updater_email ? row.updater_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.updater_name}</a>
<small class="text-muted block truncate">${email}</small>
</div>
</div>
`.trim();
};
/**
* Convierte bytes en un formato legible por humanos.
*
* @param {int} value - Valor en bytes
* @returns {string} Tamaño en formato legible
*/
export const bytesToHumanReadable = (value, row, index) => {
if (!value || value === 0) return;
const precision = 2; // define aquí la precisión que desees
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const base = Math.floor(Math.log(value) / Math.log(1024));
return (value / Math.pow(1024, base)).toFixed(precision) + ' ' + units[base];
};
export const userRoleFormatter = (value, row, index) => {
if (!value) return '';
const rolesStyles = window.userRoleStyles || {};
return value.split('|').map(role => {
const trimmedRole = role.trim();
const color = rolesStyles[trimmedRole] || 'secondary'; // fallback si no existe
return `<span class="badge bg-label-${color} mr-2 mb-2">${trimmedRole}</span>`;
}).join('');
};
export const booleanStatusFormatter = (value, row, index) => {
return `<span class="badge bg-label-${value ? 'success' : 'danger'}">${value ? 'Activo' : 'Inactivo'}</span>`;
};

Some files were not shown because too many files have changed in this diff Show More