- Default filter cambia de 'all' a 'false' (Inactivos) - Limpieza de whitespace
263 lines
7.1 KiB
Vue
263 lines
7.1 KiB
Vue
<template>
|
|
<v-container fluid>
|
|
<!-- Header + Filtros -->
|
|
<v-row align="center">
|
|
<v-col cols="12" md="6">
|
|
<h1 class="text-h4">Gestión de Productos</h1>
|
|
</v-col>
|
|
<v-col cols="12" md="6" class="text-md-right">
|
|
<!-- Chips de filtro -->
|
|
<v-chip-group v-model="activeFilter" mandatory color="primary">
|
|
<v-chip value="false">Inactivos</v-chip>
|
|
<v-chip value="true">Activos</v-chip>
|
|
<v-chip value="all">Todos</v-chip>
|
|
</v-chip-group>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Barra de búsqueda -->
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-text-field
|
|
v-model="searchQuery"
|
|
label="Buscar por nombre"
|
|
prepend-inner-icon="mdi-magnify"
|
|
clearable
|
|
density="compact"
|
|
variant="outlined"
|
|
hide-details
|
|
></v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Barra de acciones por lote -->
|
|
<v-row v-if="selected.length > 0">
|
|
<v-col cols="12">
|
|
<v-alert type="info" variant="tonal" border="start">
|
|
<v-row align="center">
|
|
<v-col cols="12" md="6">
|
|
<strong>{{ selected.length }} producto(s) seleccionado(s)</strong>
|
|
</v-col>
|
|
<v-col cols="12" md="6" class="text-md-right">
|
|
<!-- Botón Activar: solo visible en filtro "Inactivos" -->
|
|
<v-btn
|
|
v-if="activeFilter === 'false'"
|
|
@click="activateSelected"
|
|
color="success"
|
|
variant="elevated"
|
|
prepend-icon="mdi-check-circle"
|
|
:disabled="loading"
|
|
>
|
|
Activar seleccionados
|
|
</v-btn>
|
|
|
|
<!-- Botón Desactivar: solo visible en filtro "Activos" -->
|
|
<v-btn
|
|
v-if="activeFilter === 'true'"
|
|
@click="deactivateSelected"
|
|
color="error"
|
|
variant="elevated"
|
|
prepend-icon="mdi-close-circle"
|
|
:disabled="loading"
|
|
>
|
|
Desactivar seleccionados
|
|
</v-btn>
|
|
|
|
<!-- Ambos botones visibles en filtro "Todos" -->
|
|
<template v-if="activeFilter === 'all'">
|
|
<v-btn
|
|
@click="activateSelected"
|
|
color="success"
|
|
variant="elevated"
|
|
prepend-icon="mdi-check-circle"
|
|
class="mr-2"
|
|
:disabled="loading"
|
|
>
|
|
Activar seleccionados
|
|
</v-btn>
|
|
<v-btn
|
|
@click="deactivateSelected"
|
|
color="error"
|
|
variant="elevated"
|
|
prepend-icon="mdi-close-circle"
|
|
:disabled="loading"
|
|
>
|
|
Desactivar seleccionados
|
|
</v-btn>
|
|
</template>
|
|
</v-col>
|
|
</v-row>
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Tabla de productos -->
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<v-card>
|
|
<v-data-table
|
|
v-model="selected"
|
|
:headers="headers"
|
|
:items="filteredProducts"
|
|
:loading="loading"
|
|
show-select
|
|
density="compact"
|
|
item-value="id"
|
|
items-per-page="25"
|
|
:items-per-page-options="[10, 25, 50, 100]"
|
|
>
|
|
<!-- Slot para columna de precio (formato) -->
|
|
<template #item.price="{ item }">
|
|
${{ Number(item.price).toLocaleString("es-CO") }}
|
|
</template>
|
|
|
|
<!-- Slot para columna de estado -->
|
|
<template #item.active="{ item }">
|
|
<v-chip
|
|
:color="item.active ? 'success' : 'error'"
|
|
size="small"
|
|
variant="flat"
|
|
>
|
|
{{ item.active ? "Activo" : "Inactivo" }}
|
|
</v-chip>
|
|
</template>
|
|
|
|
<!-- Loading state -->
|
|
<template #loading>
|
|
<v-skeleton-loader type="table-row@10"></v-skeleton-loader>
|
|
</template>
|
|
|
|
<!-- No data state -->
|
|
<template #no-data>
|
|
<v-alert type="info" variant="tonal" class="my-4">
|
|
No hay productos para mostrar
|
|
</v-alert>
|
|
</template>
|
|
</v-data-table>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Snackbar de feedback -->
|
|
<v-snackbar
|
|
v-model="snackbar.show"
|
|
:color="snackbar.color"
|
|
:timeout="3000"
|
|
location="top"
|
|
>
|
|
{{ snackbar.message }}
|
|
<template #actions>
|
|
<v-btn variant="text" @click="snackbar.show = false"> Cerrar </v-btn>
|
|
</template>
|
|
</v-snackbar>
|
|
</v-container>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, inject, onMounted, computed } from "vue";
|
|
|
|
// Estado
|
|
const api = inject("api");
|
|
const activeFilter = ref("false");
|
|
const products = ref([]);
|
|
const selected = ref([]);
|
|
const loading = ref(false);
|
|
const snackbar = ref({ show: false, message: "", color: "success" });
|
|
const searchQuery = ref("");
|
|
|
|
// Headers de la tabla
|
|
const headers = [
|
|
{ title: "ID", key: "id", sortable: true },
|
|
{ title: "Nombre", key: "name", sortable: true },
|
|
{ title: "Precio", key: "price", sortable: true },
|
|
{ title: "Estado", key: "active", sortable: true },
|
|
];
|
|
|
|
// Computed - Productos filtrados por búsqueda
|
|
const filteredProducts = computed(() => {
|
|
if (!searchQuery.value) {
|
|
return products.value;
|
|
}
|
|
|
|
const query = searchQuery.value.toLowerCase().trim();
|
|
return products.value.filter((product) =>
|
|
product.name.toLowerCase().includes(query),
|
|
);
|
|
});
|
|
|
|
// Métodos
|
|
async function loadProducts() {
|
|
loading.value = true;
|
|
try {
|
|
const data = await api.getProducts(activeFilter.value);
|
|
products.value = data;
|
|
} catch (error) {
|
|
console.error("Error al cargar productos:", error);
|
|
showSnackbar("Error al cargar productos", "error");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function activateSelected() {
|
|
await updateSelectedStatus(true);
|
|
}
|
|
|
|
async function deactivateSelected() {
|
|
await updateSelectedStatus(false);
|
|
}
|
|
|
|
async function updateSelectedStatus(active) {
|
|
loading.value = true;
|
|
try {
|
|
// Actualizar productos en paralelo
|
|
await Promise.all(
|
|
selected.value.map((id) => api.updateProduct(id, { active })),
|
|
);
|
|
|
|
const action = active ? "activado(s)" : "desactivado(s)";
|
|
showSnackbar(
|
|
`${selected.value.length} producto(s) ${action} exitosamente`,
|
|
"success",
|
|
);
|
|
|
|
// Limpiar selección y recargar
|
|
selected.value = [];
|
|
await loadProducts();
|
|
} catch (error) {
|
|
console.error("Error al actualizar productos:", error);
|
|
showSnackbar("Error al actualizar productos", "error");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function showSnackbar(message, color) {
|
|
snackbar.value = { show: true, message, color };
|
|
}
|
|
|
|
// Watchers
|
|
watch(activeFilter, () => {
|
|
selected.value = [];
|
|
searchQuery.value = "";
|
|
loadProducts();
|
|
});
|
|
|
|
// Inicialización
|
|
onMounted(() => {
|
|
loadProducts();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.text-md-right {
|
|
text-align: right;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.text-md-right {
|
|
text-align: left;
|
|
}
|
|
}
|
|
</style>
|