Habilidades Puntuadas

This commit is contained in:
Juan Felipe Zapata Moreno 2025-07-07 16:37:22 -06:00
parent 6934fa6b28
commit df8a4c258a
34 changed files with 1990 additions and 319 deletions

View File

@ -33,7 +33,6 @@ public function index()
return $this->vuew('index', [ return $this->vuew('index', [
'departments' => department::where('name', 'LIKE', "%{$q}%") 'departments' => department::where('name', 'LIKE', "%{$q}%")
->orWhere('description', 'LIKE', "%{$q}%")
->select([ ->select([
'id', 'id',
'name', 'name',

View File

@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers\Admin;
/**
* @copyright Copyright (c) 2023 Notsoweb (https://notsoweb.com) - All rights reserved.
*/
use App\Http\Requests\StoreMainRoleSkills;
use App\Http\Requests\UpdateMainRoleSkills;
use App\Models\mainRole;
use App\Models\MainRoleSkills;
use App\Models\Score;
use App\Models\Skill;
use Illuminate\Support\Facades\Log;
use Notsoweb\Core\Http\Controllers\VueController;
/**
* Descripción
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
class MainRoleSkillsController extends VueController
{
public function __construct()
{
return $this->vueRoot('admin.mainRoleSkills');
}
public function index()
{
$q = request()->get('q');
$mainRole = mainRole::where('name', 'LIKE', "%{$q}%")->pluck('id');
$skills = Skill::where('name', 'LIKE', "%{$q}%")->pluck('id');
$scores = Score::where('alias', 'LIKE', "%{$q}%")->pluck('id');
$mainRoleSkills = MainRoleSkills::whereIn('main_role_id', $mainRole)
->orWhereIn('skill_id', $skills)
->orWhereIn('scored_id', $scores)
->with([
'mainRole:id,name,department_id',
'mainRole.department:id,name,description',
'skill:id,name,department_id',
'score:id,alias'
])
->paginate(config('app.pagination'));
return $this->vuew('index', [
'mainRoleSkills' => $mainRoleSkills
]);
}
public function create()
{
$mainRoles = mainRole::with('department:id,name')->orderBy('name', 'ASC')->get();
$skills = Skill::with('department:id,name')->orderBy('name', 'ASC')->get();
$scores = Score::orderBy('alias', 'ASC')->get();
return $this->vuew('create', [
'mainRoles' => $mainRoles,
'skills' => $skills,
'scores' => $scores
]);
}
public function store(StoreMainRoleSkills $request)
{
$create = [];
foreach ($request['skills'] as $skill){
$create[] = [
'main_role_id' => $request['main_role_id'],
'skill_id' => $skill['skill_id'],
'scored_id' => $skill['scored_id'],
'created_at' => now(),
'updated_at' => now(),
];
}
MainRoleSkills::insert($create);
return $this->index();
}
public function update(UpdateMainRoleSkills $request, MainRoleSkills $mainRoleSkills)
{
$mainRoleSkills->update($request->all());
}
public function destroy($id)
{
$mainRoleSkill = MainRoleSkills::findOrFail($id);
$mainRoleSkill->delete();
}
}

View File

@ -0,0 +1,65 @@
<?php namespace App\Http\Controllers\Admin;
/**
* @copyright Copyright (c) 2023 Notsoweb (https://notsoweb.com) - All rights reserved.
*/
use App\Http\Requests\StoreSkill;
use App\Http\Requests\UpdateSkill;
use App\Models\department;
use App\Models\Skill;
use Notsoweb\Core\Http\Controllers\VueController;
/**
* Descripción
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
class SkillController extends VueController
{
public function __construct()
{
return $this->vueRoot('admin.skills');
}
public function index()
{
$q = request()->get('q');
$skills = Skill::orderBy('name', 'ASC')
->where('name', 'LIKE', "%{$q}%")
->with('department:id,name')
->paginate(config('app.pagination'));
return $this->vuew('index', [
'skills' => $skills,
]);
}
public function create()
{
$department = department::orderBy('name', 'ASC')->get();
return $this->vuew('create', [
'departments' => $department,
]);
}
public function store(StoreSkill $request)
{
Skill::create($request->all());
return $this->index();
}
public function update(UpdateSkill $request, Skill $skill)
{
$skill->update($request->all());
}
public function destroy (Skill $skill)
{
$skill->delete();
}
}

View File

@ -0,0 +1,41 @@
<?php namespace App\Http\Requests;
/**
* @copyright Copyright (c) 2023 Notsoweb (https://notsoweb.com) - All rights reserved.
*/
use Illuminate\Foundation\Http\FormRequest;
/**
* Valida el almacenamiento de un usuario
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
class StoreMainRoleSkills extends FormRequest
{
/**
* Determinar si el usuario esta autorizado
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Reglas de validación
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'main_role_id' => ['required', 'integer'],
'skills' => ['required', 'array', 'min:1'],
'skills.*.skill_id' => ['required', 'integer'],
'skills.*.scored_id' => ['required', 'integer'],
];
}
}

View File

@ -0,0 +1,40 @@
<?php namespace App\Http\Requests;
/**
* @copyright Copyright (c) 2023 Notsoweb (https://notsoweb.com) - All rights reserved.
*/
use Illuminate\Foundation\Http\FormRequest;
/**
* Valida el almacenamiento de un usuario
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
class StoreSkill extends FormRequest
{
/**
* Determinar si el usuario esta autorizado
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Reglas de validación
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => ['required', 'string'],
'description' => ['nullable', 'string'],
'department_id' => ['required', 'integer'],
];
}
}

View File

@ -0,0 +1,40 @@
<?php namespace App\Http\Requests;
/**
* @copyright Copyright (c) 2023 Notsoweb (https://notsoweb.com) - All rights reserved.
*/
use Illuminate\Foundation\Http\FormRequest;
/**
* Valida el almacenamiento de un usuario
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
class UpdateMainRoleSkills extends FormRequest
{
/**
* Determinar si el usuario esta autorizado
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Reglas de validación
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'main_role_id' => ['required', 'integer'],
'skill_id' => ['required', 'integer'],
'scored_id' => ['required', 'integer'],
];
}
}

View File

@ -0,0 +1,40 @@
<?php namespace App\Http\Requests;
/**
* @copyright Copyright (c) 2023 Notsoweb (https://notsoweb.com) - All rights reserved.
*/
use Illuminate\Foundation\Http\FormRequest;
/**
* Valida el almacenamiento de un usuario
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
class UpdateSkill extends FormRequest
{
/**
* Determinar si el usuario esta autorizado
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Reglas de validación
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => ['required', 'string'],
'description' => ['nullable', 'string'],
'department_id' => ['required', 'integer','exists:departments,id'],
];
}
}

View File

@ -0,0 +1,45 @@
<?php namespace App\Models;
/**
* @copyright Copyright (c) 2023 Notsoweb (https://notsoweb.com) - All rights reserved.
*/
use App\Http\Traits\ModelExtend;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* Descripción
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
class MainRoleSkills extends Model
{
use HasFactory,
ModelExtend;
/**
* Atributos llenables masivamente
*/
protected $fillable = [
'main_role_id',
'skill_id',
'scored_id',
];
public function mainRole()
{
return $this->belongsTo(mainRole::class, 'main_role_id');
}
public function skill()
{
return $this->belongsTo(Skill::class, 'skill_id');
}
public function score()
{
return $this->belongsTo(Score::class, 'scored_id');
}
}

View File

@ -27,4 +27,9 @@ class Score extends Model
'value', 'value',
'description', 'description',
]; ];
public function mainRoleSkills()
{
return $this->hasMany(MainRoleSkills::class);
}
} }

46
app/Models/Skill.php Normal file
View File

@ -0,0 +1,46 @@
<?php namespace App\Models;
/**
* @copyright Copyright (c) 2023 Notsoweb (https://notsoweb.com) - All rights reserved.
*/
use App\Http\Traits\ModelExtend;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* Descripción
*
* @author Moisés de Jesús Cortés Castellanos <ing.moisesdejesuscortesc@notsoweb.com>
*
* @version 1.0.0
*/
class Skill extends Model
{
use HasFactory,
ModelExtend;
/**
* Atributos llenables masivamente
*/
protected $fillable = [
'name',
'description',
'department_id',
];
public function department()
{
return $this->belongsTo(department::class);
}
public function mainRoles()
{
return $this->belongsTo(MainRole::class);
}
public function mainRoleSkills()
{
return $this->hasMany(MainRoleSkills::class);
}
}

View File

@ -24,7 +24,7 @@ class department extends Model
*/ */
protected $fillable = [ protected $fillable = [
'name', 'name',
'description' 'description',
]; ];
/** /**
@ -34,4 +34,9 @@ public function mainRoles()
{ {
return $this->hasMany(MainRole::class); return $this->hasMany(MainRole::class);
} }
public function skills()
{
return $this->hasMany(Skill::class);
}
} }

View File

@ -22,7 +22,7 @@ class MainRole extends Model
/** /**
* Nombre de la tabla * Nombre de la tabla
*/ */
protected $table = 'main_role'; protected $table = 'main_roles';
/** /**
* Atributos llenables masivamente * Atributos llenables masivamente
@ -40,4 +40,14 @@ public function department()
{ {
return $this->belongsTo(department::class); return $this->belongsTo(department::class);
} }
public function skills()
{
return $this->hasMany(Skill::class);
}
public function mainRoleSkills()
{
return $this->hasMany(MainRoleSkills::class);
}
} }

View File

@ -11,7 +11,7 @@
*/ */
public function up(): void public function up(): void
{ {
Schema::create('main_role', function (Blueprint $table) { Schema::create('main_roles', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('name'); $table->string('name');
$table->string('description')->nullable(); $table->string('description')->nullable();
@ -25,6 +25,6 @@ public function up(): void
*/ */
public function down(): void public function down(): void
{ {
Schema::dropIfExists('main_role'); Schema::dropIfExists('main_roles');
} }
}; };

View File

@ -0,0 +1,30 @@
<?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('skills', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('description');
$table->foreignId('department_id')->constrained('departments')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('skills');
}
};

View File

@ -0,0 +1,30 @@
<?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('main_role_skills', function (Blueprint $table) {
$table->id();
$table->foreignId('main_role_id')->constrained('main_roles')->onDelete('cascade');
$table->foreignId('skill_id')->constrained('skills')->onDelete('cascade');
$table->foreignId('scored_id')->constrained('scores')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('main_role_skills');
}
};

View File

@ -0,0 +1,34 @@
<script setup>
defineEmits(['select']);
const props = defineProps({
departments: Object,
});
console.log('Department props:', props.departments);
</script>
<template>
<div class="w-full">
<div class="mb-6">
<h2 class="text-xl font-semibold mb-2">{{ $t('department.select.title') }}</h2>
<p class="text-gray-600">{{ $t('department.select.description') }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<button
v-for="department in departments"
:key="department.id"
@click="$emit('select', department)"
class="group p-6 border-2 border-gray-200 rounded-lg hover:border-primary hover:bg-primary/5 transition-all duration-200 text-left focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
<h3 class="font-semibold text-lg mb-2 group-hover:text-primary transition-colors">
{{ department.name }}
</h3>
<p class="text-gray-600 text-sm">
{{ department.description }}
</p>
</button>
</div>
</div>
</template>

View File

@ -0,0 +1,53 @@
<script setup>
import { computed } from 'vue';
const emit = defineEmits(['select']);
const props = defineProps({
mainRoles: {
type: Object,
required: true
},
selectedDepartment: {
type: Object,
required: true
}
});
const departmentMainRoles = computed(() => {
return props.mainRoles.filter(role =>
role.department && role.department.id === props.selectedDepartment.id
);
});
</script>
<template>
<div class="w-full">
<div class="mb-6">
<h2 class="text-xl font-semibold mb-2">
{{ $t('mainRole.inDepartment', { department: selectedDepartment.name }) }}
</h2>
<p class="text-gray-600">{{ $t('mainRole.select.description') }}</p>
</div>
<div v-if="departmentMainRoles.length > 0" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<button
v-for="mainRole in departmentMainRoles"
:key="mainRole.id"
@click="$emit('select', mainRole)"
class="group p-6 border-2 border-gray-200 rounded-lg hover:border-primary hover:bg-primary/5 transition-all duration-200 text-left focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
<h3 class="font-semibold text-lg mb-2 group-hover:text-primary transition-colors">
{{ mainRole.name }}
</h3>
<p class="text-gray-600 text-sm">
{{ mainRole.description}}
</p>
</button>
</div>
<div v-else class="text-center py-12">
<p class="text-gray-500 text-lg">{{ $t('mainRole.noRolesInDepartment') }}</p>
</div>
</div>
</template>

View File

@ -0,0 +1,191 @@
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import PrimaryButton from "@/Components/Dashboard/Button/Primary.vue";
import Selectable from "@/Components/Dashboard/Form/Selectable.vue";
const emit = defineEmits(["submit", "update:modelValue"]);
const props = defineProps({
form: {
type: Object,
required: true,
},
skills: {
type: Object,
required: true,
},
scores: {
type: Object,
required: true,
},
selectedDepartment: {
type: Object,
required: true,
},
selectedMainRole: {
type: Object,
required: true,
},
modelValue: {
type: Array,
default: () => [],
},
});
const skillId = ref("");
const scoredId = ref("");
const todos = ref([]);
function addTodo() {
if (skillId.value && scoredId.value) {
// Buscar los objetos completos
const selectedSkill = departmentSkills.value.find(
(skill) => skill.id === skillId.value.id || skill.id === skillId.value
);
const selectedScore = props.scores.find(
(score) => score.id === scoredId.value.id || score.id === scoredId.value
);
if (selectedSkill && selectedScore) {
const newItem = {
skill_id: selectedSkill.id,
scored_id: selectedScore.id,
skill_name: selectedSkill.name,
score_alias: selectedScore.alias,
};
todos.value.push(newItem);
// Limpiar los campos
skillId.value = "";
scoredId.value = "";
// Emitir el arreglo actualizado
emit("update:modelValue", todos.value);
}
}
}
function removeTodo(index) {
todos.value.splice(index, 1);
emit("update:modelValue", todos.value);
}
const departmentSkills = computed(() => {
return props.skills.filter(
(skill) => skill.department_id === props.selectedDepartment.id
);
});
const isFormValid = computed(() => {
return todos.value.length > 0;
});
const submitForm = () => {
if (isFormValid.value) {
emit("submit", todos.value);
}
};
onMounted(() => {
if (Array.isArray(props.modelValue) && props.modelValue.length > 0) {
todos.value = [...props.modelValue];
}
});
watch(
() => props.modelValue,
(newValue) => {
if (Array.isArray(newValue)) {
todos.value = [...newValue];
}
}
);
</script>
<template>
<div class="w-full">
<div class="mb-6">
<h2 class="text-xl font-semibold mb-2">
{{ $t("skill.assignTo", { role: selectedMainRole.name }) }}
</h2>
<p class="text-gray-600">
{{
$t("skill.assignment.description", {
department: selectedDepartment.name,
})
}}
</p>
</div>
<!-- Formulario para agregar habilidades -->
<div class="bg-white rounded-lg shadow-sm border p-6 mb-6">
<div class="grid gap-6 grid-cols-1 md:grid-cols-2">
<Selectable
id="skill_id"
v-model="skillId"
:options="departmentSkills"
:title="$t('skill.title')"
placeholder="Selecciona una habilidad..."
label="name"
track-by="id"
required
/>
<Selectable
id="scored_id"
v-model="scoredId"
:options="scores"
:title="$t('scores.title')"
placeholder="Selecciona una puntuación..."
label="alias"
track-by="id"
required
/>
</div>
<div class="flex justify-center mt-6">
<button
@click="addTodo"
type="button"
class="rounded-lg px-4 py-2 bg-primary text-white border dark:bg-primary-dark dark:border-primary-dark-on"
>
Agregar Habilidad
</button>
</div>
</div>
<!-- Lista de habilidades agregadas -->
<div class="space-y-3 mb-6">
<template v-for="(todo, index) in todos" :key="index">
<div class="flex bg-white items-center justify-between rounded-lg px-4 py-3 border shadow-sm">
<div class="flex-1">
<div class="font-bold text-black">Habilidad: {{ todo.skill_name }}</div>
<div class="font-bold text-black">Puntuación: {{ todo.score_alias }}</div>
</div>
<div
@click="removeTodo(index)"
class="rounded-lg px-4 py-2 bg-red-600 text-white cursor-pointer hover:bg-red-700 transition-colors"
>
Quitar
</div>
</div>
</template>
<div v-if="todos.length === 0" class="text-gray-500 text-center py-8 bg-gray-50 rounded-lg">
No hay habilidades agregadas
</div>
</div>
<!-- Botón de envío -->
<div class="flex justify-center">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing || !isFormValid"
type="button"
@click="submitForm"
>
<span>{{ $t("Crear") }}</span>
</PrimaryButton>
</div>
</div>
</template>

View File

@ -0,0 +1,38 @@
<script setup>
defineProps({
currentStep: {
type: Number,
required: true
}
});
const steps = [
{ number: 1, name: 'department.title' },
{ number: 2, name: 'mainRole.title' },
{ number: 3, name: 'skill.title' }
];
</script>
<template>
<div class="w-full pb-4">
<div class="flex items-center justify-center space-x-4 mb-6">
<template v-for="(step, index) in steps" :key="step.number">
<div class="flex items-center">
<div
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors"
:class="currentStep >= step.number ? 'bg-primary text-primary-on' : 'bg-gray-300 text-gray-600'"
>
{{ step.number }}
</div>
<span class="ml-2 text-sm font-medium">{{ $t(step.name) }}</span>
</div>
<div
v-if="index < steps.length - 1"
class="w-8 h-1 bg-gray-300 transition-colors"
:class="currentStep > step.number ? 'bg-primary' : ''"
/>
</template>
</div>
</div>
</template>

View File

@ -1,257 +1,317 @@
export default { export default {
'&':'y', "&": "y",
account: { account: {
delete: { delete: {
confirm:'¿Está seguro de que quiere eliminar su cuenta? Una vez eliminada su cuenta, todos sus recursos y datos se borrarán permanentemente. Por favor, introduzca su contraseña para confirmar que desea eliminar permanentemente su cuenta.', confirm:
description:'Eliminar permanentemente su cuenta.', "¿Está seguro de que quiere eliminar su cuenta? Una vez eliminada su cuenta, todos sus recursos y datos se borrarán permanentemente. Por favor, introduzca su contraseña para confirmar que desea eliminar permanentemente su cuenta.",
onDelete:'Una vez eliminada su cuenta, todos sus recursos y datos se borrarán permanentemente. Antes de eliminar su cuenta, descargue los datos o la información que desee conservar.', description: "Eliminar permanentemente su cuenta.",
title:'Eliminar cuenta', onDelete:
"Una vez eliminada su cuenta, todos sus recursos y datos se borrarán permanentemente. Antes de eliminar su cuenta, descargue los datos o la información que desee conservar.",
title: "Eliminar cuenta",
}, },
email: { email: {
notifySendVerification:'Se ha enviado un nuevo enlace de verificación a su dirección de correo electrónico.', notifySendVerification:
sendVerification:'Haga clic aquí para volver a enviar el correo electrónico de verificación.', "Se ha enviado un nuevo enlace de verificación a su dirección de correo electrónico.",
unverify: 'Su dirección de correo electrónico no está verificada.', sendVerification:
"Haga clic aquí para volver a enviar el correo electrónico de verificación.",
unverify: "Su dirección de correo electrónico no está verificada.",
}, },
manage:'Administrar cuenta', manage: "Administrar cuenta",
password: { password: {
description:'Asegúrese de que su cuenta utiliza una contraseña larga y aleatoria para estar seguro.', description:
new:'Nueva contraseña', "Asegúrese de que su cuenta utiliza una contraseña larga y aleatoria para estar seguro.",
reset:'Restaurar contraseña', new: "Nueva contraseña",
secure:'Esta es una zona segura de la aplicación. Confirme su contraseña antes de continuar.', reset: "Restaurar contraseña",
update: 'Actualizar contraseña', secure:
verify:'Por su seguridad, confirme su contraseña para continuar.', "Esta es una zona segura de la aplicación. Confirme su contraseña antes de continuar.",
update: "Actualizar contraseña",
verify: "Por su seguridad, confirme su contraseña para continuar.",
}, },
profile: { profile: {
description:'Actualice la información del perfil de su cuenta y su dirección de correo electrónico.', description:
title:'Información del perfil', "Actualice la información del perfil de su cuenta y su dirección de correo electrónico.",
title: "Información del perfil",
}, },
sessions: { sessions: {
confirm:'Por favor, introduzca su contraseña para confirmar que desea salir de sus otras sesiones de navegación en todos sus dispositivos.', confirm:
description: 'Gestiona y cierra tus sesiones activas en otros navegadores y dispositivos.', "Por favor, introduzca su contraseña para confirmar que desea salir de sus otras sesiones de navegación en todos sus dispositivos.",
last:'Último activo', description:
logout:'Cerrar otras sesiones del navegador', "Gestiona y cierra tus sesiones activas en otros navegadores y dispositivos.",
onLogout:'Si es necesario, puede cerrar la sesión de todos sus otros navegadores en todos sus dispositivos. A continuación se enumeran algunas de sus sesiones recientes; sin embargo, esta lista puede no ser exhaustiva. Si crees que tu cuenta ha sido comprometida, también deberías actualizar tu contraseña.', last: "Último activo",
this: 'Dispositivo actual', logout: "Cerrar otras sesiones del navegador",
title: 'Sesiones del navegador', onLogout:
"Si es necesario, puede cerrar la sesión de todos sus otros navegadores en todos sus dispositivos. A continuación se enumeran algunas de sus sesiones recientes; sin embargo, esta lista puede no ser exhaustiva. Si crees que tu cuenta ha sido comprometida, también deberías actualizar tu contraseña.",
this: "Dispositivo actual",
title: "Sesiones del navegador",
}, },
twoFactor: { twoFactor: {
codes:{ codes: {
regenerate:'Regenerar los códigos de recuperación', regenerate: "Regenerar los códigos de recuperación",
show:'Mostrar códigos de recuperación', show: "Mostrar códigos de recuperación",
store:'Guarde estos códigos de recuperación en un gestor de contraseñas seguro. Pueden utilizarse para recuperar el acceso a su cuenta si se pierde su dispositivo de autenticación de dos factores.', store:
"Guarde estos códigos de recuperación en un gestor de contraseñas seguro. Pueden utilizarse para recuperar el acceso a su cuenta si se pierde su dispositivo de autenticación de dos factores.",
}, },
description:'Añada seguridad adicional a su cuenta mediante la autenticación de dos factores.', description:
isEnable:'Ha activado la autenticación de dos factores.', "Añada seguridad adicional a su cuenta mediante la autenticación de dos factores.",
isNotEnable:{ isEnable: "Ha activado la autenticación de dos factores.",
title:'No ha activado la autenticación de dos factores.', isNotEnable: {
description:'Cuando la autenticación de dos factores está activada, se le pedirá un token seguro y aleatorio durante la autenticación. Puedes recuperar este token desde la aplicación Google Authenticator de tu teléfono.', title: "No ha activado la autenticación de dos factores.",
description:
"Cuando la autenticación de dos factores está activada, se le pedirá un token seguro y aleatorio durante la autenticación. Puedes recuperar este token desde la aplicación Google Authenticator de tu teléfono.",
}, },
key:'Llave de configuración', key: "Llave de configuración",
login: { login: {
onAuth: 'Por favor, confirme el acceso a su cuenta introduciendo el código de autentificación proporcionado por su aplicación de autentificación.', onAuth:
onRecovery: 'Confirme el acceso a su cuenta introduciendo uno de sus códigos de recuperación de emergencia.', "Por favor, confirme el acceso a su cuenta introduciendo el código de autentificación proporcionado por su aplicación de autentificación.",
onRecovery:
"Confirme el acceso a su cuenta introduciendo uno de sus códigos de recuperación de emergencia.",
}, },
onFinish:'Termina de habilitar la autenticación de dos factores.', onFinish: "Termina de habilitar la autenticación de dos factores.",
qr: { qr: {
isConfirmed: 'La autenticación de dos factores ya está activada. Escanee el siguiente código QR con la aplicación de autenticación de su teléfono o introduzca la clave de configuración.', isConfirmed:
onConfirmed: 'Para terminar de habilitar la autenticación de dos factores, escanea el siguiente código QR utilizando la aplicación de autenticación de tu teléfono o introduce la clave de configuración y proporciona el código OTP generado.', "La autenticación de dos factores ya está activada. Escanee el siguiente código QR con la aplicación de autenticación de su teléfono o introduzca la clave de configuración.",
onConfirmed:
"Para terminar de habilitar la autenticación de dos factores, escanea el siguiente código QR utilizando la aplicación de autenticación de tu teléfono o introduce la clave de configuración y proporciona el código OTP generado.",
}, },
recovery: { recovery: {
code: 'Código de recuperación', code: "Código de recuperación",
useAuth: 'Utilizar un código de autentificación', useAuth: "Utilizar un código de autentificación",
useCode: 'Utiliza un código de recuperación', useCode: "Utiliza un código de recuperación",
}, },
title:'Autenticación de dos factores', title: "Autenticación de dos factores",
}, },
}, },
actions:'Acciones', actions: "Acciones",
auth: { auth: {
forgotPassword: { forgotPassword: {
ask: '¿Olvidaste tu contraseña?', ask: "¿Olvidaste tu contraseña?",
description: '¿Ha olvidado su contraseña? No hay problema. Sólo tienes que indicarnos tu dirección de correo electrónico y te enviaremos un enlace para restablecer la contraseña que te permitirá elegir una nueva.', description:
sendLink: 'Enviar enlace de recuperación por correo', "¿Ha olvidado su contraseña? No hay problema. Sólo tienes que indicarnos tu dirección de correo electrónico y te enviaremos un enlace para restablecer la contraseña que te permitirá elegir una nueva.",
title: 'Contraseña olvidada', sendLink: "Enviar enlace de recuperación por correo",
title: "Contraseña olvidada",
}, },
login: 'Iniciar sesión', login: "Iniciar sesión",
logout: 'Cerrar sesión', logout: "Cerrar sesión",
register: { register: {
already: '¿Ya estas registrado?', already: "¿Ya estas registrado?",
me: 'Registrarme', me: "Registrarme",
}, },
remember: 'Recuerdame', remember: "Recuerdame",
}, },
code:'Código', code: "Código",
cancel:'Cancelar', cancel: "Cancelar",
changes:'Cambios', changes: "Cambios",
changelogs: { changelogs: {
title:'Historial de cambios', title: "Historial de cambios",
description: 'Lista de los cambios realizados al sistema.', description: "Lista de los cambios realizados al sistema.",
}, },
close:"Cerrar", close: "Cerrar",
confirm:'Confirmar', confirm: "Confirmar",
copyright:'Todos los derechos reservados.', copyright: "Todos los derechos reservados.",
contact:'Contacto', contact: "Contacto",
crud: { crud: {
create: 'Nuevo registro', create: "Nuevo registro",
edit: 'Editar registro', edit: "Editar registro",
destroy: 'Eliminar registro', destroy: "Eliminar registro",
show: 'Más detalles', show: "Más detalles",
}, },
date: 'Fecha', date: "Fecha",
delete:{ delete: {
confirm: 'Al presionar ELIMINAR el registro se eliminará permanentemente y no podrá recuperarse.', confirm:
title: 'Eliminar', "Al presionar ELIMINAR el registro se eliminará permanentemente y no podrá recuperarse.",
title: "Eliminar",
}, },
deleted:'Eliminado', deleted: "Eliminado",
description:'Descripción', description: "Descripción",
details:'Detalles', details: "Detalles",
disable:'Deshabilitar', disable: "Deshabilitar",
disabled:'Deshabilitado', disabled: "Deshabilitado",
done:'Hecho.', done: "Hecho.",
edit:'Editar', edit: "Editar",
email:{ email: {
title:'Correo', title: "Correo",
verification:'Verificar correo' verification: "Verificar correo",
}, },
enable:'Habilitar', enable: "Habilitar",
endDate:'Fecha Fin', endDate: "Fecha Fin",
event:'Evento', event: "Evento",
help: { help: {
description:'A continuación se lista la iconografía para entender el funcionamiento del sistema.', description:
home: 'Volver a la pagina de inicio.', "A continuación se lista la iconografía para entender el funcionamiento del sistema.",
title:'Ayuda', home: "Volver a la pagina de inicio.",
title: "Ayuda",
}, },
history: { history: {
title:'Historial de acciones', title: "Historial de acciones",
description:'Historial de acciones realizadas por los usuarios en orden cronológico.' description:
"Historial de acciones realizadas por los usuarios en orden cronológico.",
}, },
home:'Inicio', home: "Inicio",
hour:'Hora', hour: "Hora",
icon:'Icono', icon: "Icono",
maternal:'Apellido materno', maternal: "Apellido materno",
menu:'Menú', menu: "Menú",
name:'Nombre', name: "Nombre",
noRecords:'Sin registros', noRecords: "Sin registros",
notifications: { notifications: {
readed:'Marcar como leído', readed: "Marcar como leído",
deleted:'Notificación eliminada', deleted: "Notificación eliminada",
description:'Notificaciones del usuario', description: "Notificaciones del usuario",
notFound:'Notificación no encontrada', notFound: "Notificación no encontrada",
title:'Notificaciones', title: "Notificaciones",
}, },
password:'Contraseña', password: "Contraseña",
passwordConfirmation:'Confirmar contraseña', passwordConfirmation: "Confirmar contraseña",
passwordCurrent:'Contraseña actual', passwordCurrent: "Contraseña actual",
passwordReset:'Restaurar contraseña', passwordReset: "Restaurar contraseña",
paternal:'Apellido paterno', paternal: "Apellido paterno",
phone:'Teléfono', phone: "Teléfono",
photo: { photo: {
new: 'Seleccionar una nueva foto', new: "Seleccionar una nueva foto",
remove:'Remover foto', remove: "Remover foto",
title:'Foto', title: "Foto",
}, },
profile:'Perfil', profile: "Perfil",
readed:'Leído', readed: "Leído",
register: { register: {
agree:'Estoy de acuerdo con los', agree: "Estoy de acuerdo con los",
privacy:'Política de Privacidad', privacy: "Política de Privacidad",
signUp:'Registrarme', signUp: "Registrarme",
terms:'Términos de Servicio', terms: "Términos de Servicio",
}, },
registers:{ registers: {
title:'Registros', title: "Registros",
empty:'Sin registros', empty: "Sin registros",
}, },
remove: 'Remover', remove: "Remover",
return: 'Regresar', return: "Regresar",
role:'Rol', role: "Rol",
roles:{ roles: {
create: { create: {
title: 'Crear rol', title: "Crear rol",
description: 'Estos roles serán usados para dar permisos en el sistema.', description: "Estos roles serán usados para dar permisos en el sistema.",
onSuccess: 'Rol creado exitosamente', onSuccess: "Rol creado exitosamente",
onError: 'Error al crear el role', onError: "Error al crear el role",
}, },
deleted:'Rol eliminado', deleted: "Rol eliminado",
title: 'Roles', title: "Roles",
}, },
save:'Guardar', save: "Guardar",
saved:'¡Guardado!', saved: "¡Guardado!",
search:'Buscar', search: "Buscar",
selected: 'Seleccionado', selected: "Seleccionado",
select: 'Seleccionar', select: "Seleccionar",
setting: 'Configuración', setting: "Configuración",
show: { show: {
all:'Mostrar todo', all: "Mostrar todo",
title:'Mostrar', title: "Mostrar",
}, },
startDate:'Fecha de inicio', startDate: "Fecha de inicio",
status:'Estado', status: "Estado",
terms: { terms: {
agree:'Estoy de acuerdo con los', agree: "Estoy de acuerdo con los",
privacy:'Política de privacidad', privacy: "Política de privacidad",
service:'Términos de servicio', service: "Términos de servicio",
}, },
unknown:'Desconocido', unknown: "Desconocido",
update:'Actualizar', update: "Actualizar",
updated:'Actualizado', updated: "Actualizado",
updateFail:'Error al actualizar', updateFail: "Error al actualizar",
unreaded:'No leído', unreaded: "No leído",
user:'Usuario', user: "Usuario",
users:{ users: {
create:{ create: {
title:'Crear usuario', title: "Crear usuario",
description:'Permite crear nuevos usuarios. No olvides otorgarle roles para que pueda acceder a las partes del sistema deseados.', description:
onSuccess:'Usuario creado', "Permite crear nuevos usuarios. No olvides otorgarle roles para que pueda acceder a las partes del sistema deseados.",
onError:'Ocurrió un error al crear el usuario' onSuccess: "Usuario creado",
onError: "Ocurrió un error al crear el usuario",
}, },
deleted:'Usuario eliminado', deleted: "Usuario eliminado",
notFount:'Usuario no encontrado', notFount: "Usuario no encontrado",
password: { password: {
description:'Permite actualizar las contraseñas de los usuarios sobreescribiendola.', description:
title:'Actualizar contraseña', "Permite actualizar las contraseñas de los usuarios sobreescribiendola.",
title: "Actualizar contraseña",
}, },
roles: { roles: {
description:'Actualiza los roles de los usuarios, permitiendo o denegando los accesos a determinadas áreas.', description:
error:{ "Actualiza los roles de los usuarios, permitiendo o denegando los accesos a determinadas áreas.",
min:'Seleccionar mínimo un role' error: {
min: "Seleccionar mínimo un role",
}, },
title:'Roles de usuario', title: "Roles de usuario",
}, },
menu:'Menú de usuario', menu: "Menú de usuario",
select:'Seleccionar un usuario', select: "Seleccionar un usuario",
settings:'Ajustes del usuario', settings: "Ajustes del usuario",
system:'Usuarios del sistema', system: "Usuarios del sistema",
title:'Usuarios', title: "Usuarios",
}, },
scores:{ scores: {
system: 'Sistema de puntuación', system: "Sistema de puntuación",
create:{ title: "Puntuaciones",
title:'Crear puntuación', create: {
description:'Permite crear nuevas puntuaciones para los usuarios.', title: "Crear puntuación",
onSuccess:'Puntuación creada exitosamente', description: "Permite crear nuevas puntuaciones para los usuarios.",
onError:'Error al crear la puntuación', onSuccess: "Puntuación creada exitosamente",
} onError: "Error al crear la puntuación",
}, },
department:{ },
system: 'Sistema de departamentos', department: {
title: 'Departamentos', system: "Sistema de departamentos",
create:{ title: "Departamentos",
title:'Crear departamento', create: {
description:'Permite crear nuevos departamentos para los usuarios.', title: "Crear departamento",
onSuccess:'Departamento creado exitosamente', description: "Permite crear nuevos departamentos para los usuarios.",
onError:'Error al crear el departamento', onSuccess: "Departamento creado exitosamente",
onError: "Error al crear el departamento",
},
select:{
title: "Seleccionar un departamento",
description: "Seleccione un departamento para filtrar los roles.",
} }
}, },
mainRole: { mainRole: {
system: 'Sistema de roles principales', title: "Roles principales",
create:{ inDepartment: "Roles principales en el departamento",
title:'Crear rol principal', system: "Sistema de roles principales",
description:'Permite crear nuevos roles principales para los usuarios.', select:{
onSuccess:'Rol principal creado exitosamente', description: "Seleccione un rol principal.",
onError:'Error al crear el rol principal', },
create: {
title: "Crear rol principal",
description: "Permite crear nuevos roles principales para los usuarios.",
onSuccess: "Rol principal creado exitosamente",
onError: "Error al crear el rol principal",
}, },
}, },
version:'Versión', skill: {
} title: "Habilidades",
assignTo: "Asignar a rol principal",
system: "Sistema de habilidades",
assignment: {
description: "Asigna una habilidad a un rol principal.",
},
create: {
title: "Crear habilidad",
description:
"Permite crear nuevas habilidades para los roles principales.",
onSuccess: "Habilidad creada exitosamente",
onError: "Error al crear la habilidad",
},
deleted: "Habilidad eliminada",
},
mainRoleSkills: {
system: "Sistema de habilidades de rol principal",
create: {
title: "Crear habilidad de rol principal",
description:
"Permite crear nuevas habilidades para los roles principales.",
onSuccess: "Habilidad de rol principal creada exitosamente",
onError: "Error al crear la habilidad de rol principal",
},
},
version: "Versión",
};

View File

@ -85,11 +85,21 @@ onMounted(()=> {
name="Rol Principal" name="Rol Principal"
to="admin.mainRoles.index" to="admin.mainRoles.index"
/> />
<Link
icon="psychology"
name="Habilidades"
to="admin.skills.index"
/>
<Link <Link
icon="leaderboard" icon="leaderboard"
name="Scores" name="Scores"
to="admin.scores.index" to="admin.scores.index"
/> />
<Link
icon="sports_handball"
name="Roles Puntuados"
to="admin.mainRoleSkills.index"
/>
<Link <Link
icon="history" icon="history"
name="changelogs.title" name="changelogs.title"

View File

@ -1,47 +1,38 @@
<script setup> <script setup>
import { goTo } from './Component'; import { goTo } from "./Component";
import { useForm } from '@inertiajs/vue3'; import { useForm } from "@inertiajs/vue3";
import { onUpdated } from 'vue'; import { onUpdated } from "vue";
import Input from '@/Components/Dashboard/Form/Input.vue'; import Input from "@/Components/Dashboard/Form/Input.vue";
import EditModal from '@/Components/Dashboard/Modal/Edit.vue'; import EditModal from "@/Components/Dashboard/Modal/Edit.vue";
import Header from '@/Components/Dashboard/Modal/Elements/Header.vue'; import Header from "@/Components/Dashboard/Modal/Elements/Header.vue";
const emit = defineEmits([ const emit = defineEmits(["close", "switchModal"]);
'close',
'switchModal'
]);
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
model: Object model: Object,
}); });
const form = useForm({}); const form = useForm({});
const update = (id) => { const update = (id) => {
form.transform(data => ({ form
...props.model .transform((data) => ({
})).put(route(goTo('update'), {id}),{ ...props.model,
}))
.put(route(goTo("update"), { id }), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
Notify.success(lang('updated')) Notify.success(lang("updated"));
emit('switchModal') emit("switchModal");
} },
}); });
} };
</script> </script>
<template> <template>
<EditModal <EditModal :show="show" @close="$emit('close')" @update="update(model.id)">
:show="show" <Header :title="model.alias" />
@close="$emit('close')"
@update="update(model.id)"
>
<Header
:title="model.alias"
/>
<div class="py-2 border-b"> <div class="py-2 border-b">
<div class="p-4"> <div class="p-4">
<form> <form>

View File

@ -17,7 +17,6 @@ import ShowView from './Show.vue';
const props = defineProps({ const props = defineProps({
departments: Object departments: Object
}); });
// Controladores // Controladores
const Modal = new ModalController(); const Modal = new ModalController();
const Searcher = new SearcherController(goTo('index')); const Searcher = new SearcherController(goTo('index'));

View File

@ -0,0 +1,15 @@
import { t } from '@/Lang/i18n';
import { hasPermission } from '@/rolePermission.js';
// Obtener ruta
const goTo = (route) => `admin.mainRoleSkills.${route}`
// Obtener traducción del componente
const transl = (lang) => t(`mainRoleSkills.${lang}`)
// Determina si un usuario puede hacer algo no en base a los permisos
const can = (permission) => hasPermission(`users.${permission}`)
export{
can,
goTo,
transl
}

View File

@ -0,0 +1,163 @@
<script setup>
import { goTo, transl } from "./Component";
import { Link, useForm } from "@inertiajs/vue3";
import { ref, computed } from "vue";
import PrimaryButton from "@/Components/Dashboard/Button/Primary.vue";
import SecondaryButton from "@/Components/Dashboard/Button/Secondary.vue";
import PageHeader from "@/Components/Dashboard/PageHeader.vue";
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue";
import DashboardLayout from "@/Layouts/DashboardLayout.vue";
// Componentes específicos para el flujo
import StepIndicator from "@/Components/App/StepIndicator.vue";
import DepartmentSelector from "@/Components/App/DepartmentSelector.vue";
import MainRoleSelector from "@/Components/App/MainRoleSelector.vue";
import SkillAssignment from "@/Components/App/SkillAssignment.vue";
const props = defineProps({
mainRoles: Object,
skills: Object,
scores: Object,
});
// Estado del flujo
const currentStep = ref(1);
const selectedDepartment = ref(null);
const selectedMainRole = ref(null);
const form = useForm({
main_role_id: "",
skill_id: "",
scored_id: "",
});
// Datos computados
const uniqueDepartments = computed(() => {
const deps = [];
const seen = new Set();
props.mainRoles.forEach((role) => {
if (role.department && !seen.has(role.department.id)) {
seen.add(role.department.id);
deps.push(role.department);
}
});
return deps;
});
// Métodos de navegación
const handleDepartmentSelect = (department) => {
selectedDepartment.value = department;
currentStep.value = 2;
};
const handleMainRoleSelect = (mainRole) => {
selectedMainRole.value = mainRole;
form.main_role_id = mainRole.id;
currentStep.value = 3;
};
const goBack = () => {
if (currentStep.value === 3) {
selectedMainRole.value = null;
form.main_role_id = "";
form.skill_id = "";
form.scored_id = "";
currentStep.value = 2;
} else if (currentStep.value === 2) {
selectedDepartment.value = null;
selectedMainRole.value = null;
form.reset();
currentStep.value = 1;
}
};
const resetFlow = () => {
selectedDepartment.value = null;
selectedMainRole.value = null;
form.reset();
currentStep.value = 1;
};
const submit = (skillArray) => {
const skillsData = skillArray.map(skill => ({
skill_id: skill.skill_id,
scored_id: skill.scored_id,
}));
form
.transform((data) => ({
main_role_id: data.main_role_id,
skills: skillsData
}))
.post(route(goTo("store")), {
onSuccess: () => {
Notify.success(transl("create.onSuccess"));
resetFlow();
},
onError: () => Notify.error(transl("create.onError")),
});
};
</script>
<template>
<DashboardLayout :title="transl('create.title')">
<PageHeader>
<div class="flex items-center space-x-2">
<Link :href="route(goTo('index'))">
<GoogleIcon
:title="$t('return')"
class="btn-icon-primary"
name="arrow_back"
outline
/>
</Link>
</div>
</PageHeader>
<!-- Indicador de progreso componentizado -->
<div class="mt-6">
<StepIndicator :current-step="currentStep" />
<div class="flex justify-center gap-2">
<SecondaryButton v-if="currentStep > 1" @click="goBack" type="button">
<GoogleIcon name="keyboard_backspace" class="mr-2" />
{{ $t("Regresar") }}
</SecondaryButton>
<SecondaryButton
v-if="currentStep > 1"
@click="resetFlow"
type="button"
>
<GoogleIcon name="refresh" class="mr-2" />
{{ $t("Inicio") }}
</SecondaryButton>
</div>
</div>
<!-- Componentes de pasos -->
<DepartmentSelector
v-if="currentStep === 1"
:departments="uniqueDepartments"
@select="handleDepartmentSelect"
/>
<MainRoleSelector
v-if="currentStep === 2"
:main-roles="mainRoles"
:selected-department="selectedDepartment"
@select="handleMainRoleSelect"
/>
<SkillAssignment
v-if="currentStep === 3"
:form="form"
:skills="skills"
:scores="scores"
:selected-department="selectedDepartment"
:selected-main-role="selectedMainRole"
@submit="submit"
/>
</DashboardLayout>
</template>

View File

@ -0,0 +1,43 @@
<script setup>
import { transl, goTo } from './Component'
import { router } from '@inertiajs/vue3';
import DestroyModal from '@/Components/Dashboard/Modal/Destroy.vue';
import Header from '@/Components/Dashboard/Modal/Elements/Header.vue';
const emit = defineEmits([
'close',
'switchModal'
]);
const props = defineProps({
show: Boolean,
model: Object
});
const destroy = (id) => router.delete(route(goTo('destroy'), {id}), {
preserveScroll: true,
onSuccess: () => {
props.model.pop;
Notify.success(transl('deleted'));
emit('close');
},
onError: () => {
Notify.info(transl('notFound'));
emit('close');
}
});
</script>
<template>
<DestroyModal
:show="show"
@close="$emit('close')"
@destroy="destroy(model.id)"
>
<Header
:title="model.name"
:subtitle="model.description"
/>
</DestroyModal>
</template>

View File

@ -0,0 +1,67 @@
<script setup>
import { goTo } from './Component';
import { useForm } from '@inertiajs/vue3';
import { onUpdated } from 'vue';
import Input from '@/Components/Dashboard/Form/Input.vue';
import EditModal from '@/Components/Dashboard/Modal/Edit.vue';
import Header from '@/Components/Dashboard/Modal/Elements/Header.vue';
const emit = defineEmits([
'close',
'switchModal'
]);
const props = defineProps({
show: Boolean,
model: Object
});
const form = useForm({});
const update = (id) => {
form.transform(data => ({
...props.model
})).put(route(goTo('update'), {id}),{
preserveScroll: true,
onSuccess: () => {
Notify.success(lang('updated'))
emit('switchModal')
}
});
}
</script>
<template>
<EditModal
:show="show"
@close="$emit('close')"
@update="update(model.id)"
>
<Header
:title="model.name"
/>
<div class="py-2 border-b">
<div class="p-4">
<form>
<div class="grid gap-6 mb-6 lg:grid-cols-2">
<Input
id="Nombre"
placeholder="name"
v-model="model.name"
:onError="form.errors.name"
required
/>
<Input
id="Descripción"
v-model="model.description"
:onError="form.errors.description"
required
/>
</div>
</form>
</div>
</div>
</EditModal>
</template>

View File

@ -0,0 +1,146 @@
<script setup>
import { transl, can, goTo } from "./Component";
import { ref, computed } from "vue";
import { Link } from "@inertiajs/vue3";
import ModalController from "@/Controllers/ModalController.js";
import SearcherController from "@/Controllers/SearcherController.js";
import SearcherHead from "@/Components/Dashboard/Searcher.vue";
import Table from "@/Components/Dashboard/Table.vue";
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue";
import DashboardLayout from "@/Layouts/DashboardLayout.vue";
import DestroyView from "./Destroy.vue";
import EditView from "./Edit.vue";
const props = defineProps({
mainRoleSkills: Object,
});
// Controladores
const Modal = new ModalController();
const Searcher = new SearcherController(goTo("index"));
// Variables de controladores
const destroyModal = ref(Modal.destroyModal);
const editModal = ref(Modal.editModal);
const modelModal = ref(Modal.modelModal);
const query = ref(Searcher.query);
const groupedRoles = computed(() => {
const groups = {};
props.mainRoleSkills.data?.forEach(item => {
const key = item.main_role?.id;
if(!groups[key]) groups[key] = { ...item, skills: []};
groups[key].skills.push(item);
})
return Object.values(groups);
});
</script>
<template>
<DashboardLayout :title="transl('system')">
<SearcherHead @search="Searcher.search">
<Link v-if="can('create')" :href="route(goTo('create'))">
<GoogleIcon
:title="$t('crud.create')"
class="btn-icon-primary"
name="add"
outline
/>
</Link>
</SearcherHead>
<div class="pt-2 w-full">
<Table
:items="mainRoleSkills"
@send-pagination="Searcher.searchWithPagination"
>
<template #head>
<th class="table-item" v-text="$t('Departamento')" />
<th class="table-item" v-text="$t('Rol Principal')" />
<th class="table-item" v-text="$t('Habilidades Puntuadas')" />
<th class="table-item w-44" v-text="$t('actions')" />
</template>
<template #body="{ items }">
<tr v-for="group in groupedRoles" :key="group.main_role.id">
<td class="table-item border">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold">
{{ group.main_role?.department?.name }}
</p>
</div>
</div>
</td>
<td class="table-item border">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold">
{{ group.main_role?.name }}
</p>
</div>
</div>
</td>
<div>
<td class="table-item border">
<div class="flex items-center text-sm">
<div>
<p v-for="skill in group.skills" :key="skill.id" class="font-semibold">
{{ skill.skill?.name }} - {{ skill.score?.alias }}
</p>
</div>
</div>
</td>
</div>
<td class="table-item border">
<div class="flex justify-center space-x-2">
<GoogleIcon
v-if="can('edit')"
:title="$t('crud.edit')"
class="btn-icon-primary"
name="edit"
outline
@click="Modal.switchEditModal(model)"
/>
<GoogleIcon
v-if="can('destroy')"
:title="$t('crud.destroy')"
class="btn-icon-primary"
name="delete"
outline
@click="Modal.switchDestroyModal(model)"
/>
</div>
</td>
</tr>
</template>
<template #empty>
<td class="table-item border">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold">
{{ $t("registers.empty") }}
</p>
</div>
</div>
</td>
<td class="table-item border">-</td>
<td class="table-item border">-</td>
</template>
</Table>
</div>
<EditView
v-if="can('edit')"
:show="editModal"
:model="modelModal"
@switchModal="Modal.switchShowEditModal"
@close="Modal.switchEditModal"
/>
<DestroyView
v-if="can('create')"
:show="destroyModal"
:model="modelModal"
@close="Modal.switchDestroyModal"
/>
</DashboardLayout>
</template>

View File

@ -0,0 +1,15 @@
import { t } from '@/Lang/i18n';
import { hasPermission } from '@/rolePermission.js';
// Obtener ruta
const goTo = (route) => `admin.skills.${route}`
// Obtener traducción del componente
const transl = (lang) => t(`skill.${lang}`)
// Determina si un usuario puede hacer algo no en base a los permisos
const can = (permission) => hasPermission(`users.${permission}`)
export{
can,
goTo,
transl
}

View File

@ -0,0 +1,89 @@
<script setup>
import { goTo, transl } from "./Component";
import { Link, useForm } from "@inertiajs/vue3";
import PrimaryButton from "@/Components/Dashboard/Button/Primary.vue";
import Input from "@/Components/Dashboard/Form/Input.vue";
import PageHeader from "@/Components/Dashboard/PageHeader.vue";
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue";
import DashboardLayout from "@/Layouts/DashboardLayout.vue";
import Selectable from "@/Components/Dashboard/Form/Selectable.vue";
defineProps({
departments: Object,
});
const form = useForm({
name: "",
description: "",
department_id: "",
});
const submit = () =>
form
.transform((data) => ({
...data,
department_id: form.department_id?.id,
}))
.post(route(goTo("store")), {
onSuccess: () => Notify.success(transl("create.onSuccess")),
onError: () => Notify.error(transl("create.onError")),
});
</script>
<template>
<DashboardLayout :title="transl('create.title')">
<PageHeader>
<Link :href="route(goTo('index'))">
<GoogleIcon
:title="$t('return')"
class="btn-icon-primary"
name="arrow_back"
outline
/>
</Link>
</PageHeader>
<div class="w-full pb-8">
<div class="mt-8">
<p v-text="transl('create.description')" />
</div>
</div>
<div class="w-full">
<form @submit.prevent="submit" class="grid gap-4 grid-cols-6">
<Input
id="Nombre"
class="col-span-2"
v-model="form.name"
:onError="form.errors.name"
autofocus
required
/>
<Input
id="Descrición"
class="col-span-2"
v-model="form.description"
:onError="form.errors.description"
autofocus
/>
<Selectable
id="department_id"
class="col-span-3"
v-model="form.department_id"
:options="departments"
:onError="form.errors.department_id"
:title="$t('department.title')"
required
/>
<div
class="col-span-6 flex flex-col items-center justify-end space-y-4 mt-4"
>
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
v-text="transl('create.title')"
/>
</div>
</form>
</div>
</DashboardLayout>
</template>

View File

@ -0,0 +1,43 @@
<script setup>
import { transl, goTo } from './Component'
import { router } from '@inertiajs/vue3';
import DestroyModal from '@/Components/Dashboard/Modal/Destroy.vue';
import Header from '@/Components/Dashboard/Modal/Elements/Header.vue';
const emit = defineEmits([
'close',
'switchModal'
]);
const props = defineProps({
show: Boolean,
model: Object
});
const destroy = (id) => router.delete(route(goTo('destroy'), {id}), {
preserveScroll: true,
onSuccess: () => {
props.model.pop;
Notify.success(transl('deleted'));
emit('close');
},
onError: () => {
Notify.info(transl('notFound'));
emit('close');
}
});
</script>
<template>
<DestroyModal
:show="show"
@close="$emit('close')"
@destroy="destroy(model.id)"
>
<Header
:title="model.name"
:subtitle="model.description"
/>
</DestroyModal>
</template>

View File

@ -0,0 +1,67 @@
<script setup>
import { goTo } from './Component';
import { useForm } from '@inertiajs/vue3';
import { onUpdated } from 'vue';
import Input from '@/Components/Dashboard/Form/Input.vue';
import EditModal from '@/Components/Dashboard/Modal/Edit.vue';
import Header from '@/Components/Dashboard/Modal/Elements/Header.vue';
const emit = defineEmits([
'close',
'switchModal'
]);
const props = defineProps({
show: Boolean,
model: Object
});
const form = useForm({});
const update = (id) => {
form.transform(data => ({
...props.model
})).put(route(goTo('update'), {id}),{
preserveScroll: true,
onSuccess: () => {
Notify.success(lang('updated'))
emit('switchModal')
}
});
}
</script>
<template>
<EditModal
:show="show"
@close="$emit('close')"
@update="update(model.id)"
>
<Header
:title="model.name"
/>
<div class="py-2 border-b">
<div class="p-4">
<form>
<div class="grid gap-6 mb-6 lg:grid-cols-2">
<Input
id="Nombre"
placeholder="name"
v-model="model.name"
:onError="form.errors.name"
required
/>
<Input
id="Descripción"
v-model="model.description"
:onError="form.errors.description"
required
/>
</div>
</form>
</div>
</div>
</EditModal>
</template>

View File

@ -0,0 +1,149 @@
<script setup>
import { transl, can, goTo } from './Component'
import { ref } from 'vue';
import { Link } from '@inertiajs/vue3';
import ModalController from '@/Controllers/ModalController.js';
import SearcherController from '@/Controllers/SearcherController.js';
import SearcherHead from '@/Components/Dashboard/Searcher.vue';
import Table from '@/Components/Dashboard/Table.vue';
import GoogleIcon from '@/Components/Shared/GoogleIcon.vue';
import DashboardLayout from '@/Layouts/DashboardLayout.vue';
import DestroyView from './Destroy.vue';
import EditView from './Edit.vue';
const props = defineProps({
skills: Object
});
// Controladores
const Modal = new ModalController();
const Searcher = new SearcherController(goTo('index'));
// Variables de controladores
const destroyModal = ref(Modal.destroyModal);
const editModal = ref(Modal.editModal);
const modelModal = ref(Modal.modelModal);
const query = ref(Searcher.query);
</script>
<template>
<DashboardLayout :title="transl('system')">
<SearcherHead @search="Searcher.search">
<Link
v-if="can('create')"
:href="route(goTo('create'))"
>
<GoogleIcon
:title="$t('crud.create')"
class="btn-icon-primary"
name="add"
outline
/>
</Link>
</SearcherHead>
<div class="pt-2 w-full">
<Table
:items="skills"
@send-pagination="Searcher.searchWithPagination"
>
<template #head>
<th
class="table-item"
v-text="$t('Nombre')"
/>
<th
class="table-item"
v-text="$t('Descripción')"
/>
<th
class="table-item"
v-text="$t('Departamento')"
/>
<th
class="table-item w-44"
v-text="$t('actions')"
/>
</template>
<template #body="{items}">
<tr v-for="model in items">
<td class="table-item border">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold">
{{ model.name }}
</p>
</div>
</div>
</td>
<td class="table-item border">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold">
{{ model.description }}
</p>
</div>
</div>
</td>
<td class="table-item border">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold">
{{ model.department?.name }}
</p>
</div>
</div>
</td>
<td class="table-item border">
<div class="flex justify-center space-x-2">
<GoogleIcon
v-if="can('edit')"
:title="$t('crud.edit')"
class="btn-icon-primary"
name="edit"
outline
@click="Modal.switchEditModal(model)"
/>
<GoogleIcon
v-if="can('destroy')"
:title="$t('crud.destroy')"
class="btn-icon-primary"
name="delete"
outline
@click="Modal.switchDestroyModal(model)"
/>
</div>
</td>
</tr>
</template>
<template #empty>
<td class="table-item border">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold">
{{ $t('registers.empty') }}
</p>
</div>
</div>
</td>
<td class="table-item border">-</td>
<td class="table-item border">-</td>
</template>
</Table>
</div>
<EditView
v-if="can('edit')"
:show="editModal"
:model="modelModal"
@switchModal="Modal.switchShowEditModal"
@close="Modal.switchEditModal"
/>
<DestroyView
v-if="can('create')"
:show="destroyModal"
:model="modelModal"
@close="Modal.switchDestroyModal"
/>
</DashboardLayout>
</template>

View File

@ -7,8 +7,11 @@
use App\Http\Controllers\Dashboard\NotificationController; use App\Http\Controllers\Dashboard\NotificationController;
use App\Http\Controllers\Admin\DepartmentController; use App\Http\Controllers\Admin\DepartmentController;
use App\Http\Controllers\Admin\MainRoleController; use App\Http\Controllers\Admin\MainRoleController;
use App\Http\Controllers\Admin\MainRoleSkillsController;
use App\Http\Controllers\Admin\SkillController;
use App\Http\Controllers\Developer\RoleController; use App\Http\Controllers\Developer\RoleController;
use App\Http\Controllers\Example\IndexController as ExampleIndexController; use App\Http\Controllers\Example\IndexController as ExampleIndexController;
use App\Models\MainRoleSkills;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/** /**
@ -17,7 +20,6 @@
* Rutas accesibles por todos los usuarios y no usuarios * Rutas accesibles por todos los usuarios y no usuarios
*/ */
Route::redirect('/', '/login'); Route::redirect('/', '/login');
/** /**
* Rutas del Dashboard * Rutas del Dashboard
* *
@ -59,6 +61,8 @@
Route::resource('scores', ScoreController::class); Route::resource('scores', ScoreController::class);
Route::resource('departments', DepartmentController::class); Route::resource('departments', DepartmentController::class);
Route::resource('mainRoles', MainRoleController::class); Route::resource('mainRoles', MainRoleController::class);
Route::resource('skills', SkillController::class);
Route::resource('mainRoleSkills', MainRoleSkillsController::class);
Route::prefix('/users')->name('users.')->group(function() Route::prefix('/users')->name('users.')->group(function()
{ {