403 lines
14 KiB
Vue
403 lines
14 KiB
Vue
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
<!-- Toast Notifications -->
|
|
<Toast position="bottom-right" />
|
|
|
|
<!-- Confirm Dialog -->
|
|
<ConfirmDialog />
|
|
|
|
<!-- Header -->
|
|
<div class="flex flex-wrap justify-between gap-4 items-center">
|
|
<div class="flex min-w-72 flex-col gap-1">
|
|
<h1 class="text-surface-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">
|
|
Administración de Roles de Usuario
|
|
</h1>
|
|
<p class="text-surface-500 dark:text-surface-400 text-base font-normal leading-normal">
|
|
Crea nuevos roles, edita los existentes y gestiona los permisos del sistema.
|
|
</p>
|
|
</div>
|
|
<Button
|
|
label="Nuevo Rol"
|
|
icon="pi pi-plus"
|
|
@click="openCreateDialog"
|
|
class="min-w-[180px]"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Table Card -->
|
|
<Card class="shadow-sm">
|
|
<template #content>
|
|
<!-- Search and Filters -->
|
|
<div class="flex flex-col md:flex-row justify-between gap-4 mb-4">
|
|
<div class="flex-1">
|
|
<IconField iconPosition="left">
|
|
<InputIcon class="pi pi-search" />
|
|
<InputText
|
|
v-model="searchQuery"
|
|
placeholder="Buscar roles por nombre..."
|
|
class="w-full"
|
|
/>
|
|
</IconField>
|
|
</div>
|
|
<div class="flex gap-2 flex-wrap">
|
|
<Dropdown
|
|
v-model="selectedStatus"
|
|
:options="statusOptions"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
placeholder="Estado: Todos"
|
|
class="w-40"
|
|
/>
|
|
<Dropdown
|
|
v-model="selectedType"
|
|
:options="typeOptions"
|
|
optionLabel="label"
|
|
optionValue="value"
|
|
placeholder="Tipo: Todos"
|
|
class="w-40"
|
|
/>
|
|
<Button
|
|
label="Limpiar"
|
|
icon="pi pi-times"
|
|
outlined
|
|
@click="clearFilters"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<DataTable
|
|
:value="roles"
|
|
:rows="rowsPerPage"
|
|
:totalRecords="totalRecords"
|
|
:loading="loading"
|
|
paginator
|
|
lazy
|
|
:rowsPerPageOptions="[5, 10, 20, 50]"
|
|
@page="onPage"
|
|
stripedRows
|
|
responsiveLayout="scroll"
|
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
|
|
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} resultados"
|
|
>
|
|
<Column field="name" header="Nombre del Rol" sortable>
|
|
<template #body="slotProps">
|
|
<span class="font-medium">{{ slotProps.data.name }}</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="description" header="Descripción" sortable>
|
|
<template #body="slotProps">
|
|
<span class="text-slate-600 dark:text-slate-400">{{ slotProps.data.description }}</span>
|
|
</template>
|
|
</Column>
|
|
<Column field="created_at" header="Fecha de Creación">
|
|
<template #body="slotProps">
|
|
{{ formatDate(slotProps.data.created_at) }}
|
|
</template>
|
|
</Column>
|
|
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right" style="min-width: 160px">
|
|
<template #body="slotProps">
|
|
<div class="flex justify-end gap-2">
|
|
<Button icon="pi pi-shield" size="small" text @click="handlePermissions(slotProps.data)" v-tooltip="'Gestionar Permisos'" />
|
|
<Button icon="pi pi-pencil" size="small" text @click="handleEdit(slotProps.data)" v-tooltip="'Editar'" />
|
|
<Button icon="pi pi-trash" size="small" text severity="danger" @click="handleDelete(slotProps.data)" v-tooltip="'Eliminar'" />
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
</template>
|
|
</Card>
|
|
|
|
<!-- Role Form Modal -->
|
|
<Dialog
|
|
v-model:visible="showDialog"
|
|
modal
|
|
:header="isEditing ? 'Editar Rol' : 'Crear Nuevo Rol'"
|
|
:style="{ width: '90vw', maxWidth: '500px' }"
|
|
:contentStyle="{ padding: '1.5rem' }"
|
|
>
|
|
<form @submit.prevent="handleSaveRole" class="space-y-4">
|
|
<!-- Role Description -->
|
|
<div class="flex flex-col gap-2">
|
|
<label for="modal-role-description" class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
Descripción del Rol *
|
|
</label>
|
|
<InputText
|
|
id="modal-role-description"
|
|
v-model="formData.description"
|
|
:invalid="showError('description')"
|
|
placeholder="Ej: Administrador de almacén"
|
|
class="w-full"
|
|
/>
|
|
<small v-if="showError('description')" class="text-red-500">
|
|
{{ getErrorMessage('description') }}
|
|
</small>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex items-center justify-end gap-3 pt-4">
|
|
<Button
|
|
label="Cancelar"
|
|
outlined
|
|
@click="handleCancelForm"
|
|
:disabled="modalLoading"
|
|
/>
|
|
<Button
|
|
:label="isEditing ? 'Actualizar' : 'Crear Rol'"
|
|
type="submit"
|
|
:loading="modalLoading"
|
|
:disabled="!isFormValid && modalTouched"
|
|
/>
|
|
</div>
|
|
</form>
|
|
</Dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import Button from 'primevue/button';
|
|
import InputText from 'primevue/inputtext';
|
|
import DataTable from 'primevue/datatable';
|
|
import Column from 'primevue/column';
|
|
import Card from 'primevue/card';
|
|
import Dropdown from 'primevue/dropdown';
|
|
import Toast from 'primevue/toast';
|
|
import Dialog from 'primevue/dialog';
|
|
import ConfirmDialog from 'primevue/confirmdialog';
|
|
import { useToast } from 'primevue/usetoast';
|
|
import { useConfirm } from 'primevue/useconfirm';
|
|
import IconField from 'primevue/iconfield';
|
|
import InputIcon from 'primevue/inputicon';
|
|
import { roleService } from '../services/roleService';
|
|
import type { Role, CreateRoleData, UpdateRoleData } from '../types/role';
|
|
|
|
const toast = useToast();
|
|
const confirm = useConfirm();
|
|
const router = useRouter();
|
|
|
|
const roles = ref<Role[]>([]);
|
|
const searchQuery = ref('');
|
|
const selectedStatus = ref('all');
|
|
const selectedType = ref('all');
|
|
const loading = ref(false);
|
|
const totalRecords = ref(0);
|
|
const rowsPerPage = ref(10);
|
|
const currentPage = ref(1);
|
|
|
|
// Modal states
|
|
const showDialog = ref(false);
|
|
const isEditing = ref(false);
|
|
const selectedRole = ref<Role | null>(null);
|
|
const modalLoading = ref(false);
|
|
const modalTouched = ref(false);
|
|
const fieldErrors = ref<Record<string, string[]>>({});
|
|
|
|
// Form data
|
|
const formData = ref<CreateRoleData>({
|
|
description: ''
|
|
});
|
|
|
|
const isFormValid = computed(() =>
|
|
formData.value.description.trim() !== ''
|
|
);
|
|
|
|
const statusOptions = [
|
|
{ label: 'Todos', value: 'all' },
|
|
{ label: 'Activo', value: 'active' },
|
|
{ label: 'Inactivo', value: 'inactive' },
|
|
];
|
|
const typeOptions = [
|
|
{ label: 'Todos', value: 'all' },
|
|
{ label: 'Sistema', value: 'system' },
|
|
{ label: 'Personalizado', value: 'custom' },
|
|
];
|
|
|
|
function clearFilters() {
|
|
searchQuery.value = '';
|
|
selectedStatus.value = 'all';
|
|
selectedType.value = 'all';
|
|
}
|
|
|
|
function formatDate(date: string) {
|
|
return new Date(date).toLocaleDateString('es-MX');
|
|
}
|
|
|
|
// Fetch roles from API
|
|
const fetchRoles = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const response = await roleService.getAllRoles(currentPage.value, rowsPerPage.value);
|
|
const data = response.data.data.models;
|
|
roles.value = data.data;
|
|
totalRecords.value = data.total;
|
|
currentPage.value = data.current_page;
|
|
} catch (error) {
|
|
console.error('Error fetching roles:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: 'No se pudieron cargar los roles',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
function onPage(event: any) {
|
|
currentPage.value = event.page + 1;
|
|
rowsPerPage.value = event.rows;
|
|
fetchRoles();
|
|
}
|
|
|
|
function openCreateDialog() {
|
|
selectedRole.value = null;
|
|
isEditing.value = false;
|
|
formData.value = { description: '' };
|
|
modalTouched.value = false;
|
|
fieldErrors.value = {};
|
|
showDialog.value = true;
|
|
}
|
|
|
|
function handlePermissions(role: Role) {
|
|
router.push({ name: 'RolePermissions', params: { id: role.id } });
|
|
}
|
|
|
|
function handleEdit(role: Role) {
|
|
selectedRole.value = role;
|
|
isEditing.value = true;
|
|
formData.value = { description: role.description };
|
|
modalTouched.value = false;
|
|
fieldErrors.value = {};
|
|
showDialog.value = true;
|
|
}
|
|
|
|
function handleCancelForm() {
|
|
showDialog.value = false;
|
|
selectedRole.value = null;
|
|
formData.value = { description: '' };
|
|
modalTouched.value = false;
|
|
fieldErrors.value = {};
|
|
}
|
|
|
|
function showError(field: keyof CreateRoleData): boolean {
|
|
// Mostrar error si hay errores del servidor para este campo
|
|
if (fieldErrors.value[field] && fieldErrors.value[field].length > 0) {
|
|
return true;
|
|
}
|
|
// Mostrar error de validación local solo después del primer intento
|
|
if (!modalTouched.value) return false;
|
|
return formData.value[field].trim() === '';
|
|
}
|
|
|
|
function getErrorMessage(field: keyof CreateRoleData): string {
|
|
// Priorizar errores del servidor
|
|
if (fieldErrors.value[field] && fieldErrors.value[field].length > 0) {
|
|
return fieldErrors.value[field][0] || '';
|
|
}
|
|
// Mensaje de validación local
|
|
if (modalTouched.value && formData.value[field].trim() === '') {
|
|
return 'La descripción del rol es obligatoria';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
async function handleSaveRole() {
|
|
modalTouched.value = true;
|
|
fieldErrors.value = {}; // Limpiar errores anteriores
|
|
|
|
if (!isFormValid.value) {
|
|
return;
|
|
}
|
|
|
|
modalLoading.value = true;
|
|
|
|
try {
|
|
if (isEditing.value && selectedRole.value) {
|
|
await roleService.updateRole(selectedRole.value.id, formData.value);
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Rol Actualizado',
|
|
detail: `El rol "${formData.value.description}" ha sido actualizado exitosamente.`,
|
|
life: 3000
|
|
});
|
|
} else {
|
|
await roleService.createRole(formData.value);
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Rol Creado',
|
|
detail: `El rol "${formData.value.description}" ha sido creado exitosamente.`,
|
|
life: 3000
|
|
});
|
|
}
|
|
|
|
await fetchRoles(); // Recargar la tabla
|
|
handleCancelForm();
|
|
|
|
} catch (error: any) {
|
|
console.error('Error saving role:', error);
|
|
|
|
// Manejar errores específicos del backend
|
|
if (error.response && error.response.data && error.response.data.errors) {
|
|
fieldErrors.value = error.response.data.errors;
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error de Validación',
|
|
detail: error.response.data.message || 'Revisa los campos marcados en rojo.',
|
|
life: 4000
|
|
});
|
|
} else {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: 'No se pudo guardar el rol. Inténtalo de nuevo.',
|
|
life: 3000
|
|
});
|
|
}
|
|
} finally {
|
|
modalLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function handleDelete(role: Role) {
|
|
confirm.require({
|
|
message: `¿Estás seguro de eliminar el rol "${role.description}"?`,
|
|
header: 'Confirmar Eliminación',
|
|
icon: 'pi pi-exclamation-triangle',
|
|
rejectLabel: 'Cancelar',
|
|
acceptLabel: 'Eliminar',
|
|
rejectClass: 'p-button-secondary p-button-outlined',
|
|
acceptClass: 'p-button-danger',
|
|
accept: async () => {
|
|
try {
|
|
await roleService.deleteRole(role.id);
|
|
toast.add({
|
|
severity: 'success',
|
|
summary: 'Rol Eliminado',
|
|
detail: `El rol "${role.description}" ha sido eliminado exitosamente.`,
|
|
life: 3000
|
|
});
|
|
await fetchRoles(); // Recargar la tabla
|
|
} catch (error) {
|
|
console.error('Error deleting role:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: 'No se pudo eliminar el rol. Inténtalo de nuevo.',
|
|
life: 3000
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Load roles on component mount
|
|
onMounted(() => {
|
|
fetchRoles();
|
|
});
|
|
</script> |