Nuevo diseño usuarios

This commit is contained in:
jose.lopez 2025-09-25 15:52:04 -06:00
parent 68cde3dcea
commit 004836ece7
10 changed files with 287 additions and 205 deletions

View 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>

View File

@ -150,6 +150,7 @@ export default {
}, },
dashboard: 'Dashboard', dashboard: 'Dashboard',
date: 'Fecha', date: 'Fecha',
department: 'Departamento',
dates: { dates: {
start: 'Fecha Inicial', start: 'Fecha Inicial',
end: 'Fecha Final' end: 'Fecha Final'
@ -202,6 +203,8 @@ 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',
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.'
@ -405,6 +408,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'

View File

@ -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"
/> />
<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"

View File

@ -21,16 +21,19 @@ const form = useForm({
email: '', email: '',
phone: '', phone: '',
password: '', password: '',
roles: [] roles: [],
departmen_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),
departmen_id: data.departmen_id?.id
})).post(apiTo('store'), { })).post(apiTo('store'), {
onSuccess: () => { onSuccess: () => {
Notify.success(Lang('register.create.onSuccess')) Notify.success(Lang('register.create.onSuccess'))
@ -46,6 +49,17 @@ onMounted(() => {
roles.value = r.roles; roles.value = r.roles;
} }
}); });
api.catalog({
'department:all': null,
},
{
onSuccess: (r) => {
console.log(r);
departments.value = r['department:all'] ?? [];
}
}
);
}) })
</script> </script>
@ -82,5 +96,10 @@ onMounted(() => {
:options="roles" :options="roles"
multiple multiple
/> />
<Selectable
v-model="form.departmen_id"
title="Departamentos"
:options="departments"
/>
</Form> </Form>
</template> </template>

View File

@ -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')"
:to="viewTo({ name: 'create' })"
>
<IconButton
class="text-white"
icon="add"
:title="$t('crud.create')"
filled
/>
</RouterLink>
<IconButton
icon="refresh"
:title="$t('refresh')"
@click="searcher.search()"
/>
</SearcherHead>
<div class="pt-2 w-full">
<Table
:items="models"
:processing="searcher.processing"
@send-pagination="(page) => searcher.pagination(page)"
>
<template #head>
<th v-text="$t('user')" />
<th v-text="$t('contact')" />
<th
v-text="$t('actions')"
class="w-32 text-center"
/>
</template>
<template #body="{items}">
<tr
v-for="model in items"
class="table-row"
>
<td class="table-cell">
{{ `${model.name} ${model.paternal}` }}
</td>
<td class="table-cell">
<p>
<a
class="hover:underline"
target="_blank"
:href="`mailto:${model.email}`"
>
{{ model.email }}
</a>
</p>
<p v-if="model.phone" class="font-semibold text-xs">
<b>Teléfono: </b>
<span
class="hover:underline"
target="_blank"
:href="`tel:${model.phone}`"
>
{{ 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>
</td>
<td class="table-cell">-</td>
<td class="table-cell">-</td>
</template>
</Table>
</div> </div>
<ShowView <RouterLink :to="viewTo({ name: 'create' })">
ref="showModal" <Adding text="Nuevo Empleado" />
/> </RouterLink>
</div>
<DestroyView <!-- Search Card -->
v-if="can('destroy')" <div class="pt-2 w-full">
ref="destroyModal" <Searcher @search="(x) => searcher.search(x)">
subtitle="last_name" </Searcher>
:to="(user) => apiTo('destroy', { user })" </div>
@update="searcher.search()"
/> <!-- List Card -->
<div class="pt-2 w-full">
<Table :items="models" :processing="searcher.processing" @send-pagination="(page) => searcher.pagination(page)">
<template #head>
<th v-text="$t('name')" />
<th v-text="$t('department')" />
<th v-text="$t('headquarter')" />
<th v-text="$t('hire_date')" />
<th v-text="$t('status')" />
<th class="w-32 text-center" v-text="$t('actions')" />
</template>
<template #body="{ items }">
<tr v-for="model in items" class="table-row">
<td>{{ model.full_name }}</td>
<td>{{ model.department?.name }}</td>
<td>{{ model.headquarter }}</td>
<td>{{ getDate(model.hire_date) }}</td>
<td class="py-6">
<span
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 }}
</span>
</td>
<td>
<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 colspan="6" class="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 empleados</p>
<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>

View File

@ -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,27 @@ 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: 'admin/users/academic',
name: 'admin.users.academic',
component: () => import('@Pages/Admin/Users/Academic.vue'),
},
{
path: 'admin/users/security',
name: 'admin.users.security',
component: () => import('@Pages/Admin/Users/Security.vue'),
},
{
path: 'admin/users/additional',
name: 'admin.users.additional',
component: () => import('@Pages/Admin/Users/Additional.vue'),
},
] ]
}, },
{ {