Habilidades Puntuadas #2

Merged
juan.zapata merged 7 commits from Skills into main 2025-07-17 19:50:45 +00:00
41 changed files with 2167 additions and 353 deletions

View File

@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}

View File

@ -0,0 +1,151 @@
<?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\department;
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',
'skill:id,name',
'score:id,alias'
])
->paginate(config('app.pagination'));
$departments = department::select(['id', 'name', 'description'])->orderBy('name', 'ASC')->get();
$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('index', [
'mainRoleSkills' => $mainRoleSkills,
'departments' => $departments,
'mainRoles' => $mainRoles,
'skills' => $skills,
'scores' => $scores,
]);
}
public function create()
{
$roleId = request()->get('role_id');
$departmentId = request()->get('department_id');
// Obtener el rol seleccionado y su departamento
$selectedRole = null;
$selectedDepartment = null;
if ($roleId) {
$selectedRole = mainRole::with('department:id,name')->find($roleId);
if ($selectedRole && $selectedRole->department) {
$selectedDepartment = $selectedRole->department;
}
} elseif ($departmentId) {
$selectedDepartment = department::find($departmentId);
}
$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,
'selectedRole' => $selectedRole,
'selectedDepartment' => $selectedDepartment
]);
}
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 edit(MainRoleSkills $mainRoleSkills)
{
$mainRoleSkills->load(['mainRole.department', 'skill.department', 'score']);
$scores = Score::orderBy('alias', 'ASC')->get();
return $this->vuew('edit', [
'mainRoleSkills' => $mainRoleSkills,
'scores' => $scores,
]);
}
public function update(UpdateMainRoleSkills $request, $id)
{
$mainRoleSkills = MainRoleSkills::findOrFail($id);
$mainRoleSkills->update([
'scored_id' => $request->scored_id,
]);
return redirect()->back();
}
public function destroy($id)
{
try {
$mainRoleSkills = MainRoleSkills::findOrFail($id);
$mainRoleSkills->delete();
return $this->index();
} catch (\Throwable $th) {
Log::error($th->getMessage());
return response()->json(['error' => 'Error al eliminar las habilidades']);
}
}
}

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,38 @@
<?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 [
'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',
'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

@ -34,4 +34,9 @@ public function mainRoles()
{
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
*/
protected $table = 'main_role';
protected $table = 'main_roles';
/**
* Atributos llenables masivamente
@ -40,4 +40,14 @@ public function department()
{
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
{
Schema::create('main_role', function (Blueprint $table) {
Schema::create('main_roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('description')->nullable();
@ -25,6 +25,6 @@ public function up(): 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,32 @@
<?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();
$table->unique(['main_role_id', 'skill_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('main_role_skills');
}
};

View File

@ -0,0 +1,32 @@
<script setup>
defineEmits(['select']);
const props = defineProps({
departments: Object,
});
</script>
<template>
<div class="w-full">
<div class="mb-6 mt-12">
<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,47 @@
<script setup>
import { computed } from 'vue';
const emit = defineEmits(['select']);
const props = defineProps({
mainRoles: Object,
selectedDepartment: Object,
});
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 mt-12">
<h2 class="text-xl font-semibold mb-2">
{{ $t('mainRole.inDepartment') }}
</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,177 @@
<script setup>
import { ref, computed } from "vue";
import PrimaryButton from "@/Components/Dashboard/Button/Primary.vue";
import Selectable from "@/Components/Dashboard/Form/Selectable.vue";
const emit = defineEmits(["submit"]);
const props = defineProps({
form: {
type: Object,
required: true,
},
skills: {
type: Array,
required: true,
},
scores: {
type: Array,
required: true,
},
selectedDepartment: {
type: Object,
required: true,
},
selectedMainRole: {
type: Object,
required: true,
},
});
const skillId = ref("");
const scoredId = ref("");
const todos = ref([]);
function addTodo() {
if (skillId.value && scoredId.value) {
const selectedSkill = props.skills.find(
(skill) => skill.id === (skillId.value.id || skillId.value)
);
const selectedScore = props.scores.find(
(score) => score.id === (scoredId.value.id || scoredId.value)
);
if (selectedSkill && selectedScore) {
// Verificar que no esté duplicada
const isDuplicate = todos.value.some(
(todo) => todo.skill_id === selectedSkill.id
);
if (!isDuplicate) {
const newItem = {
skill_id: selectedSkill.id,
scored_id: selectedScore.id,
skill_name: selectedSkill.name,
score_alias: selectedScore.alias,
};
todos.value.push(newItem);
// Limpiar campos
skillId.value = "";
scoredId.value = "";
}
}
}
}
function removeTodo(index) {
todos.value.splice(index, 1);
}
const isFormValid = computed(() => {
return todos.value.length > 0;
});
const submitForm = () => {
if (isFormValid.value) {
emit("submit", todos.value);
}
};
</script>
<template>
<div class="w-full">
<div class="mb-6">
<h2 class="text-xl font-semibold mb-2">
Asignar Habilidades al Rol: {{ selectedMainRole.name }}
</h2>
<p class="text-gray-600">Departamento: {{ selectedDepartment.name }}</p>
</div>
<div
v-if="form.errors.skills"
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 mt-4"
>
{{ form.errors.skills }}
</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="skills"
: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"
:disabled="!skillId || !scoredId"
class="rounded-lg px-4 py-2 bg-primary text-white border disabled:opacity-50 disabled:cursor-not-allowed"
>
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">{{ todo.skill_name }}</div>
<div class="text-sm text-gray-600">
Puntuación: {{ todo.score_alias }}
</div>
</div>
<button
@click="removeTodo(index)"
class="rounded-lg px-4 py-2 bg-red-600 text-white hover:bg-red-700 transition-colors"
>
Quitar
</button>
</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>Crear Asignación</span>
</PrimaryButton>
</div>
</div>
</template>

View File

@ -0,0 +1,135 @@
<script setup>
import { computed, ref } from "vue";
import { Link } from "@inertiajs/vue3";
import { can, goTo } from "@/Pages/Admin/MainRoleSkills/Component";
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue";
import ModalController from "@/Controllers/ModalController.js";
import DestroyView from "@/Pages/Admin/MainRoleSkills/Destroy.vue";
import EditView from '@/Pages/Admin/MainRoleSkills/Edit.vue';
const emit = defineEmits(["select"]);
const props = defineProps({
mainRoleSkills: Array,
selectedRole: Object,
scores: {
type: Array,
default: () => []
}
});
// Controlador de modal
const Modal = new ModalController();
const destroyModal = ref(Modal.destroyModal);
const editModal = ref(Modal.editModal);
const modelModal = ref(Modal.modelModal);
const roleSkills = computed(() => {
if (!props.mainRoleSkills || !props.selectedRole) {
return [];
}
return props.mainRoleSkills.filter(
(roleSkill) => roleSkill.main_role_id === props.selectedRole.id
);
});
</script>
<template>
<div class="w-full">
<div class="mb-6 mt-12">
<div class="flex gap-6">
<h2 class="text-xl font-semibold mb-2">
{{ $t("skill.assignment.title") }}
</h2>
<Link
v-if="can('create')"
:href="
route(goTo('create'), {
role_id: selectedRole.id,
department_id:
selectedRole.department?.id || selectedRole.department_id,
})
"
>
<GoogleIcon
:title="$t('crud.create')"
class="btn-icon-primary"
name="add"
outline
/>
</Link>
</div>
<p class="text-gray-600">
Habilidades asignadas al rol: {{ selectedRole.name }}
</p>
</div>
<div
v-if="roleSkills.length > 0"
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
>
<div
v-for="roleSkill in roleSkills"
:key="roleSkill.id"
class="bg-white p-4 rounded-lg shadow hover:shadow-lg transition-shadow duration-200 cursor-pointer border"
@click="emit('select', roleSkill)"
>
<h3 class="text-lg font-medium mb-2 text-blue-600">
{{ roleSkill.skill.name }}
</h3>
<div class="text-sm text-gray-600">
<span class="font-semibold">Puntuación:</span>
{{ roleSkill.score.alias }}
</div>
<p
v-if="roleSkill.skill.description"
class="text-xs text-gray-500 mt-2"
>
{{ roleSkill.skill.description }}
</p>
<div class="flex items-center justify-end mt-4 gap-2">
<GoogleIcon
v-if="can('edit')"
:title="$t('crud.edit')"
class="btn-icon-danger w-6 h-6 bg-blue-500 text-white rounded hover:bg-blue-600"
name="edit"
@click.stop="Modal.switchEditModal(roleSkill)"
/>
<GoogleIcon
v-if="can('destroy')"
:title="$t('crud.destroy')"
class="btn-icon-danger w-6 h-6 bg-red-500 text-white rounded hover:bg-red-600"
name="delete"
@click.stop="Modal.switchDestroyModal(roleSkill)"
/>
</div>
</div>
</div>
<div v-else class="text-center py-12">
<p class="text-gray-500 text-lg">
No hay habilidades asignadas a este rol
</p>
<p class="text-gray-400 text-sm mt-2">
Las habilidades se pueden asignar desde la sección de creación
</p>
</div>
<EditView
v-if="can('edit')"
:show="editModal"
:model="modelModal"
:scores="scores"
@close="Modal.switchEditModal"
@switchModal="Modal.switchEditModal"
/>
<DestroyView
v-if="can('destroy')"
:show="destroyModal"
:model="modelModal"
@close="Modal.switchDestroyModal"
/>
</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

@ -46,34 +46,34 @@ const onRemove = () => {
</script>
<template>
<div class="flex flex-col">
<div class="flex flex-col relative">
<label v-if="title" class="block mb-2 text-sm font-medium text-gray-900">
{{title}} <span class="text-red-500" v-if="required">*</span> <slot name="label-icon" />
</label>
<VueMultiselect
v-model="value"
ref="multiselect"
:options="options"
:mode="mode"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="true"
selectedLabel="Seleccionado"
selectLabel="Seleccionar"
deselectLabel="Remover"
:placeholder="placeholder"
:label="label"
:track-by="trackBy"
:required="required"
@select="onChange"
@remove="onRemove"
>
<template #noOptions>
{{ $t('noRecords') }}
</template>
<VueMultiselect
v-model="value"
ref="multiselect"
:options="options"
:mode="mode"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="true"
selectedLabel="Seleccionado"
selectLabel="Seleccionar"
deselectLabel="Remover"
:placeholder="placeholder"
:label="label"
:track-by="trackBy"
:required="required"
@select="onChange"
@remove="onRemove"
>
<template #noOptions>
{{ $t('noRecords') }}
</template>
</VueMultiselect>
<p v-show="onError" class="text-sm text-red-600">
{{ onError }}
</p>
</div>
</template>
</template>

View File

@ -1,10 +1,10 @@
<script setup>
import DialogModal from '@/Components/Dashboard/DialogModal.vue';
import SecondaryButton from '@/Components/Dashboard/Button/Secondary.vue';
const emit = defineEmits([
'close',
'update'
'update'
]);
const props = defineProps({
@ -26,7 +26,7 @@ const props = defineProps({
</template>
<template #content>
<div class="w-full right-0 mt-2">
<div class="rounded overflow-hidden">
<div class="rounded">
<slot />
</div>
</div>
@ -34,15 +34,15 @@ const props = defineProps({
<template #footer>
<div class="space-x-2">
<slot name="buttons" />
<SecondaryButton
<SecondaryButton
@click="$emit('update')"
v-text="$t('update')"
/>
<SecondaryButton
@click="$emit('close')"
@click="$emit('close')"
v-text="$t('cancel')"
/>
</div>
</template>
</DialogModal>
</template>
</template>

View File

@ -1,7 +1,7 @@
<script setup>
import SecondaryButton from '@/Components/Dashboard/Button/Secondary.vue';
import DialogModal from '@/Components/Dashboard/DialogModal.vue';
const emit = defineEmits([
'close',
'edit'
@ -35,16 +35,16 @@ const props = defineProps({
<template #footer>
<div class="space-x-2">
<slot name="buttons" />
<SecondaryButton
<SecondaryButton
v-if="editable"
@click="$emit('edit')"
v-text="$t('update')"
/>
<SecondaryButton
@click="$emit('close')"
@click="$emit('close')"
v-text="$t('close')"
/>
</div>
</template>
</DialogModal>
</template>
</template>

View File

@ -1,257 +1,318 @@
export default {
'&':'y',
account: {
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.',
description:'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.',
title:'Eliminar cuenta',
},
email: {
notifySendVerification:'Se ha enviado un nuevo enlace de verificación a su dirección de correo electrónico.',
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',
password: {
description:'Asegúrese de que su cuenta utiliza una contraseña larga y aleatoria para estar seguro.',
new:'Nueva contraseña',
reset:'Restaurar contraseña',
secure:'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: {
description:'Actualice la información del perfil de su cuenta y su dirección de correo electrónico.',
title:'Información del perfil',
},
sessions: {
confirm:'Por favor, introduzca su contraseña para confirmar que desea salir de sus otras sesiones de navegación en todos sus dispositivos.',
description: 'Gestiona y cierra tus sesiones activas en otros navegadores y dispositivos.',
last:'Último activo',
logout:'Cerrar otras 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: {
codes:{
regenerate:'Regenerar los 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.',
},
description:'Añada seguridad adicional a su cuenta mediante la autenticación de dos factores.',
isEnable:'Ha activado la autenticación de dos factores.',
isNotEnable:{
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',
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.',
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.',
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.',
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: {
code: 'Código de recuperación',
useAuth: 'Utilizar un código de autentificación',
useCode: 'Utiliza un código de recuperación',
},
title:'Autenticación de dos factores',
},
"&": "y",
account: {
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.",
description: "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.",
title: "Eliminar cuenta",
},
actions:'Acciones',
auth: {
forgotPassword: {
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.',
sendLink: 'Enviar enlace de recuperación por correo',
title: 'Contraseña olvidada',
},
login: 'Iniciar sesión',
logout: 'Cerrar sesión',
register: {
already: '¿Ya estas registrado?',
me: 'Registrarme',
},
remember: 'Recuerdame',
email: {
notifySendVerification:
"Se ha enviado un nuevo enlace de verificación a su dirección de correo electrónico.",
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.",
},
code:'Código',
cancel:'Cancelar',
changes:'Cambios',
changelogs: {
title:'Historial de cambios',
description: 'Lista de los cambios realizados al sistema.',
manage: "Administrar cuenta",
password: {
description:
"Asegúrese de que su cuenta utiliza una contraseña larga y aleatoria para estar seguro.",
new: "Nueva contraseña",
reset: "Restaurar contraseña",
secure:
"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.",
},
close:"Cerrar",
confirm:'Confirmar',
copyright:'Todos los derechos reservados.',
contact:'Contacto',
crud: {
create: 'Nuevo registro',
edit: 'Editar registro',
destroy: 'Eliminar registro',
show: 'Más detalles',
profile: {
description:
"Actualice la información del perfil de su cuenta y su dirección de correo electrónico.",
title: "Información del perfil",
},
date: 'Fecha',
delete:{
confirm: 'Al presionar ELIMINAR el registro se eliminará permanentemente y no podrá recuperarse.',
title: 'Eliminar',
sessions: {
confirm:
"Por favor, introduzca su contraseña para confirmar que desea salir de sus otras sesiones de navegación en todos sus dispositivos.",
description:
"Gestiona y cierra tus sesiones activas en otros navegadores y dispositivos.",
last: "Último activo",
logout: "Cerrar otras 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",
},
deleted:'Eliminado',
description:'Descripción',
details:'Detalles',
disable:'Deshabilitar',
disabled:'Deshabilitado',
done:'Hecho.',
edit:'Editar',
email:{
title:'Correo',
verification:'Verificar correo'
},
enable:'Habilitar',
endDate:'Fecha Fin',
event:'Evento',
help: {
description:'A continuación se lista la iconografía para entender el funcionamiento del sistema.',
home: 'Volver a la pagina de inicio.',
title:'Ayuda',
},
history: {
title:'Historial de acciones',
description:'Historial de acciones realizadas por los usuarios en orden cronológico.'
},
home:'Inicio',
hour:'Hora',
icon:'Icono',
maternal:'Apellido materno',
menu:'Menú',
name:'Nombre',
noRecords:'Sin registros',
notifications: {
readed:'Marcar como leído',
deleted:'Notificación eliminada',
description:'Notificaciones del usuario',
notFound:'Notificación no encontrada',
title:'Notificaciones',
},
password:'Contraseña',
passwordConfirmation:'Confirmar contraseña',
passwordCurrent:'Contraseña actual',
passwordReset:'Restaurar contraseña',
paternal:'Apellido paterno',
phone:'Teléfono',
photo: {
new: 'Seleccionar una nueva foto',
remove:'Remover foto',
title:'Foto',
},
profile:'Perfil',
readed:'Leído',
register: {
agree:'Estoy de acuerdo con los',
privacy:'Política de Privacidad',
signUp:'Registrarme',
terms:'Términos de Servicio',
},
registers:{
title:'Registros',
empty:'Sin registros',
},
remove: 'Remover',
return: 'Regresar',
role:'Rol',
roles:{
create: {
title: 'Crear rol',
description: 'Estos roles serán usados para dar permisos en el sistema.',
onSuccess: 'Rol creado exitosamente',
onError: 'Error al crear el role',
},
deleted:'Rol eliminado',
title: 'Roles',
},
save:'Guardar',
saved:'¡Guardado!',
search:'Buscar',
selected: 'Seleccionado',
select: 'Seleccionar',
setting: 'Configuración',
show: {
all:'Mostrar todo',
title:'Mostrar',
},
startDate:'Fecha de inicio',
status:'Estado',
terms: {
agree:'Estoy de acuerdo con los',
privacy:'Política de privacidad',
service:'Términos de servicio',
},
unknown:'Desconocido',
update:'Actualizar',
updated:'Actualizado',
updateFail:'Error al actualizar',
unreaded:'No leído',
user:'Usuario',
users:{
create:{
title:'Crear usuario',
description:'Permite crear nuevos usuarios. No olvides otorgarle roles para que pueda acceder a las partes del sistema deseados.',
onSuccess:'Usuario creado',
onError:'Ocurrió un error al crear el usuario'
},
deleted:'Usuario eliminado',
notFount:'Usuario no encontrado',
password: {
description:'Permite actualizar las contraseñas de los usuarios sobreescribiendola.',
title:'Actualizar contraseña',
},
roles: {
description:'Actualiza los roles de los usuarios, permitiendo o denegando los accesos a determinadas áreas.',
error:{
min:'Seleccionar mínimo un role'
},
title:'Roles de usuario',
},
menu:'Menú de usuario',
select:'Seleccionar un usuario',
settings:'Ajustes del usuario',
system:'Usuarios del sistema',
title:'Usuarios',
},
scores:{
system: 'Sistema de puntuación',
create:{
title:'Crear puntuación',
description:'Permite crear nuevas puntuaciones para los usuarios.',
onSuccess:'Puntuación creada exitosamente',
onError:'Error al crear la puntuación',
}
},
department:{
system: 'Sistema de departamentos',
title: 'Departamentos',
create:{
title:'Crear departamento',
description:'Permite crear nuevos departamentos para los usuarios.',
onSuccess:'Departamento creado exitosamente',
onError:'Error al crear el departamento',
}
},
mainRole: {
system: 'Sistema de roles principales',
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',
twoFactor: {
codes: {
regenerate: "Regenerar los 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.",
},
description:
"Añada seguridad adicional a su cuenta mediante la autenticación de dos factores.",
isEnable: "Ha activado la autenticación de dos factores.",
isNotEnable: {
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",
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.",
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.",
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.",
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: {
code: "Código de recuperación",
useAuth: "Utilizar un código de autentificación",
useCode: "Utiliza un código de recuperación",
},
title: "Autenticación de dos factores",
},
version:'Versión',
}
},
actions: "Acciones",
auth: {
forgotPassword: {
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.",
sendLink: "Enviar enlace de recuperación por correo",
title: "Contraseña olvidada",
},
login: "Iniciar sesión",
logout: "Cerrar sesión",
register: {
already: "¿Ya estas registrado?",
me: "Registrarme",
},
remember: "Recuerdame",
},
code: "Código",
cancel: "Cancelar",
changes: "Cambios",
changelogs: {
title: "Historial de cambios",
description: "Lista de los cambios realizados al sistema.",
},
close: "Cerrar",
confirm: "Confirmar",
copyright: "Todos los derechos reservados.",
contact: "Contacto",
crud: {
create: "Nuevo registro",
edit: "Editar registro",
destroy: "Eliminar registro",
show: "Más detalles",
},
date: "Fecha",
delete: {
confirm:
"Al presionar ELIMINAR el registro se eliminará permanentemente y no podrá recuperarse.",
title: "Eliminar",
},
deleted: "Eliminado",
description: "Descripción",
details: "Detalles",
disable: "Deshabilitar",
disabled: "Deshabilitado",
done: "Hecho.",
edit: "Editar",
email: {
title: "Correo",
verification: "Verificar correo",
},
enable: "Habilitar",
endDate: "Fecha Fin",
event: "Evento",
help: {
description:
"A continuación se lista la iconografía para entender el funcionamiento del sistema.",
home: "Volver a la pagina de inicio.",
title: "Ayuda",
},
history: {
title: "Historial de acciones",
description:
"Historial de acciones realizadas por los usuarios en orden cronológico.",
},
home: "Inicio",
hour: "Hora",
icon: "Icono",
maternal: "Apellido materno",
menu: "Menú",
name: "Nombre",
noRecords: "Sin registros",
notifications: {
readed: "Marcar como leído",
deleted: "Notificación eliminada",
description: "Notificaciones del usuario",
notFound: "Notificación no encontrada",
title: "Notificaciones",
},
password: "Contraseña",
passwordConfirmation: "Confirmar contraseña",
passwordCurrent: "Contraseña actual",
passwordReset: "Restaurar contraseña",
paternal: "Apellido paterno",
phone: "Teléfono",
photo: {
new: "Seleccionar una nueva foto",
remove: "Remover foto",
title: "Foto",
},
profile: "Perfil",
readed: "Leído",
register: {
agree: "Estoy de acuerdo con los",
privacy: "Política de Privacidad",
signUp: "Registrarme",
terms: "Términos de Servicio",
},
registers: {
title: "Registros",
empty: "Sin registros",
},
remove: "Remover",
return: "Regresar",
role: "Rol",
roles: {
create: {
title: "Crear rol",
description: "Estos roles serán usados para dar permisos en el sistema.",
onSuccess: "Rol creado exitosamente",
onError: "Error al crear el role",
},
deleted: "Rol eliminado",
title: "Roles",
},
save: "Guardar",
saved: "¡Guardado!",
search: "Buscar",
selected: "Seleccionado",
select: "Seleccionar",
setting: "Configuración",
show: {
all: "Mostrar todo",
title: "Mostrar",
},
startDate: "Fecha de inicio",
status: "Estado",
terms: {
agree: "Estoy de acuerdo con los",
privacy: "Política de privacidad",
service: "Términos de servicio",
},
unknown: "Desconocido",
update: "Actualizar",
updated: "Actualizado",
updateFail: "Error al actualizar",
unreaded: "No leído",
user: "Usuario",
users: {
create: {
title: "Crear usuario",
description:
"Permite crear nuevos usuarios. No olvides otorgarle roles para que pueda acceder a las partes del sistema deseados.",
onSuccess: "Usuario creado",
onError: "Ocurrió un error al crear el usuario",
},
deleted: "Usuario eliminado",
notFount: "Usuario no encontrado",
password: {
description:
"Permite actualizar las contraseñas de los usuarios sobreescribiendola.",
title: "Actualizar contraseña",
},
roles: {
description:
"Actualiza los roles de los usuarios, permitiendo o denegando los accesos a determinadas áreas.",
error: {
min: "Seleccionar mínimo un role",
},
title: "Roles de usuario",
},
menu: "Menú de usuario",
select: "Seleccionar un usuario",
settings: "Ajustes del usuario",
system: "Usuarios del sistema",
title: "Usuarios",
},
scores: {
system: "Sistema de puntuación",
title: "Puntuaciones",
create: {
title: "Crear puntuación",
description: "Permite crear nuevas puntuaciones para los usuarios.",
onSuccess: "Puntuación creada exitosamente",
onError: "Error al crear la puntuación",
},
},
department: {
system: "Sistema de departamentos",
title: "Departamentos",
create: {
title: "Crear departamento",
description: "Permite crear nuevos departamentos para los usuarios.",
onSuccess: "Departamento creado exitosamente",
onError: "Error al crear el departamento",
},
select:{
title: "Seleccionar un departamento",
description: "Seleccione un departamento para filtrar los roles.",
}
},
mainRole: {
title: "Roles principales",
inDepartment: "Roles principales en el departamento",
system: "Sistema de roles principales",
select:{
description: "Seleccione un 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",
},
},
skill: {
title: "Habilidades",
assignTo: "Asignar a rol principal",
system: "Sistema de habilidades",
assignment: {
title: "Habilidades del Rol",
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"
to="admin.mainRoles.index"
/>
<Link
icon="psychology"
name="Habilidades"
to="admin.skills.index"
/>
<Link
icon="leaderboard"
name="Scores"
to="admin.scores.index"
/>
<Link
icon="sports_handball"
name="Roles Puntuados"
to="admin.mainRoleSkills.index"
/>
<Link
icon="history"
name="changelogs.title"

View File

@ -65,7 +65,7 @@ const submit = () => form.post(route(goTo('store')), {
required
/>
<Input
id="Descrición"
id="Descripción"
class="col-span-2"
v-model="form.description"
:onError="form.errors.description"

View File

@ -1,73 +1,64 @@
<script setup>
import { goTo } from './Component';
import { useForm } from '@inertiajs/vue3';
import { onUpdated } from 'vue';
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';
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 emit = defineEmits(["close", "switchModal"]);
const props = defineProps({
show: Boolean,
model: Object
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')
}
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.alias"
/>
<div class="py-2 border-b">
<div class="p-4">
<form>
<div class="grid gap-6 mb-6 lg:grid-cols-2">
<Input
id="Alias"
placeholder="Alias"
v-model="model.alias"
:onError="form.errors.alias"
required
/>
<Input
id="Valor"
v-model="model.value"
:onError="form.errors.value"
required
/>
<Input
id="Descripción"
v-model="model.description"
:onError="form.errors.description"
required
/>
</div>
</form>
</div>
</div>
</EditModal>
<EditModal :show="show" @close="$emit('close')" @update="update(model.id)">
<Header :title="model.alias" />
<div class="py-2 border-b">
<div class="p-4">
<form>
<div class="grid gap-6 mb-6 lg:grid-cols-2">
<Input
id="Alias"
placeholder="Alias"
v-model="model.alias"
:onError="form.errors.alias"
required
/>
<Input
id="Valor"
v-model="model.value"
:onError="form.errors.value"
required
/>
<Input
id="Descripción"
v-model="model.description"
:onError="form.errors.description"
required
/>
</div>
</form>
</div>
</div>
</EditModal>
</template>

View File

@ -54,7 +54,7 @@ const submit = () => form.post(route(goTo('store')), {
required
/>
<Input
id="Descrición"
id="Descripción"
class="col-span-2"
v-model="form.description"
:onError="form.errors.description"

View File

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

View File

@ -59,7 +59,7 @@ const submit = () =>
required
/>
<Input
id="Descrición"
id="Descripción"
class="col-span-2"
v-model="form.description"
:onError="form.errors.description"

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,169 @@
<script setup>
import { goTo, transl } from "./Component";
import { Link, useForm } from "@inertiajs/vue3";
import { ref, computed, onMounted } from "vue";
import PageHeader from "@/Components/Dashboard/PageHeader.vue";
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue";
import DashboardLayout from "@/Layouts/DashboardLayout.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: {
type: Array,
default: () => []
},
skills: {
type: Array,
default: () => []
},
scores: {
type: Array,
default: () => []
},
selectedRole: {
type: Object,
default: null
},
selectedDepartment: {
type: Object,
default: null
}
});
const selectedDepartment = ref(props.selectedDepartment);
const selectedMainRole = ref(props.selectedRole);
const form = useForm({
main_role_id: props.selectedRole?.id || '',
skills: []
});
// Obtener departamentos únicos
const departments = computed(() => {
const uniqueDepartments = [];
const seenIds = new Set();
props.mainRoles.forEach(role => {
if (role.department && !seenIds.has(role.department.id)) {
seenIds.add(role.department.id);
uniqueDepartments.push(role.department);
}
});
return uniqueDepartments;
});
// Filtrar habilidades por departamento seleccionado
const departmentSkills = computed(() => {
if (!selectedDepartment.value) return [];
return props.skills.filter(skill =>
skill.department_id === selectedDepartment.value.id
);
});
const handleDepartmentSelect = (department) => {
selectedDepartment.value = department;
selectedMainRole.value = null;
form.main_role_id = '';
};
const handleRoleSelect = (role) => {
selectedMainRole.value = role;
form.main_role_id = role.id;
};
const submit = (skillsData) => {
form.skills = skillsData;
form.post(route(goTo("store")), {
onSuccess: () => {
// Redireccionar al index
},
onError: () => {
// Mostrar mensaje de error
},
});
};
const goBack = () => {
if (selectedMainRole.value) {
selectedMainRole.value = null;
form.main_role_id = '';
} else if (selectedDepartment.value) {
selectedDepartment.value = null;
}
};
onMounted(() => {
// Si tenemos rol seleccionado, asegurar que el departamento también esté seleccionado
if (props.selectedRole && props.selectedRole.department) {
selectedDepartment.value = props.selectedRole.department;
selectedMainRole.value = props.selectedRole;
form.main_role_id = props.selectedRole.id;
}
});
</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">
<!-- Navegación hacia atrás -->
<div v-if="selectedDepartment || selectedMainRole" class="mb-6">
<button
@click="goBack"
class="text-primary hover:underline flex items-center gap-2"
>
<GoogleIcon name="arrow_back" class="w-4 h-4" />
<span v-if="selectedMainRole">Cambiar rol principal</span>
<span v-else-if="selectedDepartment">Cambiar departamento</span>
</button>
</div>
<!-- Paso 1: Selector de Departamento -->
<DepartmentSelector
v-if="!selectedDepartment"
:departments="departments"
@select="handleDepartmentSelect"
/>
<!-- Paso 2: Selector de Rol Principal -->
<MainRoleSelector
v-else-if="selectedDepartment && !selectedMainRole"
:selectedDepartment="selectedDepartment"
:mainRoles="mainRoles"
@select="handleRoleSelect"
/>
<!-- Paso 3: Asignación de Habilidades -->
<SkillAssignment
v-else-if="selectedDepartment && selectedMainRole"
:form="form"
:skills="departmentSkills"
:scores="scores"
:selectedDepartment="selectedDepartment"
:selectedMainRole="selectedMainRole"
@submit="submit"
/>
</div>
</DashboardLayout>
</template>

View File

@ -0,0 +1,42 @@
<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.skill.name"
/>
</DestroyModal>
</template>

View File

@ -0,0 +1,87 @@
<script setup>
import { goTo } from "./Component";
import { router, useForm } from "@inertiajs/vue3";
import Selectable from "@/Components/Dashboard/Form/Selectable.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,
scores: Array,
});
const form = useForm({
scored_id: "",
});
const update = (id) => {
form
.transform((data) => ({
scored_id:
typeof data.scored_id === "object" ? data.scored_id.id : data.scored_id,
}))
.put(route(goTo("update"), { id }), {
preserveScroll: true,
onSuccess: () => {
Notify.success(lang("updated"));
emit("switchModal");
router.reload();
},
});
};
</script>
<template>
<EditModal :show="show" @close="$emit('close')" @update="update(model.id)">
<Header :title="model.main_role?.name" />
<div class="py-2 border-b">
<div class="p-4">
<form>
<div class="grid gap-6 mb-6 overflow-auto">
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="bg-gray-50 text-gray-700 mb-2 font-medium">
Información:
</h4>
<div class="text-sm text-gray-600 space-y-1">
<p>
<span class="font-medium">Rol: </span
>{{ model.main_role?.name }}
</p>
<p>
<span class="font-medium">Departamento: </span>
{{ model.main_role?.department?.name }}
</p>
<p>
<span class="font-medium">Habilidad: </span
>{{ model.skill?.name }}
</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
Cambiar Puntuación
</label>
<Selectable
id="scored_id"
v-model="form.scored_id"
:options="scores"
:onError="form.errors.scored_id"
label="alias"
track-by="id"
required
/>
<p class="text-xs text-gray-500 mt-1">
Puntuación actual:
<span class="font-medium"> {{ model.score?.alias }}</span>
</p>
</div>
</div>
</form>
</div>
</div>
</EditModal>
</template>

View File

@ -0,0 +1,92 @@
<script setup>
import { transl } from "./Component";
import { ref, computed } from "vue";
import DashboardLayout from "@/Layouts/DashboardLayout.vue";
import DepartmentSelector from "@/Components/App/DepartmentSelector.vue";
import MainRoleSelector from "@/Components/App/MainRoleSelector.vue";
import SkillRole from "@/Components/App/SkillsRole.vue";
import PageHeader from "@/Components/Dashboard/PageHeader.vue";
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue";
defineEmits(["select"]);
const props = defineProps({
mainRoleSkills: Object,
departments: Object,
mainRoles: Object,
skills: Object,
scores: Object,
});
const selectedDepartment = ref(null);
const selectedRole = ref(null);
const filteredMainRoleSkills = computed(() => {
if (!props.mainRoleSkills?.data || !selectedRole.value) {
return [];
}
return props.mainRoleSkills.data.filter(roleSkill =>
roleSkill.main_role_id === selectedRole.value.id
);
});
const handleDepartmentSelect = (department) => {
selectedDepartment.value = department;
selectedRole.value= null;
};
const handleRoleSelect = (role) => {
selectedRole.value = role;
};
const handleSkillSelect = (roleSkill) => {
console.log('Habilidad de rol seleccionada:', roleSkill);
}
const goBack = () => {
if (selectedRole.value) {
selectedRole.value = null;
} else if (selectedDepartment.value) {
selectedDepartment.value = null;
}
};
</script>
<template>
<DashboardLayout :title="transl('system')">
<PageHeader>
<button @click="goBack">
<GoogleIcon
:title="$t('return')"
class="btn-icon-primary"
name="arrow_back"
outline
/>
</button>
</PageHeader>
<!-- Selector de Departamento -->
<DepartmentSelector
v-if="!selectedDepartment"
:departments="departments"
@select="handleDepartmentSelect"
/>
<!-- Selector de Roles Principales -->
<MainRoleSelector
v-if="selectedDepartment"
:selectedDepartment="selectedDepartment"
:mainRoles="mainRoles"
@select="handleRoleSelect"
/>
<SkillRole
v-if="selectedRole"
:mainRoleSkills="filteredMainRoleSkills"
:selectedRole="selectedRole"
:scores="scores"
@select="handleSkillSelect"
/>
</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="Descripció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\Admin\DepartmentController;
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\Example\IndexController as ExampleIndexController;
use App\Models\MainRoleSkills;
use Illuminate\Support\Facades\Route;
/**
@ -17,7 +20,6 @@
* Rutas accesibles por todos los usuarios y no usuarios
*/
Route::redirect('/', '/login');
/**
* Rutas del Dashboard
*
@ -55,10 +57,12 @@
'auth:sanctum',
config('jetstream.auth_session')
])->group(function () {
Route::resource('users', UserController::class);
Route::resource('scores', ScoreController::class);
Route::resource('departments', DepartmentController::class);
Route::resource('mainRoles', MainRoleController::class);
Route::resource('users', UserController::class);
Route::resource('scores', ScoreController::class);
Route::resource('departments', DepartmentController::class);
Route::resource('mainRoles', MainRoleController::class);
Route::resource('skills', SkillController::class);
Route::resource('mainRoleSkills', MainRoleSkillsController::class);
Route::prefix('/users')->name('users.')->group(function()
{