historial-academico #6
83
src/components/Holos/Card/AcademicRecords.vue
Normal file
83
src/components/Holos/Card/AcademicRecords.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<script setup>
|
||||||
|
import { getDate } from '@Controllers/DateController';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Props */
|
||||||
|
const props = defineProps({
|
||||||
|
records: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['destroy', 'edit']);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function openDestroy(record) {
|
||||||
|
emit('destroy', record);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(record) {
|
||||||
|
emit('edit', record);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-6">
|
||||||
|
<h3 class="text-base font-medium text-gray-800 flex items-center gap-2 dark:text-primary-dt">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-black dark:text-primary-dt text-xl"
|
||||||
|
name="book"
|
||||||
|
/>
|
||||||
|
Grados Académicos
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-4 grid-cols-1 md:grid-cols-2">
|
||||||
|
<!-- Item grado dinámico -->
|
||||||
|
<article
|
||||||
|
v-for="record in records"
|
||||||
|
:key="record.id"
|
||||||
|
class="rounded-lg border border-gray-100 bg-white p-4 relative dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-gray-800 dark:text-primary-dt">{{ record.title }}</h4>
|
||||||
|
<p class="text-xs text-gray-500 mt-2 dark:text-primary-dt/70">{{ record.institution }}</p>
|
||||||
|
<p class="text-xs text-gray-400 mt-2 dark:text-primary-dt/70">Fecha de obtención: {{ record.date_obtained ? getDate(record.date_obtained) : '-' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700 dark:bg-primary/10 dark:text-primary-dt">
|
||||||
|
{{ record.degree_type_ek }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
@click="openEdit(record)"
|
||||||
|
class="p-1 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
title="Editar grado académico"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="edit" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="openDestroy(record)"
|
||||||
|
class="p-1 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
title="Eliminar grado académico"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Estado vacío para grados académicos -->
|
||||||
|
<div v-if="!records || records.length === 0" class="col-span-2 py-8 text-center">
|
||||||
|
<div class="text-gray-500 dark:text-primary-dt/70">
|
||||||
|
<GoogleIcon name="school" class="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||||
|
<p class="text-lg font-medium">No se encontraron grados académicos</p>
|
||||||
|
<p class="text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
121
src/components/Holos/Card/Certifications.vue
Normal file
121
src/components/Holos/Card/Certifications.vue
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<script setup>
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import { getDate } from '@Controllers/DateController';
|
||||||
|
|
||||||
|
/** Props */
|
||||||
|
const props = defineProps({
|
||||||
|
certifications: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['destroy', 'edit']);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function openDestroy(cert) {
|
||||||
|
emit('destroy', cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(cert) {
|
||||||
|
emit('edit', cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function getCertificationStatus(cert) {
|
||||||
|
// Si no tiene fecha de expiración, se considera permanente
|
||||||
|
if (!cert.date_expiration) {
|
||||||
|
return {
|
||||||
|
status: 'permanente',
|
||||||
|
statusText: 'Permanente',
|
||||||
|
isExpired: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const expirationDate = new Date(cert.date_expiration);
|
||||||
|
|
||||||
|
// Si la fecha actual es mayor a la fecha de expiración, está vencida
|
||||||
|
const isExpired = now > expirationDate;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: isExpired ? 'vencida' : 'vigente',
|
||||||
|
statusText: isExpired ? 'Vencida' : 'Vigente',
|
||||||
|
isExpired
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-8">
|
||||||
|
<h3 class="text-base font-medium text-gray-800 flex items-center gap-2 dark:text-primary-dt">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-black dark:text-primary-dt text-xl"
|
||||||
|
name="license"
|
||||||
|
/>
|
||||||
|
Certificaciones
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-4 grid-cols-1 md:grid-cols-2">
|
||||||
|
<!-- Certificación dinámica -->
|
||||||
|
<article
|
||||||
|
v-for="cert in certifications"
|
||||||
|
:key="cert.id"
|
||||||
|
:class="[
|
||||||
|
'rounded-lg border p-4 relative',
|
||||||
|
getCertificationStatus(cert).status === 'vigente'
|
||||||
|
? 'border-gray-100 bg-green-50/40 dark:bg-success-d/10 dark:border-primary/20 dark:text-primary-dt'
|
||||||
|
: 'border-gray-100 bg-white dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-gray-800 dark:text-primary-dt">{{ cert.name }}</h4>
|
||||||
|
<p class="text-xs text-gray-500 mt-2 dark:text-primary-dt/70">{{ cert.institution }}</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1 dark:text-primary-dt/70">Obtenida: {{ cert.date_obtained ? getDate(cert.date_obtained) : '-' }}</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-primary-dt/70">Vigencia: {{ cert.date_expiration ? getDate(cert.date_expiration) : 'Permanente' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold',
|
||||||
|
getCertificationStatus(cert).status === 'vigente'
|
||||||
|
? 'bg-emerald-100 text-emerald-700 dark:bg-success-d dark:text-success-dt'
|
||||||
|
: getCertificationStatus(cert).status === 'permanente'
|
||||||
|
? 'bg-blue-100 text-blue-700 dark:bg-info-d dark:text-info-dt'
|
||||||
|
: 'bg-amber-100 text-amber-800 dark:bg-warning-d dark:text-warning-dt'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ getCertificationStatus(cert).statusText }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
@click="openEdit(cert)"
|
||||||
|
class="p-1 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
title="Editar certificación"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="edit" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="openDestroy(cert)"
|
||||||
|
class="p-1 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
title="Eliminar certificación"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Estado vacío para certificaciones -->
|
||||||
|
<div v-if="!certifications || certifications.length === 0" class="col-span-2 py-8 text-center">
|
||||||
|
<div class="text-gray-500 dark:text-primary-dt/70">
|
||||||
|
<GoogleIcon name="license" class="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||||
|
<p class="text-lg font-medium">No se encontraron certificaciones</p>
|
||||||
|
<p class="text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
151
src/components/Holos/NewTable.vue
Normal file
151
src/components/Holos/NewTable.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<script setup>
|
||||||
|
import GoogleIcon from '../Shared/GoogleIcon.vue';
|
||||||
|
import Loader from '../Shared/Loader.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'send-pagination'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
items: Object,
|
||||||
|
processing: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm p-6 dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
||||||
|
|
||||||
|
<!-- Tabla -->
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table v-if="!processing" class="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left text-sm text-gray-500 dark:text-primary-dt/70">
|
||||||
|
<slot name="head" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100 dark:divide-primary/20 text-sm text-gray-700 dark:text-primary-dt">
|
||||||
|
<template v-if="items?.total > 0">
|
||||||
|
<slot
|
||||||
|
name="body"
|
||||||
|
:items="items?.data"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr>
|
||||||
|
<slot name="empty" />
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Estado de carga -->
|
||||||
|
<table v-else class="animate-pulse w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="100%" class="h-8 text-center">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="i in 3" :key="i">
|
||||||
|
<td colspan="100%" class="table-cell h-16 text-center">
|
||||||
|
<div class="w-full h-4 bg-secondary/50 rounded-md"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paginación -->
|
||||||
|
<template v-if="items?.links && items.links.length > 3">
|
||||||
|
<div class="mt-6 flex w-full justify-end">
|
||||||
|
<div class="flex w-full justify-end flex-wrap space-x-1 -mb-1">
|
||||||
|
<template v-for="(link, k) in items.links" :key="k">
|
||||||
|
<!-- Botón anterior deshabilitado -->
|
||||||
|
<div v-if="link.url === null && k == 0"
|
||||||
|
class="px-3 py-2 text-sm leading-4 text-gray-400 border rounded-lg bg-gray-50 dark:bg-primary-d dark:border-primary/20"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="arrow_back" class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón anterior activo -->
|
||||||
|
<button v-else-if="k === 0"
|
||||||
|
class="px-3 py-2 text-sm leading-4 border rounded-lg transition-colors duration-200 hover:bg-gray-50 dark:hover:bg-primary-d/50"
|
||||||
|
:class="{
|
||||||
|
'bg-primary text-white border-primary dark:bg-primary-dark dark:border-primary-dark': link.active,
|
||||||
|
'border-gray-300 dark:border-primary/20 text-gray-700 dark:text-primary-dt': !link.active
|
||||||
|
}"
|
||||||
|
@click="emit('send-pagination', link.url)"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="arrow_back" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Botón siguiente deshabilitado -->
|
||||||
|
<div v-else-if="link.url === null && k == (items.links.length - 1)"
|
||||||
|
class="px-3 py-2 text-sm leading-4 text-gray-400 border rounded-lg bg-gray-50 dark:bg-primary-d dark:border-primary/20"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="arrow_forward" class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón siguiente activo -->
|
||||||
|
<button v-else-if="k === (items.links.length - 1)"
|
||||||
|
class="px-3 py-2 text-sm leading-4 border rounded-lg transition-colors duration-200 hover:bg-gray-50 dark:hover:bg-primary-d/50"
|
||||||
|
:class="{
|
||||||
|
'bg-primary text-white border-primary dark:bg-primary-dark dark:border-primary-dark': link.active,
|
||||||
|
'border-gray-300 dark:border-primary/20 text-gray-700 dark:text-primary-dt': !link.active
|
||||||
|
}"
|
||||||
|
@click="emit('send-pagination', link.url)"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="arrow_forward" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Números de página -->
|
||||||
|
<button v-else
|
||||||
|
class="px-3 py-2 text-sm leading-4 border rounded-lg transition-colors duration-200 hover:bg-gray-50 dark:hover:bg-primary-d/50"
|
||||||
|
:class="{
|
||||||
|
'bg-primary text-white border-primary dark:bg-primary-dark dark:border-primary-dark': link.active,
|
||||||
|
'border-gray-300 dark:border-primary/20 text-gray-700 dark:text-primary-dt': !link.active
|
||||||
|
}"
|
||||||
|
v-html="link.label"
|
||||||
|
@click="emit('send-pagination', link.url)"
|
||||||
|
></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Estilos adicionales para mejorar la experiencia */
|
||||||
|
tbody tr {
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark tbody tr:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animación suave para los botones de paginación */
|
||||||
|
button {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -150,6 +150,9 @@ export default {
|
|||||||
},
|
},
|
||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
date: 'Fecha',
|
date: 'Fecha',
|
||||||
|
date_expiration: 'Fecha de expiración',
|
||||||
|
date_obtained: 'Fecha de obtención',
|
||||||
|
department: 'Departamento',
|
||||||
dates: {
|
dates: {
|
||||||
start: 'Fecha Inicial',
|
start: 'Fecha Inicial',
|
||||||
end: 'Fecha Final'
|
end: 'Fecha Final'
|
||||||
@ -168,6 +171,7 @@ export default {
|
|||||||
confirm: 'Al presionar ELIMINAR el registro se eliminará permanentemente y no podrá recuperarse.',
|
confirm: 'Al presionar ELIMINAR el registro se eliminará permanentemente y no podrá recuperarse.',
|
||||||
title: 'Eliminar',
|
title: 'Eliminar',
|
||||||
},
|
},
|
||||||
|
degreeType: 'Tipo de grado',
|
||||||
deleted:'Registro eliminado',
|
deleted:'Registro eliminado',
|
||||||
description:'Descripción',
|
description:'Descripción',
|
||||||
details:'Detalles',
|
details:'Detalles',
|
||||||
@ -202,6 +206,9 @@ export default {
|
|||||||
home: 'Volver a la pagina de inicio.',
|
home: 'Volver a la pagina de inicio.',
|
||||||
title:'Ayuda',
|
title:'Ayuda',
|
||||||
},
|
},
|
||||||
|
headquarter: 'Sede',
|
||||||
|
hire_date: 'Fecha de contratación',
|
||||||
|
institution: 'Institución',
|
||||||
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.'
|
||||||
@ -216,6 +223,7 @@ export default {
|
|||||||
menu:'Menú',
|
menu:'Menú',
|
||||||
name:'Nombre',
|
name:'Nombre',
|
||||||
noRecords:'Sin registros',
|
noRecords:'Sin registros',
|
||||||
|
number: 'Número',
|
||||||
notification:'Notificación',
|
notification:'Notificación',
|
||||||
notifications: {
|
notifications: {
|
||||||
unreadClosed:'Ocultas',
|
unreadClosed:'Ocultas',
|
||||||
@ -394,6 +402,28 @@ export default {
|
|||||||
unreaded:'No leído',
|
unreaded:'No leído',
|
||||||
user:'Usuario',
|
user:'Usuario',
|
||||||
users:{
|
users:{
|
||||||
|
academic: {
|
||||||
|
create: {
|
||||||
|
certification: {
|
||||||
|
description: 'Permite agregar nuevas certificaciones profesionales al historial académico del usuario.',
|
||||||
|
title: 'Crear certificación'
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
description: 'Permite agregar nuevos grados académicos al historial del usuario.',
|
||||||
|
title: 'Crear registro académico'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
certification: {
|
||||||
|
description: 'Permite modificar los datos de la certificación profesional seleccionada.',
|
||||||
|
title: 'Editar certificación'
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
description: 'Permite modificar los datos del registro académico seleccionado.',
|
||||||
|
title: 'Editar registro académico'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
activity: {
|
activity: {
|
||||||
title: 'Actividad del usuario',
|
title: 'Actividad del usuario',
|
||||||
description: 'Historial de acciones realizadas por el usuario.',
|
description: 'Historial de acciones realizadas por el usuario.',
|
||||||
@ -405,6 +435,7 @@ export default {
|
|||||||
onError:'Ocurrió un error al crear el usuario'
|
onError:'Ocurrió un error al crear el usuario'
|
||||||
},
|
},
|
||||||
deleted:'Usuario eliminado',
|
deleted:'Usuario eliminado',
|
||||||
|
description: 'Gestión de información general de empleados',
|
||||||
remove: 'Remover usuario',
|
remove: 'Remover usuario',
|
||||||
edit: {
|
edit: {
|
||||||
title: 'Editar usuario'
|
title: 'Editar usuario'
|
||||||
|
|||||||
@ -48,29 +48,24 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
<DropDown
|
<DropDown
|
||||||
icon="people"
|
icon="people"
|
||||||
name="Empleados"
|
name="Usuarios"
|
||||||
to="admin.employees.index"
|
to="admin.users.index"
|
||||||
:collapsed="true"
|
:collapsed="true"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
icon="school"
|
icon="school"
|
||||||
name="Historial Académico"
|
name="Historial Académico"
|
||||||
to="admin.academic.index"
|
to="admin.users.academic.index"
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
icon="security"
|
icon="security"
|
||||||
name="Seguridad y Salud"
|
name="Seguridad y Salud"
|
||||||
to="admin.security.index"
|
to="admin.users.security"
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
icon="payments"
|
|
||||||
name="Nómina"
|
|
||||||
to="admin.payroll.index"
|
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
icon="info"
|
icon="info"
|
||||||
name="Información Adicional"
|
name="Información Adicional"
|
||||||
to="admin.additional.index"
|
to="admin.users.additional"
|
||||||
/>
|
/>
|
||||||
</DropDown>
|
</DropDown>
|
||||||
</Section>
|
</Section>
|
||||||
@ -133,12 +128,6 @@ onMounted(() => {
|
|||||||
v-if="hasPermission('users.index')"
|
v-if="hasPermission('users.index')"
|
||||||
:name="$t('admin.title')"
|
:name="$t('admin.title')"
|
||||||
>
|
>
|
||||||
<!-- <Link
|
|
||||||
v-if="hasPermission('users.index')"
|
|
||||||
icon="people"
|
|
||||||
name="users.title"
|
|
||||||
to="admin.users.index"
|
|
||||||
/> -->
|
|
||||||
<Link
|
<Link
|
||||||
v-if="hasPermission('roles.index')"
|
v-if="hasPermission('roles.index')"
|
||||||
icon="license"
|
icon="license"
|
||||||
|
|||||||
@ -1,147 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|
||||||
import Adding from '@Holos/Button/ButtonRh.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-6 max-w-auto mx-auto">
|
|
||||||
<!-- Página: Header principal -->
|
|
||||||
<div class="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-4xl font-extrabold text-gray-900 dark:text-primary-dt">Historial Académico</h1>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">Gestión de grados académicos y certificaciones profesionales</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Adding text="Agregar Registro" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card principal: perfil + secciones -->
|
|
||||||
<section class="mt-6 bg-white rounded-lg shadow-sm p-6 dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
|
||||||
<!-- Perfil -->
|
|
||||||
<header class="flex items-start gap-4">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<div class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-primary/10 dark:text-primary-dt">
|
|
||||||
<!-- icono usuario -->
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-black dark:text-primary-dt text-xl"
|
|
||||||
name="school"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-primary-dt">María González López</h2>
|
|
||||||
<p class="text-sm text-gray-500 mt-1 dark:text-primary-dt/70">Información académica y certificaciones profesionales</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Secciones: Grados Académicos -->
|
|
||||||
<div class="mt-6">
|
|
||||||
<h3 class="text-base font-medium text-gray-800 flex items-center gap-2 dark:text-primary-dt">
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-black dark:text-primary-dt text-xl"
|
|
||||||
name="book"
|
|
||||||
/>
|
|
||||||
Grados Académicos
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="mt-4 grid gap-4 grid-cols-1 md:grid-cols-2">
|
|
||||||
<!-- Item grado -->
|
|
||||||
<article class="rounded-lg border border-gray-100 bg-white p-4 relative dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
|
||||||
<div class="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-primary-dt">Licenciatura en Ingeniería en Sistemas</h4>
|
|
||||||
<p class="text-xs text-gray-500 mt-2 dark:text-primary-dt/70">Universidad Nacional Autónoma de México</p>
|
|
||||||
<p class="text-xs text-gray-400 mt-2 dark:text-primary-dt/70">Año: 2015</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="ml-4 inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700 dark:bg-primary/10 dark:text-primary-dt">
|
|
||||||
Licenciatura
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="rounded-lg border border-gray-100 bg-white p-4 relative dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
|
||||||
<div class="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-primary-dt">Maestría en Ciencias de la Computación</h4>
|
|
||||||
<p class="text-xs text-gray-500 mt-2 dark:text-primary-dt/70">Instituto Tecnológico de Monterrey</p>
|
|
||||||
<p class="text-xs text-gray-400 mt-2 dark:text-primary-dt/70">Año: 2018</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="ml-4 inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-700 dark:bg-primary/10 dark:text-primary-dt">
|
|
||||||
Maestría
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Secciones: Certificaciones -->
|
|
||||||
<div class="mt-8">
|
|
||||||
<h3 class="text-base font-medium text-gray-800 flex items-center gap-2 dark:text-primary-dt">
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-black dark:text-primary-dt text-xl"
|
|
||||||
name="license"
|
|
||||||
/>
|
|
||||||
Certificaciones
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="mt-4 grid gap-4 grid-cols-1 md:grid-cols-2">
|
|
||||||
<!-- Cert vigente -->
|
|
||||||
<article class="rounded-lg border border-gray-100 bg-green-50/40 p-4 relative dark:bg-success-d/10 dark:border-primary/20 dark:text-primary-dt">
|
|
||||||
<div class="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-primary-dt">AWS Certified Solutions Architect</h4>
|
|
||||||
<p class="text-xs text-gray-500 mt-2 dark:text-primary-dt/70">Amazon Web Services</p>
|
|
||||||
<p class="text-xs text-gray-500 mt-1 dark:text-primary-dt/70">Obtenida: 2023-05-15</p>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-primary-dt/70">Vigencia: 2026-05-15</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="ml-4 inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-700 dark:bg-success-d dark:text-success-dt">
|
|
||||||
Vigente
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<!-- Cert vencida -->
|
|
||||||
<article class="rounded-lg border border-gray-100 bg-white p-4 relative dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
|
||||||
<div class="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold text-gray-800 dark:text-primary-dt">Certified ScrumMaster</h4>
|
|
||||||
<p class="text-xs text-gray-500 mt-2 dark:text-primary-dt/70">Scrum Alliance</p>
|
|
||||||
<p class="text-xs text-gray-500 mt-1 dark:text-primary-dt/70">Obtenida: 2022-08-20</p>
|
|
||||||
<p class="text-xs text-gray-500 dark:text-primary-dt/70">Vigencia: 2024-08-20</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="ml-4 inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-amber-100 text-amber-800 dark:bg-warning-d dark:text-warning-dt">
|
|
||||||
Vencida
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Acciones al final de la tarjeta -->
|
|
||||||
<div class="mt-6 border-t border-gray-100 pt-4 flex gap-3 dark:border-primary/20">
|
|
||||||
<button class="inline-flex items-center gap-2 px-3 py-2 rounded-md border border-gray-200 bg-white text-sm text-gray-700 shadow-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-black dark:text-primary-dt text-xl"
|
|
||||||
name="add"
|
|
||||||
/>
|
|
||||||
Agregar Grado
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="inline-flex items-center gap-2 px-3 py-2 rounded-md border border-gray-200 bg-white text-sm text-gray-700 shadow-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
|
||||||
<GoogleIcon
|
|
||||||
class="text-black dark:text-primary-dt text-xl"
|
|
||||||
name="add"
|
|
||||||
/>
|
|
||||||
Agregar Certificación
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
58
src/pages/Admin/Users/Academic/CreateCertification.vue
Normal file
58
src/pages/Admin/Users/Academic/CreateCertification.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { useForm } from '@Services/Api';
|
||||||
|
import { apiTo, transl, viewTo } from './Module';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import FormCertification from './FormCertification.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
institution: '',
|
||||||
|
date_obtained: '',
|
||||||
|
date_expiration: '',
|
||||||
|
number: '',
|
||||||
|
user_id: route.params.id || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
form.post(apiTo('store-certificate'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('register.create.onSuccess'))
|
||||||
|
router.push(viewTo({ name: 'index' }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader
|
||||||
|
:title="transl('create.certification.title')"
|
||||||
|
>
|
||||||
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="w-full pb-2">
|
||||||
|
<p class="text-justify text-sm" v-text="transl('create.certification.description')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormCertification
|
||||||
|
:form="form"
|
||||||
|
@submit="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
82
src/pages/Admin/Users/Academic/CreateRecord.vue
Normal file
82
src/pages/Admin/Users/Academic/CreateRecord.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { api, useForm } from '@Services/Api';
|
||||||
|
import { apiTo, transl, viewTo } from './Module';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import Selectable from '@Holos/Form/Selectable.vue';
|
||||||
|
import FormRecord from './FormRecord.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
title: '',
|
||||||
|
institution: '',
|
||||||
|
date_obtained: '',
|
||||||
|
degree_type_ek: '',
|
||||||
|
user_id: route.params.id || '',
|
||||||
|
// file_path: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const degreeTypes = ref([]);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
form.transform(data => ({
|
||||||
|
...data,
|
||||||
|
degree_type_ek: form.degree_type_ek?.id
|
||||||
|
})).post(apiTo('store-record'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('register.create.onSuccess'))
|
||||||
|
router.push(viewTo({ name: 'index' }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
api.catalog({
|
||||||
|
'degreeType:all': null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (r) => {
|
||||||
|
degreeTypes.value = r['degreeType:all'] ?? [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader
|
||||||
|
:title="transl('create.record.title')"
|
||||||
|
>
|
||||||
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="w-full pb-2">
|
||||||
|
<p class="text-justify text-sm" v-text="transl('create.record.description')" />
|
||||||
|
</div>
|
||||||
|
<FormRecord
|
||||||
|
:degreeTypes="degreeTypes"
|
||||||
|
:form="form"
|
||||||
|
@submit="submit"
|
||||||
|
>
|
||||||
|
<Selectable
|
||||||
|
v-model="form.degree_type_ek"
|
||||||
|
title="degreeType"
|
||||||
|
:options="degreeTypes"
|
||||||
|
/>
|
||||||
|
</FormRecord>
|
||||||
|
</template>
|
||||||
75
src/pages/Admin/Users/Academic/EditCertification.vue
Normal file
75
src/pages/Admin/Users/Academic/EditCertification.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { api, useForm } from '@Services/Api';
|
||||||
|
import { apiTo, transl, viewTo } from './Module';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import FormCertification from './FormCertification.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
const vroute = useRoute();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
institution: '',
|
||||||
|
date_obtained: '',
|
||||||
|
date_expiration: '',
|
||||||
|
number: '',
|
||||||
|
user_id: vroute.params.id || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
form.put(apiTo('update-certificate', { certificate: vroute.params.certification }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('register.edit.onSuccess'))
|
||||||
|
router.push(viewTo({ name: 'index' }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
api.get(apiTo('show-certificate', { certificate: vroute.params.certification }), {
|
||||||
|
onSuccess: (r) => {
|
||||||
|
form.fill(r.certificate)
|
||||||
|
if (r.certificate.date_obtained) {
|
||||||
|
form.date_obtained = new Date(r.certificate.date_obtained).toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
if (r.certificate.date_expiration) {
|
||||||
|
form.date_expiration = new Date(r.certificate.date_expiration).toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader
|
||||||
|
:title="transl('edit.certification.title')"
|
||||||
|
>
|
||||||
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="w-full pb-2">
|
||||||
|
<p class="text-justify text-sm" v-text="transl('edit.certification.description')" />
|
||||||
|
</div>
|
||||||
|
<FormCertification
|
||||||
|
action="update"
|
||||||
|
:form="form"
|
||||||
|
@submit="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
97
src/pages/Admin/Users/Academic/EditRecord.vue
Normal file
97
src/pages/Admin/Users/Academic/EditRecord.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { api, useForm } from '@Services/Api';
|
||||||
|
import { apiTo, transl, viewTo } from './Module';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import Selectable from '@Holos/Form/Selectable.vue';
|
||||||
|
import FormRecord from './FormRecord.vue';
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
const vroute = useRoute();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
title: '',
|
||||||
|
institution: '',
|
||||||
|
date_obtained: '',
|
||||||
|
degree_type_ek: '',
|
||||||
|
user_id: vroute.params.id || '',
|
||||||
|
// file_path: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const degreeTypes = ref([]);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
form.transform(data => ({
|
||||||
|
...data,
|
||||||
|
degree_type_ek: form.degree_type_ek?.id
|
||||||
|
})).put(apiTo('update-record', { academicRecord: vroute.params.record }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('register.edit.onSuccess'))
|
||||||
|
router.push(viewTo({ name: 'index' }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm() {
|
||||||
|
api.get(apiTo('show-record', { academicRecord: vroute.params.record }), {
|
||||||
|
onSuccess: (r) => {
|
||||||
|
form.fill(r.academic_record)
|
||||||
|
form.degree_type_ek = degreeTypes.value.find(type => type.name == form.degree_type_ek)
|
||||||
|
if (r.academic_record.date_obtained) {
|
||||||
|
form.date_obtained = new Date(r.academic_record.date_obtained).toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
api.catalog({
|
||||||
|
'degreeType:all': null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (r) => {
|
||||||
|
degreeTypes.value = r['degreeType:all'] ?? [];
|
||||||
|
// De esta manera se puede rellenar el form con el degree_type_ek del catalogo
|
||||||
|
fillForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader
|
||||||
|
:title="transl('edit.record.title')"
|
||||||
|
>
|
||||||
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="w-full pb-2">
|
||||||
|
<p class="text-justify text-sm" v-text="transl('edit.record.description')" />
|
||||||
|
</div>
|
||||||
|
<FormRecord
|
||||||
|
:degreeTypes="degreeTypes"
|
||||||
|
:form="form"
|
||||||
|
@submit="submit"
|
||||||
|
>
|
||||||
|
<Selectable
|
||||||
|
action="update"
|
||||||
|
v-model="form.degree_type_ek"
|
||||||
|
title="degreeType"
|
||||||
|
:options="degreeTypes"
|
||||||
|
/>
|
||||||
|
</FormRecord>
|
||||||
|
</template>
|
||||||
76
src/pages/Admin/Users/Academic/FormCertification.vue
Normal file
76
src/pages/Admin/Users/Academic/FormCertification.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
|
import Input from '@Holos/Form/Input.vue';
|
||||||
|
import Textarea from '@Holos/Form/Textarea.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'submit'
|
||||||
|
])
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
action: {
|
||||||
|
default: 'create',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
form: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
emit('submit')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<form @submit.prevent="submit" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
<Input
|
||||||
|
v-model="form.name"
|
||||||
|
id="name"
|
||||||
|
:onError="form.errors.name"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.institution"
|
||||||
|
id="institution"
|
||||||
|
:onError="form.errors.institution"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.number"
|
||||||
|
id="number"
|
||||||
|
:onError="form.errors.number"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.date_obtained"
|
||||||
|
id="date_obtained"
|
||||||
|
:onError="form.errors.date_obtained"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.date_expiration"
|
||||||
|
id="date_expiration"
|
||||||
|
:onError="form.errors.date_expiration"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4">
|
||||||
|
<Textarea
|
||||||
|
v-model="form.description"
|
||||||
|
id="description"
|
||||||
|
:onError="form.errors.description"
|
||||||
|
rows="4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4">
|
||||||
|
<PrimaryButton
|
||||||
|
v-text="$t('create')"
|
||||||
|
:class="{ 'opacity-25': form.processing }"
|
||||||
|
:disabled="form.processing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
64
src/pages/Admin/Users/Academic/FormRecord.vue
Normal file
64
src/pages/Admin/Users/Academic/FormRecord.vue
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<script setup>
|
||||||
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
|
import Input from '@Holos/Form/Input.vue';
|
||||||
|
import SingleFile from '@Holos/Form/SingleFile.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'submit'
|
||||||
|
])
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
action: {
|
||||||
|
default: 'create',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
form: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
emit('submit')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<form @submit.prevent="submit" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
<Input
|
||||||
|
v-model="form.title"
|
||||||
|
id="title"
|
||||||
|
:onError="form.errors.title"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.institution"
|
||||||
|
id="institution"
|
||||||
|
:onError="form.errors.institution"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.date_obtained"
|
||||||
|
id="date_obtained"
|
||||||
|
:onError="form.errors.date_obtained"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<!-- <div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4">
|
||||||
|
<SingleFile
|
||||||
|
v-model="form.file_path"
|
||||||
|
title="Archivo"
|
||||||
|
accept="application/pdf,image/png,image/jpeg"
|
||||||
|
/>
|
||||||
|
</div> -->
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4">
|
||||||
|
<PrimaryButton
|
||||||
|
v-text="$t('create')"
|
||||||
|
:class="{ 'opacity-25': form.processing }"
|
||||||
|
:disabled="form.processing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
151
src/pages/Admin/Users/Academic/Index.vue
Normal file
151
src/pages/Admin/Users/Academic/Index.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useSearcher } from '@Services/Api';
|
||||||
|
import { getDate } from '@Controllers/DateController';
|
||||||
|
import { apiTo, viewTo } from './Module'
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Searcher from '@Holos/Searcher.vue';
|
||||||
|
import AcademicRecords from '@Holos/Card/AcademicRecords.vue';
|
||||||
|
import Certifications from '@Holos/Card/Certifications.vue';
|
||||||
|
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const models = ref([]);
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Referencias */
|
||||||
|
const destroyRecordModal = ref(false);
|
||||||
|
const destroyCertModal = ref(false);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const searcher = useSearcher({
|
||||||
|
url: apiTo('index'),
|
||||||
|
onSuccess: (r) => models.value = r.models,
|
||||||
|
onError: () => models.value = []
|
||||||
|
});
|
||||||
|
|
||||||
|
function openDestroyRecord(record) {
|
||||||
|
destroyRecordModal.value.open(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDestroyCert(cert) {
|
||||||
|
destroyCertModal.value.open(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditRecord(record) {
|
||||||
|
router.push(viewTo({ name: 'editRecord', params: { id: record.user_id, record: record.id } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditCert(cert) {
|
||||||
|
router.push(viewTo({ name: 'editCertification', params: { id: cert.user_id, certification: cert.id } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
searcher.search();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6 max-w-auto mx-auto">
|
||||||
|
<!-- Página: Header principal -->
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-4xl font-extrabold text-gray-900 dark:text-primary-dt">Historial Académico</h1>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">Gestión de grados académicos y certificaciones profesionales</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Card -->
|
||||||
|
<div class="pt-2 w-full">
|
||||||
|
<Searcher @search="(x) => searcher.search(x)">
|
||||||
|
</Searcher>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cards principales: una por usuario -->
|
||||||
|
<section
|
||||||
|
v-for="user in models.data"
|
||||||
|
:key="user.id"
|
||||||
|
class="mt-6 bg-white rounded-lg shadow-sm p-6 dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
||||||
|
>
|
||||||
|
<!-- Perfil -->
|
||||||
|
<header class="flex items-start gap-4">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 dark:bg-primary/10 dark:text-primary-dt">
|
||||||
|
<!-- icono usuario -->
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-black dark:text-primary-dt text-xl"
|
||||||
|
name="school"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-800 dark:text-primary-dt">{{ user.full_name }}</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1 dark:text-primary-dt/70">Información académica y certificaciones profesionales</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Secciones: Grados Académicos -->
|
||||||
|
<AcademicRecords
|
||||||
|
:records="user.academic_records || []"
|
||||||
|
@destroy="openDestroyRecord"
|
||||||
|
@edit="openEditRecord"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Secciones: Certificaciones -->
|
||||||
|
<Certifications
|
||||||
|
:certifications="user.certificates || []"
|
||||||
|
@destroy="openDestroyCert"
|
||||||
|
@edit="openEditCert"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Acciones al final de la tarjeta -->
|
||||||
|
<div class="mt-6 border-t border-gray-100 pt-4 flex gap-3 dark:border-primary/20">
|
||||||
|
<RouterLink :to="viewTo({ name: 'createRecord', params: { id: user.id } })" class="inline-flex items-center gap-2 px-3 py-2 rounded-md border border-gray-200 bg-white text-sm text-gray-700 shadow-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-black dark:text-primary-dt text-xl"
|
||||||
|
name="add"
|
||||||
|
/>
|
||||||
|
Agregar Grado
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<RouterLink :to="viewTo({ name: 'createCertification', params: { id: user.id } })" class="inline-flex items-center gap-2 px-3 py-2 rounded-md border border-gray-200 bg-white text-sm text-gray-700 shadow-sm dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-black dark:text-primary-dt text-xl"
|
||||||
|
name="add"
|
||||||
|
/>
|
||||||
|
Agregar Certificación
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Estado vacío general -->
|
||||||
|
<div v-if="models.length === 0" class="mt-6 py-12 text-center">
|
||||||
|
<div class="text-gray-500 dark:text-primary-dt/70">
|
||||||
|
<GoogleIcon name="person_search" class="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||||
|
<p class="text-lg font-medium">No se encontraron usuarios</p>
|
||||||
|
<p class="text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modales de eliminación -->
|
||||||
|
<DestroyView
|
||||||
|
ref="destroyRecordModal"
|
||||||
|
subtitle="title"
|
||||||
|
:to="(academicRecord) => apiTo('destroy-record', { academicRecord })"
|
||||||
|
@update="searcher.search()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DestroyView
|
||||||
|
ref="destroyCertModal"
|
||||||
|
subtitle="name"
|
||||||
|
:to="(certificate) => apiTo('destroy-certificate', { certificate })"
|
||||||
|
@update="searcher.search()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
21
src/pages/Admin/Users/Academic/Module.js
Normal file
21
src/pages/Admin/Users/Academic/Module.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { lang } from '@Lang/i18n';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||||
|
|
||||||
|
// Ruta API
|
||||||
|
const apiTo = (name, params = {}) => route(`admin.users.academic.${name}`, params)
|
||||||
|
|
||||||
|
// Ruta visual
|
||||||
|
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `admin.users.academic.${name}`, params, query })
|
||||||
|
|
||||||
|
// Obtener traducción del componente
|
||||||
|
const transl = (str) => lang(`users.academic.${str}`)
|
||||||
|
|
||||||
|
// Determina si un usuario puede hacer algo no en base a los permisos
|
||||||
|
const can = (permission) => hasPermission(`users.academic.${permission}`)
|
||||||
|
|
||||||
|
export {
|
||||||
|
can,
|
||||||
|
viewTo,
|
||||||
|
apiTo,
|
||||||
|
transl
|
||||||
|
}
|
||||||
@ -20,17 +20,21 @@ const form = useForm({
|
|||||||
maternal: '',
|
maternal: '',
|
||||||
email: '',
|
email: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
hire_date: '',
|
||||||
password: '',
|
password: '',
|
||||||
roles: []
|
roles: [],
|
||||||
|
department_id: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const roles = ref([]);
|
const roles = ref([]);
|
||||||
|
const departments = ref([]);
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
function submit() {
|
function submit() {
|
||||||
form.transform(data => ({
|
form.transform(data => ({
|
||||||
...data,
|
...data,
|
||||||
roles: data.roles.map(role => role.id)
|
roles: data.roles.map(role => role.id),
|
||||||
|
department_id: data.department_id?.id
|
||||||
})).post(apiTo('store'), {
|
})).post(apiTo('store'), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
Notify.success(Lang('register.create.onSuccess'))
|
Notify.success(Lang('register.create.onSuccess'))
|
||||||
@ -46,6 +50,15 @@ onMounted(() => {
|
|||||||
roles.value = r.roles;
|
roles.value = r.roles;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.catalog({
|
||||||
|
'department:all': null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (r) => {
|
||||||
|
departments.value = r['department:all'] ?? [];
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -82,5 +95,10 @@ onMounted(() => {
|
|||||||
:options="roles"
|
:options="roles"
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
|
<Selectable
|
||||||
|
v-model="form.department_id"
|
||||||
|
title="Departamento"
|
||||||
|
:options="departments"
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||||
import { api, useForm } from '@Services/Api';
|
import { api, useForm } from '@Services/Api';
|
||||||
import { viewTo, apiTo , transl } from './Module';
|
import { viewTo, apiTo , transl } from './Module';
|
||||||
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue'
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
import PageHeader from '@Holos/PageHeader.vue';
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import Selectable from '@Holos/Form/Selectable.vue';
|
||||||
import Form from './Form.vue'
|
import Form from './Form.vue'
|
||||||
|
|
||||||
/** Definiciones */
|
/** Definiciones */
|
||||||
@ -19,12 +20,19 @@ const form = useForm({
|
|||||||
paternal: '',
|
paternal: '',
|
||||||
maternal: '',
|
maternal: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
hire_date: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
department_id: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const departments = ref([]);
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
function submit() {
|
function submit() {
|
||||||
form.put(apiTo('update', { user: form.id }), {
|
form.transform(data => ({
|
||||||
|
...data,
|
||||||
|
department_id: data.department_id?.id
|
||||||
|
})).put(apiTo('update', { user: form.id }), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
Notify.success(Lang('register.edit.onSuccess'))
|
Notify.success(Lang('register.edit.onSuccess'))
|
||||||
router.push(viewTo({ name: 'index' }));
|
router.push(viewTo({ name: 'index' }));
|
||||||
@ -34,8 +42,24 @@ function submit() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
api.get(apiTo('show', { user: vroute.params.id }), {
|
api.get(apiTo('show', { user: vroute.params.id }), {
|
||||||
onSuccess: (r) => form.fill(r.user)
|
onSuccess: (r) => {
|
||||||
|
form.fill(r.user)
|
||||||
|
form.department_id = r.user.department
|
||||||
|
if (r.user.hire_date) {
|
||||||
|
form.hire_date = new Date(r.user.hire_date).toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.catalog({
|
||||||
|
'department:all': null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (r) => {
|
||||||
|
departments.value = r['department:all'] ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -54,5 +78,11 @@ onMounted(() => {
|
|||||||
action="update"
|
action="update"
|
||||||
:form="form"
|
:form="form"
|
||||||
@submit="submit"
|
@submit="submit"
|
||||||
/>
|
>
|
||||||
|
<Selectable
|
||||||
|
v-model="form.department_id"
|
||||||
|
title="Departamento"
|
||||||
|
:options="departments"
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -61,6 +61,13 @@ function submit() {
|
|||||||
:onError="form.errors.email"
|
:onError="form.errors.email"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.hire_date"
|
||||||
|
id="hire_date"
|
||||||
|
:onError="form.errors.hire_date"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<slot />
|
<slot />
|
||||||
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4">
|
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
|||||||
@ -2,19 +2,24 @@
|
|||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useSearcher } from '@Services/Api';
|
import { useSearcher } from '@Services/Api';
|
||||||
import { hasPermission } from '@Plugins/RolePermission';
|
import { hasPermission } from '@Plugins/RolePermission';
|
||||||
|
import { getDate } from '@Controllers/DateController';
|
||||||
import { can, apiTo, viewTo, transl } from './Module'
|
import { can, apiTo, viewTo, transl } from './Module'
|
||||||
|
|
||||||
import IconButton from '@Holos/Button/Icon.vue'
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
||||||
import SearcherHead from '@Holos/Searcher.vue';
|
import SearcherHead from '@Holos/Searcher.vue';
|
||||||
import Table from '@Holos/Table.vue';
|
import Table from '@Holos/NewTable.vue';
|
||||||
import ShowView from './Modals/Show.vue';
|
import ShowView from './Modals/Show.vue';
|
||||||
|
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Searcher from '@Holos/Searcher.vue';
|
||||||
|
import Adding from '@Holos/Button/ButtonRh.vue';
|
||||||
|
|
||||||
/** Propiedades */
|
/** Propiedades */
|
||||||
const models = ref([]);
|
const models = ref([]);
|
||||||
|
|
||||||
/** Referencias */
|
/** Referencias */
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const destroyModal = ref(false);
|
const destroyModal = ref(false);
|
||||||
|
|
||||||
/** Métodos */
|
/** Métodos */
|
||||||
@ -31,146 +36,86 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<!-- Header -->
|
||||||
<SearcherHead
|
<div class="flex items-start justify-between gap-4">
|
||||||
:title="transl('title')"
|
<div>
|
||||||
@search="(x) => searcher.search(x)"
|
<h1 class="text-3xl font-extrabold text-gray-900 dark:text-primary-dt">{{ $t('users.title') }}</h1>
|
||||||
>
|
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">{{ $t('users.description') }}
|
||||||
<RouterLink
|
</p>
|
||||||
v-if="can('create')"
|
</div>
|
||||||
:to="viewTo({ name: 'create' })"
|
|
||||||
>
|
<RouterLink :to="viewTo({ name: 'create' })">
|
||||||
<IconButton
|
<Adding text="Nuevo Empleado" />
|
||||||
class="text-white"
|
</RouterLink>
|
||||||
icon="add"
|
</div>
|
||||||
:title="$t('crud.create')"
|
|
||||||
filled
|
<!-- Search Card -->
|
||||||
/>
|
<div class="pt-2 w-full">
|
||||||
</RouterLink>
|
<Searcher @search="(x) => searcher.search(x)">
|
||||||
<IconButton
|
</Searcher>
|
||||||
icon="refresh"
|
</div>
|
||||||
:title="$t('refresh')"
|
|
||||||
@click="searcher.search()"
|
<!-- List Card -->
|
||||||
/>
|
<div class="pt-2 w-full">
|
||||||
</SearcherHead>
|
<Table :items="models" :processing="searcher.processing" @send-pagination="(page) => searcher.pagination(page)">
|
||||||
<div class="pt-2 w-full">
|
<template #head>
|
||||||
<Table
|
<th v-text="$t('name')" />
|
||||||
:items="models"
|
<th v-text="$t('department')" />
|
||||||
:processing="searcher.processing"
|
<th v-text="$t('headquarter')" />
|
||||||
@send-pagination="(page) => searcher.pagination(page)"
|
<th v-text="$t('hire_date')" />
|
||||||
>
|
<th v-text="$t('status')" />
|
||||||
<template #head>
|
<th class="w-32 text-center" v-text="$t('actions')" />
|
||||||
<th v-text="$t('user')" />
|
</template>
|
||||||
<th v-text="$t('contact')" />
|
|
||||||
<th
|
<template #body="{ items }">
|
||||||
v-text="$t('actions')"
|
<tr v-for="model in items" class="table-row">
|
||||||
class="w-32 text-center"
|
<td>{{ model.full_name }}</td>
|
||||||
/>
|
<td>{{ model.department?.name }}</td>
|
||||||
</template>
|
<td>{{ model.headquarter }}</td>
|
||||||
<template #body="{items}">
|
<td>{{ model.hire_date ? getDate(model.hire_date) : '-' }}</td>
|
||||||
<tr
|
<td class="py-6">
|
||||||
v-for="model in items"
|
<span
|
||||||
class="table-row"
|
class="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-700 dark:bg-success-d dark:text-success-dt">
|
||||||
>
|
{{ model.status_ek }}
|
||||||
<td class="table-cell">
|
</span>
|
||||||
{{ `${model.name} ${model.paternal}` }}
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td class="table-cell">
|
<div class="table-actions">
|
||||||
<p>
|
<IconButton icon="visibility" :title="$t('crud.show')" @click="showModal.open(model)"
|
||||||
<a
|
outline />
|
||||||
class="hover:underline"
|
<RouterLink v-if="can('edit')" class="h-fit"
|
||||||
target="_blank"
|
:to="viewTo({ name: 'edit', params: { id: model.id } })">
|
||||||
:href="`mailto:${model.email}`"
|
<IconButton icon="edit" :title="$t('crud.edit')" outline />
|
||||||
>
|
</RouterLink>
|
||||||
{{ model.email }}
|
<IconButton v-if="can('destroy')" icon="delete" :title="$t('crud.destroy')"
|
||||||
</a>
|
@click="destroyModal.open(model)" outline />
|
||||||
</p>
|
<RouterLink v-if="can('settings')" class="h-fit"
|
||||||
<p v-if="model.phone" class="font-semibold text-xs">
|
:to="viewTo({ name: 'settings', params: { id: model.id } })">
|
||||||
<b>Teléfono: </b>
|
<IconButton icon="settings" :title="$t('setting')" />
|
||||||
<span
|
</RouterLink>
|
||||||
class="hover:underline"
|
<RouterLink v-if="hasPermission('activities.index')" class="h-fit"
|
||||||
target="_blank"
|
:to="$view({ name: 'admin.activities.index', query: { user: model.id } })">
|
||||||
:href="`tel:${model.phone}`"
|
<IconButton icon="timeline" :title="$t('activity')" />
|
||||||
>
|
</RouterLink>
|
||||||
{{ model.phone }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</td>
|
|
||||||
<td class="table-cell">
|
|
||||||
<div class="table-actions">
|
|
||||||
<IconButton
|
|
||||||
icon="visibility"
|
|
||||||
:title="$t('crud.show')"
|
|
||||||
@click="showModal.open(model)"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
<RouterLink
|
|
||||||
v-if="can('edit')"
|
|
||||||
class="h-fit"
|
|
||||||
:to="viewTo({ name: 'edit', params: { id: model.id } })"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon="edit"
|
|
||||||
:title="$t('crud.edit')"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
<IconButton
|
|
||||||
v-if="can('destroy')"
|
|
||||||
icon="delete"
|
|
||||||
:title="$t('crud.destroy')"
|
|
||||||
@click="destroyModal.open(model)"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
<RouterLink
|
|
||||||
v-if="can('settings')"
|
|
||||||
class="h-fit"
|
|
||||||
:to="viewTo({ name: 'settings', params: { id: model.id } })"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon="settings"
|
|
||||||
:title="$t('setting')"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink
|
|
||||||
v-if="hasPermission('activities.index')"
|
|
||||||
class="h-fit"
|
|
||||||
:to="$view({ name: 'admin.activities.index', query: { user: model.id } })"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon="timeline"
|
|
||||||
:title="$t('activity')"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
<template #empty>
|
|
||||||
<td class="table-cell">
|
|
||||||
<div class="flex items-center text-sm">
|
|
||||||
<p class="font-semibold">
|
|
||||||
{{ $t('registers.empty') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-cell">-</td>
|
</tr>
|
||||||
<td class="table-cell">-</td>
|
</template>
|
||||||
</template>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ShowView
|
|
||||||
ref="showModal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DestroyView
|
<template #empty>
|
||||||
v-if="can('destroy')"
|
<td colspan="6" class="py-12 text-center">
|
||||||
ref="destroyModal"
|
<div class="text-gray-500 dark:text-primary-dt/70">
|
||||||
subtitle="last_name"
|
<GoogleIcon name="person_search" class="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||||
:to="(user) => apiTo('destroy', { user })"
|
<p class="text-lg font-medium">No se encontraron empleados</p>
|
||||||
@update="searcher.search()"
|
<p class="text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
|
||||||
/>
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<ShowView ref="showModal" />
|
||||||
|
|
||||||
|
<DestroyView v-if="can('destroy')" ref="destroyModal" subtitle="last_name"
|
||||||
|
:to="(user) => apiTo('destroy', { user })" @update="searcher.search()" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ const router = createRouter({
|
|||||||
title: 'Inicio',
|
title: 'Inicio',
|
||||||
icon: 'home',
|
icon: 'home',
|
||||||
},
|
},
|
||||||
redirect: '/admin/employees',
|
redirect: '/admin/dashboard',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
@ -141,42 +141,6 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
component: () => import('@Pages/Dashboard/Admin.vue'),
|
component: () => import('@Pages/Dashboard/Admin.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'employees',
|
|
||||||
name: 'admin.employees',
|
|
||||||
meta: {
|
|
||||||
title: 'Empleados',
|
|
||||||
icon: 'people',
|
|
||||||
},
|
|
||||||
redirect: '/admin/employees',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'admin.employees.index',
|
|
||||||
component: () => import('@Pages/Employees/Index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'academic',
|
|
||||||
name: 'admin.academic.index',
|
|
||||||
component: () => import('@Pages/Academic/Index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'security',
|
|
||||||
name: 'admin.security.index',
|
|
||||||
component: () => import('@Pages/Security/Index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'payroll',
|
|
||||||
name: 'admin.payroll.index',
|
|
||||||
component: () => import('@Pages/Payroll/Index.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'additional',
|
|
||||||
name: 'admin.additional.index',
|
|
||||||
component: () => import('@Pages/Additional/Index.vue'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'vacations',
|
path: 'vacations',
|
||||||
name: 'admin.vacations',
|
name: 'admin.vacations',
|
||||||
@ -267,19 +231,16 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'admin.users.index',
|
name: 'admin.users.index',
|
||||||
beforeEnter: (to, from, next) => can(next, 'users.index'),
|
|
||||||
component: () => import('@Pages/Admin/Users/Index.vue'),
|
component: () => import('@Pages/Admin/Users/Index.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'online',
|
path: 'online',
|
||||||
name: 'admin.users.online',
|
name: 'admin.users.online',
|
||||||
beforeEnter: (to, from, next) => can(next, 'users.online'),
|
|
||||||
component: () => import('@Pages/Admin/Users/Online.vue')
|
component: () => import('@Pages/Admin/Users/Online.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'create',
|
path: 'create',
|
||||||
name: 'admin.users.create',
|
name: 'admin.users.create',
|
||||||
beforeEnter: (to, from, next) => can(next, 'users.create'),
|
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Crear',
|
title: 'Crear',
|
||||||
icon: 'add',
|
icon: 'add',
|
||||||
@ -288,7 +249,6 @@ const router = createRouter({
|
|||||||
}, {
|
}, {
|
||||||
path: ':id/edit',
|
path: ':id/edit',
|
||||||
name: 'admin.users.edit',
|
name: 'admin.users.edit',
|
||||||
beforeEnter: (to, from, next) => can(next, 'users.edit'),
|
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Editar',
|
title: 'Editar',
|
||||||
icon: 'edit',
|
icon: 'edit',
|
||||||
@ -297,13 +257,54 @@ const router = createRouter({
|
|||||||
}, {
|
}, {
|
||||||
path: ':id/settings',
|
path: ':id/settings',
|
||||||
name: 'admin.users.settings',
|
name: 'admin.users.settings',
|
||||||
beforeEnter: (to, from, next) => can(next, 'users.settings'),
|
|
||||||
component: () => import('@Pages/Admin/Users/Settings.vue'),
|
component: () => import('@Pages/Admin/Users/Settings.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Configuración',
|
title: 'Configuración',
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: 'academic',
|
||||||
|
name: 'admin.users.academic',
|
||||||
|
redirect: '/admin/users/academic',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'admin.users.academic.index',
|
||||||
|
component: () => import('@Pages/Admin/Users/Academic/Index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/create-record',
|
||||||
|
name: 'admin.users.academic.createRecord',
|
||||||
|
component: () => import('@Pages/Admin/Users/Academic/CreateRecord.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/create-certification',
|
||||||
|
name: 'admin.users.academic.createCertification',
|
||||||
|
component: () => import('@Pages/Admin/Users/Academic/CreateCertification.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit-record/:record',
|
||||||
|
name: 'admin.users.academic.editRecord',
|
||||||
|
component: () => import('@Pages/Admin/Users/Academic/EditRecord.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit-certification/:certification',
|
||||||
|
name: 'admin.users.academic.editCertification',
|
||||||
|
component: () => import('@Pages/Admin/Users/Academic/EditCertification.vue'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'security',
|
||||||
|
name: 'admin.users.security',
|
||||||
|
component: () => import('@Pages/Admin/Users/Security.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'additional',
|
||||||
|
name: 'admin.users.additional',
|
||||||
|
component: () => import('@Pages/Admin/Users/Additional.vue'),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user