feat: agregar página de gestión de productos activos/inactivos
- Agregar ProductsManagement.vue con tabla de productos - Filtros: Activos, Inactivos, Todos - Búsqueda por nombre en tiempo real - Acciones por lote: activar/desactivar múltiples productos - Botones condicionales según filtro activo - Agregar ruta protegida /admin/products - Actualizar API con métodos getProducts(active) y updateProduct() - Agregar método patchRequest en django-api - Agregar menú 'Gestión de Productos' en NavBar
This commit is contained in:
@@ -104,6 +104,7 @@
|
||||
{ title: 'Cuadres de tarro', route: '/cuadres_de_tarro', icon: 'mdi-chart-bar'},
|
||||
{ title: 'CSV Tryton', route: '/ventas_para_tryton', icon: 'mdi-file-table'},
|
||||
{ title: 'Compra adm', route: '/compra_admin', icon: 'mdi-cart'},
|
||||
{ title: 'Gestión de Productos', route: '/admin/products', icon: 'mdi-package-variant'},
|
||||
{ title: 'Actualizar Productos De Tryton', route: '/sincronizar_productos_tryton', icon: 'trytonIcon'},
|
||||
{ title: 'Actualizar Clientes De Tryton', route: '/sincronizar_clientes_tryton', icon: 'trytonIcon'},
|
||||
{ title: 'Actualizar Ventas Tryton', route: '/sincronizar_ventas_tryton', icon: 'trytonIcon'}
|
||||
|
||||
262
src/components/ProductsManagement.vue
Normal file
262
src/components/ProductsManagement.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<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("all");
|
||||
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>
|
||||
10
src/pages/admin/products.vue
Normal file
10
src/pages/admin/products.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<ProductsManagement v-if="authStore.isAdmin"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import ProductsManagement from '@/components/ProductsManagement.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
@@ -19,6 +19,7 @@ const ADMIN_ROUTES = [
|
||||
'/cuadres_de_tarro',
|
||||
'/compra_admin',
|
||||
'/cuadrar_tarro',
|
||||
'/admin/products',
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -7,8 +7,12 @@ class Api {
|
||||
return this.apiImplementation.getCustomers();
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
return this.apiImplementation.getProducts();
|
||||
getProducts(active = 'all') {
|
||||
return this.apiImplementation.getProducts(active);
|
||||
}
|
||||
|
||||
updateProduct(productId, data) {
|
||||
return this.apiImplementation.updateProduct(productId, data);
|
||||
}
|
||||
|
||||
getPaymentMethods() {
|
||||
|
||||
@@ -14,16 +14,31 @@ class DjangoApi {
|
||||
return http.post(url, payload).then((r) => r.data);
|
||||
}
|
||||
|
||||
patchRequest(url, payload) {
|
||||
return http.patch(url, payload).then((r) => r.data);
|
||||
}
|
||||
|
||||
getCustomers() {
|
||||
const url = this.base + "/don_confiao/api/customers/";
|
||||
return this.getRequest(url);
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
const url = this.base + "/don_confiao/api/products/";
|
||||
getProducts(active = 'all') {
|
||||
let url = this.base + "/don_confiao/api/products/";
|
||||
|
||||
// Agregar query parameter según filtro
|
||||
if (active !== 'all') {
|
||||
url += `?active=${active}`;
|
||||
}
|
||||
|
||||
return this.getRequest(url);
|
||||
}
|
||||
|
||||
updateProduct(productId, data) {
|
||||
const url = this.base + `/don_confiao/api/products/${productId}/`;
|
||||
return this.patchRequest(url, data);
|
||||
}
|
||||
|
||||
getPaymentMethods() {
|
||||
const url =
|
||||
this.base + "/don_confiao/payment_methods/all/select_format";
|
||||
|
||||
Reference in New Issue
Block a user