feature-comercial-module-ts #10

Merged
edgar.mendez merged 41 commits from feature-comercial-module-ts into qa 2026-02-13 19:52:10 +00:00
7 changed files with 894 additions and 0 deletions
Showing only changes of commit 59ddae0d46 - Show all commits

View File

@ -48,6 +48,14 @@ const menuItems = ref<MenuItem[]>([
label: 'Configuración',
icon: 'pi pi-cog',
to: '/configuracion'
},
{
label: 'Usuarios y Roles',
icon: 'pi pi-users',
items: [
{ label: 'Usuarios', icon: 'pi pi-user', to: '/users' },
{ label: 'Roles', icon: 'pi pi-shield', to: '/users/roles' }
]
}
]);

View File

@ -0,0 +1,113 @@
<template>
<div>
<!-- Encabezado principal -->
<div class="mb-8">
<h1 class="text-2xl md:text-3xl font-black text-slate-900 dark:text-white mb-1">Gestión de Roles y Permisos</h1>
<p class="text-base text-slate-500 dark:text-slate-400">Asigna permisos específicos a cada rol de usuario.</p>
</div>
<!-- Action Bar -->
<div class="sticky top-20 z-10 flex flex-wrap items-center justify-between gap-4 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md p-4 mb-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
<div class="flex-1 min-w-[200px]">
<p class="text-lg font-bold text-slate-900 dark:text-white">Permisos para <span class="text-primary">{{ activeRoleName }}</span></p>
<p class="text-sm text-slate-500 dark:text-slate-400">Hay cambios sin guardar</p>
</div>
<div class="flex items-center gap-3">
<Button label="Cancelar" class="min-w-[84px]" outlined @click="onCancel" />
<Button label="Guardar Cambios" class="min-w-[84px] px-5" @click="onSave" />
</div>
</div>
<!-- Permissions Content -->
<div class="space-y-6">
<div v-for="module in modules" :key="module.name" class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800">
<div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
<h4 class="text-base font-bold text-slate-800 dark:text-slate-200">{{ module.label }}</h4>
<template v-if="module.name === 'reportes'">
<Button label="DESELECCIONAR TODO" text class="text-xs font-bold text-primary" @click="deselectAll(module)" />
</template>
<template v-else>
<Button label="SELECCIONAR TODO" text class="text-xs font-bold" @click="selectAll(module)" />
</template>
</div>
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div v-for="perm in module.permissions" :key="perm.key" class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">{{ perm.label }}</p>
<InputSwitch v-model="perm.enabled" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import Button from 'primevue/button';
import InputSwitch from 'primevue/inputswitch';
// Mock roles list
const rolesList = ref([
{ id: 1, name: 'Administrador General', icon: 'shield_person', active: true },
{ id: 2, name: 'Gerente de Tienda', icon: 'storefront', active: false },
{ id: 3, name: 'Cajero', icon: 'point_of_sale', active: false },
{ id: 4, name: 'Bodeguero', icon: 'inventory_2', active: false },
]);
const activeRoleName = computed(() => {
const active = rolesList.value.find(r => r.active);
return active ? active.name + (active.id === 1 ? ' *' : '') : '';
});
// Mock modules and permissions
const modules = ref([
{
name: 'ventas',
label: 'Módulo de Ventas (POS)',
permissions: [
{ key: 'acceso_tpv', label: 'Acceso al TPV', enabled: true },
{ key: 'crear_ventas', label: 'Crear Ventas', enabled: true },
{ key: 'aplicar_descuentos', label: 'Aplicar Descuentos', enabled: true },
{ key: 'realizar_devoluciones', label: 'Realizar Devoluciones', enabled: true },
{ key: 'anular_tickets', label: 'Anular Tickets', enabled: false },
]
},
{
name: 'inventario',
label: 'Módulo de Inventario',
permissions: [
{ key: 'ver_productos', label: 'Ver Productos', enabled: true },
{ key: 'crear_editar_productos', label: 'Crear/Editar Productos', enabled: true },
{ key: 'ajustes_stock', label: 'Ajustes de Stock', enabled: true },
{ key: 'gestionar_proveedores', label: 'Gestionar Proveedores', enabled: false },
]
},
{
name: 'reportes',
label: 'Módulo de Reportes',
permissions: [
{ key: 'acceder_reportes', label: 'Acceder a Reportes', enabled: true },
{ key: 'ver_dashboard', label: 'Ver Dashboard', enabled: true },
{ key: 'exportar_datos', label: 'Exportar Datos', enabled: true },
]
}
]);
function onAddRole() {
// TODO: Acción para añadir nuevo rol
}
function onCancel() {
// TODO: Acción para cancelar edición
}
function onSave() {
// TODO: Acción para guardar cambios
}
function selectAll(module: any) {
module.permissions.forEach((perm: any) => perm.enabled = true);
}
function deselectAll(module: any) {
module.permissions.forEach((perm: any) => perm.enabled = false);
}
</script>

View File

@ -0,0 +1,403 @@
<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>

View File

@ -0,0 +1,214 @@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- Left Column: Roles List -->
<div class="lg:col-span-4 xl:col-span-3">
<div class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-4 h-full">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-200">Roles de Usuario</h3>
</div>
<div class="flex flex-col gap-2">
<a class="flex items-center gap-4 bg-primary/10 dark:bg-primary/20 text-primary dark:text-white p-3 rounded-lg border border-primary/50"
href="#">
<span class="material-symbols-outlined">shield_person</span>
<span class="text-sm font-bold flex-1 truncate">Administrador General</span>
<span class="text-primary font-bold text-lg">*</span>
</a>
<a class="flex items-center gap-4 hover:bg-slate-100 dark:hover:bg-slate-800/50 p-3 rounded-lg"
href="#">
<span class="material-symbols-outlined text-slate-500 dark:text-slate-400">storefront</span>
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1 truncate">Gerente de
Tienda</span>
</a>
<a class="flex items-center gap-4 hover:bg-slate-100 dark:hover:bg-slate-800/50 p-3 rounded-lg"
href="#">
<span class="material-symbols-outlined text-slate-500 dark:text-slate-400">point_of_sale</span>
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1 truncate">Cajero</span>
</a>
<a class="flex items-center gap-4 hover:bg-slate-100 dark:hover:bg-slate-800/50 p-3 rounded-lg"
href="#">
<span class="material-symbols-outlined text-slate-500 dark:text-slate-400">inventory_2</span>
<span
class="text-sm font-medium text-slate-700 dark:text-slate-300 flex-1 truncate">Bodeguero</span>
</a>
<div class="border-t border-slate-200 dark:border-slate-800 my-2"></div>
<button
class="w-full flex items-center justify-center gap-2 text-primary dark:text-primary hover:bg-primary/10 dark:hover:bg-primary/20 p-3 rounded-lg text-sm font-bold">
<span class="material-symbols-outlined text-base">add_circle</span>
<span>Añadir Nuevo Rol</span>
</button>
</div>
</div>
</div>
<!-- Right Column: Permissions Area -->
<div class="lg:col-span-8 xl:col-span-9">
<!-- Action Bar -->
<div
class="sticky top-20 z-10 flex flex-wrap items-center justify-between gap-4 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md p-4 mb-6 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
<div class="flex-1 min-w-[200px]">
<p class="text-lg font-bold text-slate-900 dark:text-white">Permisos para <span
class="text-primary">Administrador General *</span></p>
<p class="text-sm text-slate-500 dark:text-slate-400">Hay cambios sin guardar</p>
</div>
<div class="flex items-center gap-3">
<button
class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 hover:bg-slate-200 dark:hover:bg-slate-700 text-sm font-bold">Cancelar</button>
<button
class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-5 bg-primary text-white text-sm font-bold hover:bg-primary/90">Guardar
Cambios</button>
</div>
</div>
<!-- Permissions Content -->
<div class="space-y-6">
<!-- Module Card: Ventas (POS) -->
<div class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800">
<div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
<h4 class="text-base font-bold text-slate-800 dark:text-slate-200">Módulo de Ventas (POS)</h4>
<button class="text-primary dark:text-primary text-xs font-bold hover:underline">SELECCIONAR
TODO</button>
</div>
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<!-- Permission Item -->
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Acceso al TPV</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Crear Ventas</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Aplicar Descuentos</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Realizar Devoluciones</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Anular Tickets</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input class="invisible absolute" type="checkbox" />
</label>
</div>
</div>
</div>
<!-- Module Card: Inventario -->
<div class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800">
<div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
<h4 class="text-base font-bold text-slate-800 dark:text-slate-200">Módulo de Inventario</h4>
<button class="text-primary dark:text-primary text-xs font-bold hover:underline">SELECCIONAR
TODO</button>
</div>
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Ver Productos</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Crear/Editar Productos</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Ajustes de Stock</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Gestionar Proveedores</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input class="invisible absolute" type="checkbox" />
</label>
</div>
</div>
</div>
<!-- Module Card: Reportes -->
<div class="bg-white dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800">
<div class="flex items-center justify-between p-4 border-b border-slate-200 dark:border-slate-800">
<h4 class="text-base font-bold text-slate-800 dark:text-slate-200">Módulo de Reportes</h4>
<button class="text-primary dark:text-primary text-xs font-bold hover:underline">DESELECCIONAR
TODO</button>
</div>
<div class="p-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Acceder a Reportes</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Ver Dashboard</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
<div class="flex items-center justify-between bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Exportar Datos</p>
<label
class="relative flex h-[26px] w-[44px] cursor-pointer items-center rounded-full border-none bg-slate-200 dark:bg-slate-700 p-0.5 has-[:checked]:bg-primary">
<div
class="h-full w-[22px] rounded-full bg-white transition-transform duration-200 ease-in-out transform translate-x-0 has-[:checked]:translate-x-[18px]">
</div>
<input checked="" class="invisible absolute" type="checkbox" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,65 @@
import api from "../../../services/api"
import type {
Role,
RolePagination,
RoleResponse,
CreateRoleData,
UpdateRoleData,
SingleRoleResponse,
ApiError
} from '../types/role'
export const roleService = {
async getAllRoles(page = 1, perPage = 25) {
try {
console.log('🔍 Fetching roles from API...');
const response = await api.get<RoleResponse>('/api/admin/roles', {
params: {
page,
per_page: perPage
}
});
console.log('✅ Roles fetched successfully:', response.data);
return response;
} catch (error) {
console.error('❌ Error fetching roles:', error);
throw error;
}
},
async createRole(data: CreateRoleData) {
try {
console.log('🔍 Creating role...', data);
const response = await api.post<SingleRoleResponse>('/api/admin/roles', data);
console.log('✅ Role created successfully:', response.data);
return response;
} catch (error) {
console.error('❌ Error creating role:', error);
throw error;
}
},
async updateRole(id: number, data: UpdateRoleData) {
try {
console.log('🔍 Updating role...', { id, data });
const response = await api.put<SingleRoleResponse>(`/api/admin/roles/${id}`, data);
console.log('✅ Role updated successfully:', response.data);
return response;
} catch (error) {
console.error('❌ Error updating role:', error);
throw error;
}
},
async deleteRole(id: number) {
try {
console.log('🔍 Deleting role...', { id });
const response = await api.delete(`/api/admin/roles/${id}`);
console.log('✅ Role deleted successfully:', response.data);
return response;
} catch (error) {
console.error('❌ Error deleting role:', error);
throw error;
}
}
}

59
src/modules/users/types/role.d.ts vendored Normal file
View File

@ -0,0 +1,59 @@
// Base Role interface
export interface Role {
id: number;
name: string;
guard_name: string;
description: string;
created_at: string;
updated_at: string;
}
// Pagination interfaces
export interface RolePagination {
current_page: number;
data: Role[];
first_page_url: string;
from: number;
last_page: number;
last_page_url: string;
links: Array<{url: string | null; label: string; active: boolean}>;
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number;
total: number;
}
// API Response interfaces
export interface RoleResponse {
status: string;
data: {
models: RolePagination;
};
}
export interface SingleRoleResponse {
status: string;
data: {
model: Role;
};
}
// Form data interfaces
export interface CreateRoleData {
description: string;
}
export interface UpdateRoleData {
description: string;
}
// Error handling interface
export interface ApiError {
status: string;
message: string;
errors: {
[key: string]: string[];
};
}

View File

@ -14,6 +14,9 @@ import ProductsIndex from '../modules/products/components/ProductsIndex.vue';
import ProductForm from '../modules/products/components/ProductForm.vue';
import StoresIndex from '../modules/stores/components/StoresIndex.vue';
import RolesIndex from '../modules/users/components/RoleIndex.vue';
import RoleForm from '../modules/users/components/RoleForm.vue';
const routes: RouteRecordRaw[] = [
{
path: '/login',
@ -151,6 +154,35 @@ const routes: RouteRecordRaw[] = [
title: 'Tiendas',
requiresAuth: true
},
},
{
path: 'users',
name: 'Usuarios',
meta: {
title: 'Roles',
requiresAuth: true
},
children: [
{
path: 'roles',
name: 'Roles',
component: RolesIndex,
meta: {
title: 'Roles',
requiresAuth: true
}
},
{
path: 'roles/permissions/:id',
name: 'RolePermissions',
component: RoleForm,
meta: {
title: 'Administrar Permisos',
requiresAuth: true
}
},
]
}
]
},