feat: implementar gestión de pestañas para números de serie y mejorar lógica de estado

This commit is contained in:
Juan Felipe Zapata Moreno 2026-01-27 21:33:26 -06:00
parent d469b18bf5
commit 5b7b6f2343
2 changed files with 74 additions and 26 deletions

View File

@ -1,15 +1,29 @@
<script setup> <script setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import useLoader from '@Stores/Loader'; import useLoader from '@Stores/Loader';
import { hasToken } from '@Services/Api'; import { hasToken } from '@Services/Api';
/** Definidores */ /** Definidores */
const router = useRouter(); const router = useRouter();
const route = useRoute();
const loader = useLoader(); const loader = useLoader();
/** Rutas públicas que no requieren autenticación */
const publicRoutes = [
'facturacion.index',
'auth.index',
'auth.forgot-password',
'auth.reset-password'
];
/** Ciclos */ /** Ciclos */
onMounted(() => { onMounted(() => {
// No redirigir si estamos en una ruta pública
if (publicRoutes.includes(route.name)) {
return;
}
if(!hasToken()) { if(!hasToken()) {
return router.push({ name: 'auth.index' }) return router.push({ name: 'auth.index' })
} }

View File

@ -18,7 +18,7 @@ const router = useRouter();
const inventoryId = computed(() => route.params.id); const inventoryId = computed(() => route.params.id);
const inventory = ref(null); const inventory = ref(null);
const serials = ref({ data: [], total: 0 }); const serials = ref({ data: [], total: 0 });
const statusFilter = ref(''); const activeTab = ref('disponible');
// Modales // Modales
const showCreateModal = ref(false); const showCreateModal = ref(false);
@ -62,16 +62,17 @@ const loadSerials = (filters = {}) => {
url: apiURL(`inventario/${inventoryId.value}/serials`), url: apiURL(`inventario/${inventoryId.value}/serials`),
filters: { filters: {
...filters, ...filters,
status: statusFilter.value || undefined status: activeTab.value
} }
}); });
}; };
const onSearch = (query) => { const onSearch = (query) => {
searcher.search(query, { status: statusFilter.value || undefined }); searcher.search(query, { status: activeTab.value });
}; };
const onStatusChange = () => { const switchTab = (tab) => {
activeTab.value = tab;
loadSerials({ q: searcher.query }); loadSerials({ q: searcher.query });
}; };
@ -92,7 +93,7 @@ const createSerial = () => {
form.post(url, { form.post(url, {
onSuccess: (response) => { onSuccess: (response) => {
Notify.success('Número de serie creado exitosamente'); window.Notify.success('Número de serie creado exitosamente');
if (response.inventory) { if (response.inventory) {
inventory.value = response.inventory; inventory.value = response.inventory;
} }
@ -100,7 +101,7 @@ const createSerial = () => {
loadSerials(); loadSerials();
}, },
onError: () => { onError: () => {
Notify.error('Error al crear el número de serie'); window.Notify.error('Error al crear el número de serie');
} }
}); });
}; };
@ -122,12 +123,12 @@ const closeEditModal = () => {
const updateSerial = () => { const updateSerial = () => {
editForm.put(apiURL(`inventario/${inventoryId.value}/serials/${editingSerial.value.id}`), { editForm.put(apiURL(`inventario/${inventoryId.value}/serials/${editingSerial.value.id}`), {
onSuccess: () => { onSuccess: () => {
Notify.success('Número de serie actualizado'); window.Notify.success('Número de serie actualizado');
closeEditModal(); closeEditModal();
loadSerials(); loadSerials();
}, },
onError: () => { onError: () => {
Notify.error('Error al actualizar el número de serie'); window.Notify.error('Error al actualizar el número de serie');
} }
}); });
}; };
@ -146,11 +147,11 @@ const closeDeleteModal = () => {
const confirmDelete = async () => { const confirmDelete = async () => {
try { try {
await serialService.deleteSerial(inventoryId.value, deletingSerial.value.id); await serialService.deleteSerial(inventoryId.value, deletingSerial.value.id);
Notify.success('Número de serie eliminado'); window.Notify.success('Número de serie eliminado');
closeDeleteModal(); closeDeleteModal();
loadSerials(); loadSerials();
} catch (error) { } catch (error) {
Notify.error('Error al eliminar el número de serie'); window.Notify.error('Error al eliminar el número de serie');
} }
}; };
@ -215,23 +216,14 @@ onMounted(() => {
</div> </div>
</div> </div>
<!-- Buscador y filtros --> <!-- Buscador -->
<SearcherHead <SearcherHead
title="Números de Serie" title="Números de Serie"
placeholder="Buscar por número de serie..." placeholder="Buscar por número de serie..."
@search="onSearch" @search="onSearch"
> >
<!-- Filtro de estado -->
<select
v-model="statusFilter"
@change="onStatusChange"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
>
<option value="">Todos los estados</option>
<option value="disponible">Disponible</option>
<option value="vendido">Vendido</option>
</select>
<button <button
v-if="activeTab === 'disponible'"
class="flex items-center gap-2 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm" class="flex items-center gap-2 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
@click="openCreateModal" @click="openCreateModal"
> >
@ -240,12 +232,54 @@ onMounted(() => {
</button> </button>
</SearcherHead> </SearcherHead>
<!-- Pestañas -->
<div class="mt-4 border-b border-gray-200 dark:border-gray-700">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
<button
@click="switchTab('disponible')"
:class="[
activeTab === 'disponible'
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300',
'group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors'
]"
>
<GoogleIcon
name="check_circle"
:class="[
activeTab === 'disponible' ? 'text-indigo-500 dark:text-indigo-400' : 'text-gray-400 group-hover:text-gray-500',
'mr-2 text-xl'
]"
/>
Disponibles
</button>
<button
@click="switchTab('vendido')"
:class="[
activeTab === 'vendido'
? 'border-indigo-500 text-indigo-600 dark:text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300',
'group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm transition-colors'
]"
>
<GoogleIcon
name="shopping_cart"
:class="[
activeTab === 'vendido' ? 'text-indigo-500 dark:text-indigo-400' : 'text-gray-400 group-hover:text-gray-500',
'mr-2 text-xl'
]"
/>
Vendidos
</button>
</nav>
</div>
<!-- Tabla de seriales --> <!-- Tabla de seriales -->
<div class="pt-2 w-full"> <div class="pt-2 w-full">
<Table <Table
:items="serials" :items="serials"
:processing="searcher.processing" :processing="searcher.processing"
@send-pagination="(page) => searcher.pagination(page, { status: statusFilter || undefined })" @send-pagination="(page) => searcher.pagination(page, { status: activeTab })"
> >
<template #head> <template #head>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">NÚMERO DE SERIE</th> <th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">NÚMERO DE SERIE</th>
@ -316,14 +350,14 @@ onMounted(() => {
<td colspan="5" class="table-cell text-center"> <td colspan="5" class="table-cell text-center">
<div class="flex flex-col items-center justify-center py-8 text-gray-500"> <div class="flex flex-col items-center justify-center py-8 text-gray-500">
<GoogleIcon <GoogleIcon
name="qr_code_2" :name="activeTab === 'disponible' ? 'qr_code_2' : 'shopping_cart'"
class="text-6xl mb-2 opacity-50" class="text-6xl mb-2 opacity-50"
/> />
<p class="font-semibold"> <p class="font-semibold">
No hay números de serie registrados {{ activeTab === 'disponible' ? 'No hay seriales disponibles' : 'No hay seriales vendidos' }}
</p> </p>
<p class="text-sm mt-1"> <p class="text-sm mt-1">
Agrega seriales individuales o importa múltiples {{ activeTab === 'disponible' ? 'Agrega seriales para comenzar' : 'Los seriales vendidos aparecerán aquí' }}
</p> </p>
</div> </div>
</td> </td>