feat: agregar página de ventas por catálogo
- Agregar CatalogSalesManagement.vue con tabla de ventas - Filtros por texto (ID/cliente) y rango de fechas - Filas expandibles con detalle de productos y datos de envío - Agregar ruta protegida /admin/catalog-sales - Agregar endpoint getCatalogSales() en servicios API - Agregar menú 'Ver Ventas por Catálogo' en NavBar
This commit is contained in:
278
src/components/CatalogSalesManagement.vue
Normal file
278
src/components/CatalogSalesManagement.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<!-- Header -->
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="6">
|
||||
<h1 class="text-h4">Ventas por Catálogo</h1>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="text-md-right">
|
||||
<v-chip color="primary" variant="flat">
|
||||
{{ catalogSales.length }} venta(s)
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Filtros -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
label="Buscar por ID o cliente"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-text-field
|
||||
v-model="dateFrom"
|
||||
label="Fecha desde"
|
||||
type="date"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-text-field
|
||||
v-model="dateTo"
|
||||
label="Fecha hasta"
|
||||
type="date"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" class="d-flex align-center">
|
||||
<v-btn
|
||||
@click="clearFilters"
|
||||
variant="text"
|
||||
color="grey"
|
||||
size="small"
|
||||
prepend-icon="mdi-filter-remove"
|
||||
>
|
||||
Limpiar filtros
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Tabla -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-data-table
|
||||
v-model:expanded="expanded"
|
||||
:headers="headers"
|
||||
:items="filteredSales"
|
||||
:loading="loading"
|
||||
density="compact"
|
||||
item-value="id"
|
||||
items-per-page="25"
|
||||
:items-per-page-options="[10, 25, 50, 100]"
|
||||
show-expand
|
||||
>
|
||||
<!-- Fecha formateada -->
|
||||
<template #item.date="{ item }">
|
||||
{{ formatDate(item.date) }}
|
||||
</template>
|
||||
|
||||
<!-- Total formateado -->
|
||||
<template #item.total="{ item }">
|
||||
${{ Number(item.total).toLocaleString('es-CO') }}
|
||||
</template>
|
||||
|
||||
<!-- Cliente -->
|
||||
<template #item.customer="{ item }">
|
||||
<span v-if="item.customer_name">{{ item.customer_name }}</span>
|
||||
<v-chip v-else size="small" color="grey" variant="flat">
|
||||
ID: {{ item.customer }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<!-- Fila expandida: detalle de productos -->
|
||||
<template #expanded-row="{ columns, item }">
|
||||
<tr>
|
||||
<td :colspan="columns.length" class="pa-4 bg-grey-lighten-4">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<strong>Datos de envío</strong>
|
||||
<v-list density="compact">
|
||||
<v-list-item v-if="item.customer_name">
|
||||
<template #prepend><v-icon>mdi-account</v-icon></template>
|
||||
<v-list-item-title>{{ item.customer_name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="item.customer_address">
|
||||
<template #prepend><v-icon>mdi-map-marker</v-icon></template>
|
||||
<v-list-item-title>{{ item.customer_address }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="item.customer_phone">
|
||||
<template #prepend><v-icon>mdi-phone</v-icon></template>
|
||||
<v-list-item-title>{{ item.customer_phone }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="item.pickup_method">
|
||||
<template #prepend><v-icon>mdi-truck</v-icon></template>
|
||||
<v-list-item-title>
|
||||
{{ item.pickup_method === 'DELIVERY' ? 'Domicilio' : 'Recoge en tienda' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<strong>Productos</strong>
|
||||
<v-table density="compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Producto ID</th>
|
||||
<th class="text-right">Precio</th>
|
||||
<th class="text-right">Cantidad</th>
|
||||
<th class="text-right">Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="line in item.catalogsaleline_set" :key="line.id">
|
||||
<td>{{ line.product }}</td>
|
||||
<td class="text-right">${{ Number(line.unit_price).toLocaleString('es-CO') }}</td>
|
||||
<td class="text-right">{{ line.quantity }}</td>
|
||||
<td class="text-right">
|
||||
${{ (Number(line.unit_price) * Number(line.quantity)).toLocaleString('es-CO') }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Loading -->
|
||||
<template #loading>
|
||||
<v-skeleton-loader type="table-row@10"></v-skeleton-loader>
|
||||
</template>
|
||||
|
||||
<!-- No data -->
|
||||
<template #no-data>
|
||||
<v-alert type="info" variant="tonal" class="my-4">
|
||||
No hay ventas por catálogo para mostrar
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<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, computed, inject, onMounted } from 'vue';
|
||||
|
||||
const api = inject('api');
|
||||
const catalogSales = ref([]);
|
||||
const loading = ref(false);
|
||||
const expanded = ref([]);
|
||||
const searchQuery = ref('');
|
||||
const dateFrom = ref('');
|
||||
const dateTo = ref('');
|
||||
const snackbar = ref({ show: false, message: '', color: 'success' });
|
||||
|
||||
const headers = [
|
||||
{ title: 'ID', key: 'id', sortable: true },
|
||||
{ title: 'Fecha', key: 'date', sortable: true },
|
||||
{ title: 'Cliente', key: 'customer_name', sortable: true },
|
||||
{ title: 'Total', key: 'total', sortable: true },
|
||||
{ title: '', key: 'data-table-expand' },
|
||||
];
|
||||
|
||||
const filteredSales = computed(() => {
|
||||
let result = catalogSales.value;
|
||||
|
||||
// Filtro por texto (ID o nombre de cliente)
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase().trim();
|
||||
result = result.filter(sale => {
|
||||
const customerName = (sale.customer_name || '').toLowerCase();
|
||||
const customerId = String(sale.customer);
|
||||
const saleId = String(sale.id);
|
||||
return customerName.includes(query) || customerId.includes(query) || saleId.includes(query);
|
||||
});
|
||||
}
|
||||
|
||||
// Filtro por fecha desde
|
||||
if (dateFrom.value) {
|
||||
const from = new Date(dateFrom.value);
|
||||
result = result.filter(sale => new Date(sale.date) >= from);
|
||||
}
|
||||
|
||||
// Filtro por fecha hasta
|
||||
if (dateTo.value) {
|
||||
const to = new Date(dateTo.value + 'T23:59:59');
|
||||
result = result.filter(sale => new Date(sale.date) <= to);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
async function loadCatalogSales() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await api.getCatalogSales();
|
||||
catalogSales.value = data;
|
||||
} catch (error) {
|
||||
console.error('Error al cargar ventas por catálogo:', error);
|
||||
snackbar.value = { show: true, message: 'Error al cargar ventas por catálogo', color: 'error' };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('es-CO', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
searchQuery.value = '';
|
||||
dateFrom.value = '';
|
||||
dateTo.value = '';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCatalogSales();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-md-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.text-md-right {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -105,6 +105,7 @@
|
||||
{ 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: 'Ver Ventas por Catálogo', route: '/admin/catalog-sales', icon: 'mdi-cart-arrow-down'},
|
||||
{ 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'}
|
||||
|
||||
10
src/pages/admin/catalog-sales.vue
Normal file
10
src/pages/admin/catalog-sales.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<CatalogSalesManagement v-if="authStore.isAdmin"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import CatalogSalesManagement from '@/components/CatalogSalesManagement.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
@@ -20,6 +20,7 @@ const ADMIN_ROUTES = [
|
||||
'/compra_admin',
|
||||
'/cuadrar_tarro',
|
||||
'/admin/products',
|
||||
'/admin/catalog-sales',
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -67,6 +67,10 @@ class Api {
|
||||
return this.apiImplementation.sendSalesToTryton();
|
||||
}
|
||||
|
||||
getCatalogSales() {
|
||||
return this.apiImplementation.getCatalogSales();
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
return this.apiImplementation.getCurrentUser();
|
||||
}
|
||||
|
||||
@@ -110,6 +110,11 @@ class DjangoApi {
|
||||
return this.postRequest(url, {});
|
||||
}
|
||||
|
||||
getCatalogSales() {
|
||||
const url = this.base + "/don_confiao/api/catalog_sales/";
|
||||
return this.getRequest(url);
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
const url = this.base + "/api/users/me/";
|
||||
return this.getRequest(url);
|
||||
|
||||
Reference in New Issue
Block a user