Testing Alpha
This commit is contained in:
parent
988b86a33d
commit
a7002701f5
18
.gitattributes
vendored
18
.gitattributes
vendored
@ -1,14 +1,18 @@
|
||||
# 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
|
||||
*.json diff=json
|
||||
*.yml diff=yaml
|
||||
*.yaml diff=yaml
|
||||
*.stub diff=php
|
||||
|
||||
# Evitar que estos archivos se exporten con Composer create-project
|
||||
# Archivos que NO deben exportarse con Composer create-project
|
||||
/.github export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.git export-ignore
|
||||
@ -20,5 +24,13 @@
|
||||
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
68
.gitignore
vendored
@ -1,11 +1,59 @@
|
||||
/node_modules
|
||||
/vendor
|
||||
/.vscode
|
||||
/.nova
|
||||
/.fleet
|
||||
/.phpactor.json
|
||||
/.phpunit.cache
|
||||
/.phpunit.result.cache
|
||||
/.zed
|
||||
/.idea
|
||||
# ⚙️ Laravel Package Defaults
|
||||
/vendor/
|
||||
composer.lock
|
||||
|
||||
# 🧪 PHPUnit
|
||||
.phpunit.result.cache
|
||||
/.phpunit.cache
|
||||
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
99
CONVENTIONS.md
Normal file
@ -0,0 +1,99 @@
|
||||
#  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.
|
||||
|
||||
---
|
@ -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.');
|
||||
}
|
||||
}
|
@ -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".');
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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']);
|
||||
}
|
||||
}
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
@ -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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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']);
|
||||
}
|
||||
|
||||
}
|
225
Models/User.php
225
Models/User.php
@ -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;
|
||||
}
|
||||
}
|
@ -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'
|
||||
];
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyAdmin\Queries;
|
||||
|
||||
class GenericQueryBuilder extends BootstrapTableQueryBuilder
|
||||
{
|
||||
// Custom query builder
|
||||
}
|
29
README.md
29
README.md
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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)];
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
20
config/keyvault_db.php
Normal 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,
|
||||
],
|
||||
],
|
||||
];
|
@ -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",
|
||||
"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",
|
||||
"creatorName" => "Koneko Soluciones Tecnológicas",
|
||||
"creatorUrl" => "https://koneko.mx",
|
||||
"licenseUrl" => "https://koneko.mx/koneko-admin/licencia",
|
||||
"supportUrl" => "https://koneko.mx/soporte",
|
||||
"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
107
config/koneko_admin.php
Normal 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
12
config/logging.php
Normal 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,
|
||||
],
|
||||
],
|
||||
];
|
@ -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
0
database/data/apis/apis.json
Normal file
0
database/data/apis/apis.json
Normal file
4
database/data/fixtures/users.csv
Normal file
4
database/data/fixtures/users.csv
Normal 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,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"
|
||||
]
|
||||
}
|
||||
|
||||
|
892
database/data/rbac/permissions.json
Normal file
892
database/data/rbac/permissions.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
205
database/data/rbac/roles.json
Normal file
205
database/data/rbac/roles.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
4
database/data/seeder_samples/users.csv
Normal file
4
database/data/seeder_samples/users.csv
Normal 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
|
|
45
database/data/seeder_samples/users.json
Normal file
45
database/data/seeder_samples/users.json
Normal 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
|
||||
}
|
||||
]
|
@ -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,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,
|
||||
]);
|
||||
}
|
||||
}
|
@ -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']);
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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->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->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
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
};
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
68
docs/<!DOCTYPE html>.html
Normal 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">
|
117
docs/Helpers/cache-helper-guide.md
Normal file
117
docs/Helpers/cache-helper-guide.md
Normal 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.
|
68
docs/Helpers/component-access-and-exposure-policy.md
Normal file
68
docs/Helpers/component-access-and-exposure-policy.md
Normal 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.
|
120
docs/Helpers/security-logger-helper-guide.md
Normal file
120
docs/Helpers/security-logger-helper-guide.md
Normal 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.
|
161
docs/Helpers/settings-helper-guide.md
Normal file
161
docs/Helpers/settings-helper-guide.md
Normal 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.
|
126
docs/Helpers/system-logger-helper-guide.md
Normal file
126
docs/Helpers/system-logger-helper-guide.md
Normal 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.
|
110
docs/Helpers/user-interaction-logger-helper.md
Normal file
110
docs/Helpers/user-interaction-logger-helper.md
Normal 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
30
docs/factory/index.md
Normal 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`
|
@ -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!
|
54
docs/notify-guide-koneko.md
Normal file
54
docs/notify-guide-koneko.md
Normal 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
32
docs/seeder/config.md
Normal 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' => [...]
|
||||
],
|
||||
],
|
||||
```
|
33
docs/seeder/create-new-seeder.md
Normal file
33
docs/seeder/create-new-seeder.md
Normal 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
10
docs/seeder/index.md
Normal 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
30
docs/seeder/usage.md
Normal 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
0
docs/structure.md
Normal 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;
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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 textXsFormatter = (value, row, index) => {
|
||||
if (!value) return '';
|
||||
return `<span class="text-xs">${value}</span>`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export const numberFormatter = (value, row, index) => {
|
||||
return isNaN(value) ? '' : Number(value);
|
||||
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();
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
export const humaneTimeFormatter = (value, row, index) => {
|
||||
return isNaN(value) ? '' : Number(value).humaneTime();
|
||||
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();
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
function getAvatarUrl(fullName, profilePhoto) {
|
||||
const baseUrl = window.baseUrl || '';
|
||||
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>`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (profilePhoto) {
|
||||
return `${baseUrl}storage/profile-photos/${profilePhoto}`;
|
||||
}
|
||||
|
||||
return `${baseUrl}admin/usuario/avatar/?name=${fullName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea la columna del perfil de usuario con avatar, nombre y correo.
|
||||
* @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 userProfileFormatter = (value, row, index) => {
|
||||
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
Loading…
x
Reference in New Issue
Block a user