Habilidades puntuadas terminado

This commit is contained in:
Juan Felipe Zapata Moreno 2025-07-17 13:48:50 -06:00
parent 24a1e18dc3
commit d96348bbf6
10 changed files with 198 additions and 128 deletions

View File

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

View File

@ -43,6 +43,7 @@ public function index()
->orWhereIn('scored_id', $scores) ->orWhereIn('scored_id', $scores)
->with([ ->with([
'mainRole:id,name,department_id', 'mainRole:id,name,department_id',
'mainRole.department:id,name',
'skill:id,name', 'skill:id,name',
'score:id,alias' 'score:id,alias'
]) ])
@ -95,46 +96,56 @@ public function create()
public function store(StoreMainRoleSkills $request) public function store(StoreMainRoleSkills $request)
{ {
try { $create = [];
$create = []; foreach ($request['skills'] as $skill) {
foreach ($request['skills'] as $skill) { $create[] = [
$create[] = [ 'main_role_id' => $request['main_role_id'],
'main_role_id' => $request['main_role_id'], 'skill_id' => $skill['skill_id'],
'skill_id' => $skill['skill_id'], 'scored_id' => $skill['scored_id'],
'scored_id' => $skill['scored_id'], 'created_at' => now(),
'created_at' => now(), 'updated_at' => now(),
'updated_at' => now(), ];
]; }
}
MainRoleSkills::insert($create); MainRoleSkills::insert($create);
return $this->index(); return $this->index();
} catch (\Illuminate\Database\QueryException $e) {
// Si hay error de restricción única
if ($e->getCode() === '23000') {
return back()->withErrors(['skills' => 'Una o más habilidades ya están asignadas a este rol.']);
}
throw $e;
}
} }
public function update(UpdateMainRoleSkills $request, MainRoleSkills $mainRoleSkills) public function edit(MainRoleSkills $mainRoleSkills)
{ {
$mainRoleSkills->update($request->all()); $mainRoleSkills->load(['mainRole.department', 'skill.department', 'score']);
$scores = Score::orderBy('alias', 'ASC')->get();
return $this->vuew('edit', [
'mainRoleSkills' => $mainRoleSkills,
'scores' => $scores,
]);
} }
public function destroy($mainRoleId)
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 { try {
// Eliminar todas las habilidades asociadas a este rol principal $mainRoleSkills = MainRoleSkills::findOrFail($id);
MainRoleSkills::where('main_role_id', $mainRoleId)->delete();
$mainRoleSkills->delete();
return $this->index(); return $this->index();
} catch (\Throwable $th) { } catch (\Throwable $th) {
Log::channel('mainRoleSkills')->error($th->getMessage()); Log::error($th->getMessage());
return response()->json(['error' => 'Error al eliminar las habilidades'], 500); return response()->json(['error' => 'Error al eliminar las habilidades']);
} }
} }
} }

View File

@ -32,8 +32,6 @@ public function authorize()
public function rules() public function rules()
{ {
return [ return [
'main_role_id' => ['required', 'integer'],
'skill_id' => ['required', 'integer'],
'scored_id' => ['required', 'integer'], 'scored_id' => ['required', 'integer'],
]; ];
} }

View File

@ -1,22 +1,34 @@
<script setup> <script setup>
import { computed } from "vue"; import { computed, ref } from "vue";
import { Link } from "@inertiajs/vue3"; import { Link } from "@inertiajs/vue3";
import { can, goTo } from "@/Pages/Admin/MainRoleSkills/Component"; import { can, goTo } from "@/Pages/Admin/MainRoleSkills/Component";
import GoogleIcon from "@/Components/Shared/GoogleIcon.vue"; 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 emit = defineEmits(["select"]);
const props = defineProps({ const props = defineProps({
mainRoleSkills: Array, mainRoleSkills: Array,
selectedRole: Object, 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(() => { const roleSkills = computed(() => {
if (!props.mainRoleSkills || !props.selectedRole) { if (!props.mainRoleSkills || !props.selectedRole) {
return []; return [];
} }
// Filtrar habilidades por rol seleccionado
return props.mainRoleSkills.filter( return props.mainRoleSkills.filter(
(roleSkill) => roleSkill.main_role_id === props.selectedRole.id (roleSkill) => roleSkill.main_role_id === props.selectedRole.id
); );
@ -32,10 +44,13 @@ const roleSkills = computed(() => {
</h2> </h2>
<Link <Link
v-if="can('create')" v-if="can('create')"
:href="route(goTo('create'), { :href="
role_id: selectedRole.id, route(goTo('create'), {
department_id: selectedRole.department?.id || selectedRole.department_id role_id: selectedRole.id,
})" department_id:
selectedRole.department?.id || selectedRole.department_id,
})
"
> >
<GoogleIcon <GoogleIcon
:title="$t('crud.create')" :title="$t('crud.create')"
@ -75,6 +90,22 @@ const roleSkills = computed(() => {
> >
{{ roleSkill.skill.description }} {{ roleSkill.skill.description }}
</p> </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> </div>
@ -86,5 +117,19 @@ const roleSkills = computed(() => {
Las habilidades se pueden asignar desde la sección de creación Las habilidades se pueden asignar desde la sección de creación
</p> </p>
</div> </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> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

@ -36,8 +36,7 @@ const destroy = (id) => router.delete(route(goTo('destroy'), {id}), {
@destroy="destroy(model.id)" @destroy="destroy(model.id)"
> >
<Header <Header
:title="model.name" :title="model.skill.name"
:subtitle="model.description"
/> />
</DestroyModal> </DestroyModal>
</template> </template>

View File

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

View File

@ -85,6 +85,7 @@ const goBack = () => {
v-if="selectedRole" v-if="selectedRole"
:mainRoleSkills="filteredMainRoleSkills" :mainRoleSkills="filteredMainRoleSkills"
:selectedRole="selectedRole" :selectedRole="selectedRole"
:scores="scores"
@select="handleSkillSelect" @select="handleSkillSelect"
/> />
</DashboardLayout> </DashboardLayout>