Compare commits
14 Commits
d5e30c92b0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb26f55829 | |||
|
|
5397ab2255 | ||
|
|
a2ab4fceb7 | ||
| 78dfea8714 | |||
| 490cb7b53d | |||
| 5e86595831 | |||
| 897cbb3efc | |||
| 925fadba2d | |||
| d049357231 | |||
| a36fbd289e | |||
| 368b7007f6 | |||
| f79197baf5 | |||
| 3f0f1fe09a | |||
| ed42eb324c |
BIN
src/assets/not_image_for_product.jpeg
Normal file
BIN
src/assets/not_image_for_product.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
@@ -1,341 +0,0 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-form ref="purchase" v-model="valid" @change="onFormChange">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-autocomplete
|
||||
v-model="purchase.customer"
|
||||
:items="filteredClients"
|
||||
:search="client_search"
|
||||
no-data-text="No se hallaron clientes"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
@update:model-value="onFormChange"
|
||||
label="Cliente"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
class="mr-4"
|
||||
></v-autocomplete>
|
||||
<!-- <v-btn color="primary" @click="openModal">Agregar Cliente</v-btn> -->
|
||||
<CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer"/>
|
||||
</v-col>
|
||||
<v-col lg="4">
|
||||
<v-text-field
|
||||
v-model="purchase.date"
|
||||
label="Fecha"
|
||||
type="datetime-local"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-textarea
|
||||
v-model="purchase.notes"
|
||||
label="Notas"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
<v-divider></v-divider>
|
||||
<v-container>
|
||||
<v-toolbar>
|
||||
<v-toolbar-title secondary>Productos</v-toolbar-title>
|
||||
p </v-toolbar>
|
||||
<v-container v-for="(line, index) in purchase.saleline_set" :key="line.id">
|
||||
<v-row>
|
||||
<v-col
|
||||
lg="9">
|
||||
<v-autocomplete
|
||||
v-model="line.product"
|
||||
:items="filteredProducts"
|
||||
:search="product_search"
|
||||
@update:modelValue="onProductChange(index)"
|
||||
no-data-text="No se hallaron productos"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
item-subtitle="Price"
|
||||
label="Producto"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props" :title="item.raw.name" :subtitle="formatPrice(item.raw.price)"></v-list-item>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col
|
||||
lg="2"
|
||||
>
|
||||
<v-text-field
|
||||
v-model.number="line.quantity"
|
||||
label="Cantidad"
|
||||
type="number"
|
||||
:rules="[rules.required,rules.positive]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model.number="line.unit_price"
|
||||
label="Precio"
|
||||
type="number"
|
||||
:rules="[rules.required]"
|
||||
prefix="$"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="line.measuring_unit"
|
||||
label="UdM"
|
||||
persistent-placeholder="true"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
type="number"
|
||||
:value="calculateSubtotal(line)"
|
||||
label="Subtotal"
|
||||
prefix="$"
|
||||
readonly
|
||||
disable
|
||||
persistent-placeholder="true"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn @click="removeLine(index)" color="red">Eliminar</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-alert type="warning" :duration="2000" closable v-model="show_alert_lines">
|
||||
No se puede eliminar la única línea.
|
||||
</v-alert>
|
||||
</v-container>
|
||||
<v-btn @click="addLine" color="blue">Agregar</v-btn>
|
||||
</v-container>
|
||||
<v-divider></v-divider>
|
||||
<v-text-field
|
||||
:value="calculateTotal"
|
||||
label="Total"
|
||||
prefix="$"
|
||||
readonly
|
||||
persistent-placeholder="true"
|
||||
></v-text-field>
|
||||
<v-container v-if="calculateTotal > 0">
|
||||
<v-select
|
||||
:items="payment_methods"
|
||||
v-model="purchase.payment_method"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
label="Pago en"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
<v-btn @click="openCasherModal" v-if="purchase.payment_method === 'CASH'">Calcular Devuelta</v-btn>
|
||||
<CasherModal :total_purchase="calculateTotal" ref="casherModal"</CasherModal>
|
||||
</v-container>
|
||||
<v-btn @click="submit" color="green">Comprar</v-btn>
|
||||
<v-alert type="error" :duration="2000" closable v-model="show_alert_purchase">
|
||||
Verifique los campos obligatorios.
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomerForm from './CreateCustomerModal.vue';
|
||||
import CasherModal from './CasherModal.vue';
|
||||
import { inject } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'DonConfiao',
|
||||
components: {
|
||||
CustomerForm,
|
||||
CasherModal,
|
||||
},
|
||||
props: {
|
||||
msg: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
api: inject('api'),
|
||||
valid: false,
|
||||
form_changed: false,
|
||||
show_alert_lines: false,
|
||||
show_alert_purchase: false,
|
||||
client_search: '',
|
||||
product_search: '',
|
||||
payment_methods: null,
|
||||
purchase: {
|
||||
date: this.getCurrentDate(),
|
||||
customer: null,
|
||||
notes: '',
|
||||
payment_method: null,
|
||||
saleline_set: [{product:'', unit_price: 0, quantity: 0, unit: ''}],
|
||||
},
|
||||
rules: {
|
||||
required: value => !!value || 'Requerido.',
|
||||
positive: value => value > 0 || 'La cantidad debe ser mayor que 0.',
|
||||
},
|
||||
menuItems: [
|
||||
{ title: 'Inicio', route: '/'},
|
||||
{ title: 'Compras', route:'/compras'},
|
||||
],
|
||||
clients: [],
|
||||
products: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchClients();
|
||||
this.fetchProducts();
|
||||
this.fetchPaymentMethods();
|
||||
},
|
||||
watch: {
|
||||
group () {
|
||||
this.drawer = false
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
window.addEventListener('beforeunload', this.confirmLeave);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('beforeunload', this.confirmLeave);
|
||||
},
|
||||
computed: {
|
||||
calculateTotal() {
|
||||
return this.purchase.saleline_set.reduce((total, saleline) => {
|
||||
return total + this.calculateSubtotal(saleline);
|
||||
}, 0);
|
||||
},
|
||||
filteredClients() {
|
||||
return this.clients.filter(client => {
|
||||
if (this.client_search === '') {
|
||||
return [];
|
||||
} else {
|
||||
return client.name.toLowerCase().includes(this.client_search.toLowerCase());
|
||||
}
|
||||
});
|
||||
},
|
||||
filteredProducts() {
|
||||
return this.products.filter(product => {
|
||||
if (this.product_search === '') {
|
||||
return [];
|
||||
} else {
|
||||
return product.name.toLowerCase().includes(this.product_search.toLowerCase());
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.$refs.customerModal.openModal();
|
||||
},
|
||||
onFormChange() {
|
||||
this.form_changed = true;
|
||||
},
|
||||
openCasherModal() {
|
||||
this.$refs.casherModal.dialog = true
|
||||
},
|
||||
confirmLeave(event) {
|
||||
if (this.form_changed) {
|
||||
const message = '¿seguro que quieres salir? Perderas la información diligenciada';
|
||||
event.preventDefault();
|
||||
event.returnValue = message;
|
||||
return message;
|
||||
}
|
||||
},
|
||||
getCurrentDate() {
|
||||
const today = new Date();
|
||||
const gmtOffSet = -5;
|
||||
const localDate = new Date(today.getTime() + (gmtOffSet * 60 * 60 * 1000));
|
||||
// Formatear la fecha y hora en el formato YYYY-MM-DDTHH:MM
|
||||
const formattedDate = localDate.toISOString().slice(0,16);
|
||||
return formattedDate;
|
||||
},
|
||||
onProductChange(index) {
|
||||
const selectedProductId = this.purchase.saleline_set[index].product;
|
||||
const selectedProduct = this.products.find(p => p.id == selectedProductId);
|
||||
this.purchase.saleline_set[index].unit_price = selectedProduct.price;
|
||||
this.purchase.saleline_set[index].measuring_unit = selectedProduct.measuring_unit;
|
||||
},
|
||||
fetchClients() {
|
||||
this.api.getCustomers()
|
||||
.then(data => {
|
||||
this.clients = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
handleNewCustomer(newCustomer){
|
||||
this.clients.push(newCustomer);
|
||||
this.purchase.customer = newCustomer.id;
|
||||
},
|
||||
fetchProducts() {
|
||||
this.api.getProducts()
|
||||
.then(data => {
|
||||
this.products = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
fetchPaymentMethods() {
|
||||
this.api.getPaymentMethods()
|
||||
.then(data => {
|
||||
this.payment_methods = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
addLine() {
|
||||
this.purchase.saleline_set.push({ product: '', unit_price: 0, quantity:0, measuring_unit: ''});
|
||||
},
|
||||
removeLine(index) {
|
||||
if (this.purchase.saleline_set.length > 1) {
|
||||
this.purchase.saleline_set.splice(index, 1);
|
||||
} else {
|
||||
this.show_alert_lines = true;
|
||||
setTimeout(() => {
|
||||
this.show_alert_lines = false;
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
calculateSubtotal(line) {
|
||||
return line.unit_price * line.quantity;
|
||||
},
|
||||
async submit() {
|
||||
this.$refs.purchase.validate();
|
||||
if (this.valid) {
|
||||
this.api.createPurchase(this.purchase)
|
||||
.then(data => {
|
||||
console.log('Compra enviada:', data);
|
||||
this.$router.push({
|
||||
path: "/summary_purchase",
|
||||
query : {id: parseInt(data.id)}
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error al enviarl la compra:', error));
|
||||
} else {
|
||||
this.show_alert_purchase = true;
|
||||
setTimeout(() => {
|
||||
this.show_alert_purchase = false;
|
||||
}, 4000);
|
||||
}
|
||||
},
|
||||
navigate(route) {
|
||||
this.$router.push(route);
|
||||
},
|
||||
formatPrice(price) {
|
||||
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'COP' }).format(price);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchClients();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,170 +1,379 @@
|
||||
<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>
|
||||
<!-- Header Principal -->
|
||||
<v-row align="center" class="mb-4">
|
||||
<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>
|
||||
<h1 class="text-h4">Ventas por Catálogo</h1>
|
||||
<div class="text-caption text-grey mt-1">
|
||||
{{ totalPending }} sin sincronizar • {{ totalSynced }} sincronizadas
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Tabs -->
|
||||
<v-tabs v-model="activeTab" class="mb-4" color="primary">
|
||||
<v-tab value="pending">
|
||||
Sin Sincronizar
|
||||
<v-chip class="ml-2" size="small" color="orange" variant="flat">{{ totalPending }}</v-chip>
|
||||
</v-tab>
|
||||
<v-tab value="synced">
|
||||
Sincronizadas
|
||||
<v-chip class="ml-2" size="small" color="success" variant="flat">{{ totalSynced }}</v-chip>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- Window para contenido de tabs -->
|
||||
<v-window v-model="activeTab">
|
||||
<!-- Tab: Sin Sincronizar -->
|
||||
<v-window-item value="pending">
|
||||
<!-- Botón Sincronizar -->
|
||||
<v-row align="center" class="mb-4">
|
||||
<v-col>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-sync"
|
||||
@click="$router.push('/sincronizar_catalog_sales_tryton')"
|
||||
:disabled="totalPending === 0"
|
||||
>
|
||||
Sincronizar a Tryton
|
||||
</v-btn>
|
||||
</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 de ventas sin sincronizar -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-data-table
|
||||
v-model:expanded="expandedPending"
|
||||
:headers="pendingHeaders"
|
||||
:items="filteredPendingSales"
|
||||
: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>
|
||||
|
||||
<!-- Estado -->
|
||||
<template #item.status="{ item }">
|
||||
<v-chip size="small" color="orange" variant="flat">
|
||||
<v-icon start size="small">mdi-clock-outline</v-icon>
|
||||
Pendiente
|
||||
</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 pendientes de sincronizar
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
|
||||
<!-- Tab: Sincronizadas -->
|
||||
<v-window-item value="synced">
|
||||
<!-- Filtros -->
|
||||
<v-row class="mt-4">
|
||||
<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 de ventas sincronizadas -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-data-table
|
||||
v-model:expanded="expandedSynced"
|
||||
:headers="syncedHeaders"
|
||||
:items="filteredSyncedSales"
|
||||
: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>
|
||||
|
||||
<!-- Estado -->
|
||||
<template #item.status="{ item }">
|
||||
<v-chip size="small" color="success" variant="flat">
|
||||
<v-icon start size="small">mdi-check-circle</v-icon>
|
||||
Sincronizada
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<!-- ID Tryton -->
|
||||
<template #item.external_id="{ item }">
|
||||
<v-chip size="small" variant="outlined" color="primary">
|
||||
{{ item.external_id }}
|
||||
</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 sincronizadas
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
@@ -186,22 +395,52 @@ import { ref, computed, inject, onMounted } from 'vue';
|
||||
const api = inject('api');
|
||||
const catalogSales = ref([]);
|
||||
const loading = ref(false);
|
||||
const expanded = ref([]);
|
||||
const expandedPending = ref([]);
|
||||
const expandedSynced = ref([]);
|
||||
const searchQuery = ref('');
|
||||
const dateFrom = ref('');
|
||||
const dateTo = ref('');
|
||||
const snackbar = ref({ show: false, message: '', color: 'success' });
|
||||
const activeTab = ref('pending'); // Tab activo por defecto
|
||||
|
||||
const headers = [
|
||||
// Headers para tabla de ventas sin sincronizar
|
||||
const pendingHeaders = [
|
||||
{ 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: 'Estado', key: 'status', sortable: false },
|
||||
{ title: '', key: 'data-table-expand' },
|
||||
];
|
||||
|
||||
const filteredSales = computed(() => {
|
||||
let result = catalogSales.value;
|
||||
// Headers para tabla de ventas sincronizadas
|
||||
const syncedHeaders = [
|
||||
{ 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: 'Estado', key: 'status', sortable: false },
|
||||
{ title: 'ID Tryton', key: 'external_id', sortable: true },
|
||||
{ title: '', key: 'data-table-expand' },
|
||||
];
|
||||
|
||||
// Ventas sin sincronizar (sin external_id)
|
||||
const pendingSales = computed(() => {
|
||||
return catalogSales.value.filter(sale => !sale.external_id);
|
||||
});
|
||||
|
||||
// Ventas sincronizadas (con external_id)
|
||||
const syncedSales = computed(() => {
|
||||
return catalogSales.value.filter(sale => sale.external_id);
|
||||
});
|
||||
|
||||
// Contadores
|
||||
const totalPending = computed(() => pendingSales.value.length);
|
||||
const totalSynced = computed(() => syncedSales.value.length);
|
||||
|
||||
// Función común para aplicar filtros
|
||||
function applyFilters(sales) {
|
||||
let result = sales;
|
||||
|
||||
// Filtro por texto (ID o nombre de cliente)
|
||||
if (searchQuery.value) {
|
||||
@@ -227,6 +466,16 @@ const filteredSales = computed(() => {
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ventas pendientes filtradas
|
||||
const filteredPendingSales = computed(() => {
|
||||
return applyFilters(pendingSales.value);
|
||||
});
|
||||
|
||||
// Ventas sincronizadas filtradas
|
||||
const filteredSyncedSales = computed(() => {
|
||||
return applyFilters(syncedSales.value);
|
||||
});
|
||||
|
||||
async function loadCatalogSales() {
|
||||
|
||||
383
src/components/CatalogueImagesManagement.vue
Normal file
383
src/components/CatalogueImagesManagement.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="6">
|
||||
<h1 class="text-h4">Imágenes de Catálogo</h1>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="text-md-right">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
Agregar Imagen
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="images"
|
||||
:loading="loading"
|
||||
density="compact"
|
||||
item-value="id"
|
||||
items-per-page="25"
|
||||
:items-per-page-options="[10, 25, 50, 100]"
|
||||
>
|
||||
<template #item.image="{ item }">
|
||||
<v-img
|
||||
:src="item.image"
|
||||
max-width="60"
|
||||
aspect-ratio="1"
|
||||
cover
|
||||
class="rounded"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item.product="{ item }">
|
||||
{{ productMap[item.product] || `ID: ${item.product}` }}
|
||||
</template>
|
||||
|
||||
<template #item.uploaded_at="{ item }">
|
||||
{{ formatDate(item.uploaded_at) }}
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="openEditDialog(item)"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="openDeleteDialog(item)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template #loading>
|
||||
<v-skeleton-loader type="table-row@10" />
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<v-alert type="info" variant="tonal" class="my-4">
|
||||
No hay imágenes de catálogo
|
||||
</v-alert>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-dialog v-model="dialog.show" max-width="500" persistent>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ dialog.isEdit ? 'Editar' : 'Agregar' }} Imagen de Catálogo
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="formRef">
|
||||
<v-select
|
||||
v-model="form.product"
|
||||
:items="productOptions"
|
||||
label="Producto"
|
||||
:rules="[(v) => !!v || 'Seleccione un producto']"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
variant="outlined"
|
||||
:disabled="dialog.isEdit"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<v-file-input
|
||||
ref="fileInputRef"
|
||||
label="Imagen"
|
||||
accept="image/*"
|
||||
:rules="[(v) => !!v || 'Seleccione una imagen']"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-camera"
|
||||
class="mb-3"
|
||||
@update:model-value="onFileSelected"
|
||||
/>
|
||||
|
||||
<v-img
|
||||
v-if="form.preview"
|
||||
:src="form.preview"
|
||||
max-height="200"
|
||||
contain
|
||||
class="mb-3 rounded"
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeDialog">Cancelar</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
:loading="submitting"
|
||||
:disabled="submitting"
|
||||
@click="submitForm"
|
||||
>
|
||||
{{ dialog.isEdit ? 'Actualizar' : 'Agregar' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="deleteDialog.show" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>Confirmar Eliminación</v-card-title>
|
||||
<v-card-text>
|
||||
¿Está seguro de eliminar esta imagen de catálogo?
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="deleteDialog.show = false">
|
||||
Cancelar
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="elevated"
|
||||
:loading="deleting"
|
||||
:disabled="deleting"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
Eliminar
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<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, inject, onMounted, computed, watch } from "vue";
|
||||
|
||||
const api = inject("api");
|
||||
|
||||
const images = ref([]);
|
||||
const products = ref([]);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const deleting = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
const snackbar = ref({ show: false, message: "", color: "success" });
|
||||
|
||||
const headers = [
|
||||
{ title: "ID", key: "id", sortable: true },
|
||||
{ title: "Producto", key: "product", sortable: true },
|
||||
{ title: "Imagen", key: "image", sortable: false },
|
||||
{ title: "Subida el", key: "uploaded_at", sortable: true },
|
||||
{ title: "Acciones", key: "actions", sortable: false },
|
||||
];
|
||||
|
||||
const dialog = ref({
|
||||
show: false,
|
||||
isEdit: false,
|
||||
editingItem: null,
|
||||
});
|
||||
|
||||
const fileInputRef = ref(null);
|
||||
const selectedFile = ref(null);
|
||||
|
||||
const form = ref({
|
||||
product: null,
|
||||
preview: null,
|
||||
});
|
||||
|
||||
const deleteDialog = ref({
|
||||
show: false,
|
||||
item: null,
|
||||
});
|
||||
|
||||
let previewObjectUrl = null;
|
||||
|
||||
const productMap = computed(() => {
|
||||
const map = {};
|
||||
for (const p of products.value) {
|
||||
map[p.id] = p.name;
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
const productOptions = computed(() => {
|
||||
return products.value.map((p) => ({
|
||||
name: p.name,
|
||||
id: p.id,
|
||||
}));
|
||||
});
|
||||
|
||||
watch(
|
||||
selectedFile,
|
||||
(file) => {
|
||||
if (previewObjectUrl) {
|
||||
URL.revokeObjectURL(previewObjectUrl);
|
||||
previewObjectUrl = null;
|
||||
}
|
||||
if (file) {
|
||||
previewObjectUrl = URL.createObjectURL(file);
|
||||
form.value.preview = previewObjectUrl;
|
||||
} else if (dialog.value.isEdit && dialog.value.editingItem) {
|
||||
form.value.preview = dialog.value.editingItem.image;
|
||||
} else {
|
||||
form.value.preview = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function onFileSelected(file) {
|
||||
selectedFile.value = file;
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return "-";
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleString("es-CO");
|
||||
}
|
||||
|
||||
async function loadImages() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await api.getCatalogueImages();
|
||||
images.value = data;
|
||||
} catch (error) {
|
||||
console.error("Error al cargar imágenes:", error);
|
||||
showSnackbar("Error al cargar imágenes", "error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProducts() {
|
||||
try {
|
||||
const data = await api.getProducts("all");
|
||||
products.value = data;
|
||||
} catch (error) {
|
||||
console.error("Error al cargar productos:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
dialog.value = { show: true, isEdit: false, editingItem: null };
|
||||
selectedFile.value = null;
|
||||
form.value = { product: null, preview: null };
|
||||
}
|
||||
|
||||
function openEditDialog(item) {
|
||||
dialog.value = { show: true, isEdit: true, editingItem: item };
|
||||
selectedFile.value = null;
|
||||
form.value = {
|
||||
product: item.product,
|
||||
preview: item.image,
|
||||
};
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
if (previewObjectUrl) {
|
||||
URL.revokeObjectURL(previewObjectUrl);
|
||||
previewObjectUrl = null;
|
||||
}
|
||||
selectedFile.value = null;
|
||||
dialog.value.show = false;
|
||||
dialog.value.isEdit = false;
|
||||
dialog.value.editingItem = null;
|
||||
form.value = { product: null, preview: null };
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
const formComponent = formRef.value;
|
||||
if (formComponent) {
|
||||
const { valid } = await formComponent.validate();
|
||||
if (!valid) return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append("product", form.value.product);
|
||||
if (selectedFile.value) {
|
||||
fd.append("image", selectedFile.value);
|
||||
}
|
||||
|
||||
if (dialog.value.isEdit) {
|
||||
await api.updateCatalogueImage(dialog.value.editingItem.id, fd);
|
||||
showSnackbar("Imagen actualizada exitosamente", "success");
|
||||
} else {
|
||||
await api.createCatalogueImage(fd);
|
||||
showSnackbar("Imagen agregada exitosamente", "success");
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
await loadImages();
|
||||
} catch (error) {
|
||||
console.error("Error al guardar imagen:", error);
|
||||
showSnackbar("Error al guardar imagen", "error");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteDialog(item) {
|
||||
deleteDialog.value = { show: true, item };
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
deleting.value = true;
|
||||
try {
|
||||
await api.deleteCatalogueImage(deleteDialog.value.item.id);
|
||||
showSnackbar("Imagen eliminada exitosamente", "success");
|
||||
deleteDialog.value.show = false;
|
||||
await loadImages();
|
||||
} catch (error) {
|
||||
console.error("Error al eliminar imagen:", error);
|
||||
showSnackbar("Error al eliminar imagen", "error");
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showSnackbar(message, color) {
|
||||
snackbar.value = { show: true, message, color };
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadImages();
|
||||
loadProducts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-md-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.text-md-right {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,70 +1,272 @@
|
||||
<template>
|
||||
<h1>Login</h1>
|
||||
<v-container fluid class="pa-0">
|
||||
<v-sheet class="hero-section d-flex align-center justify-center">
|
||||
<div class="glow-bubble bubble-blue"></div>
|
||||
<div class="glow-bubble bubble-green"></div>
|
||||
<div class="glow-bubble bubble-yellow"></div>
|
||||
<div class="glow-bubble bubble-red"></div>
|
||||
|
||||
<v-form ref="loginForm" @submit.prevent="onSubmit">
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
label="Usuario"
|
||||
:rules="[requiredRule]"
|
||||
required
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Contraseña"
|
||||
type="password"
|
||||
:rules="[requiredRule]"
|
||||
required
|
||||
/>
|
||||
<div class="login-card">
|
||||
<v-img
|
||||
:src="logo"
|
||||
alt="Don Confiao"
|
||||
max-width="140"
|
||||
class="mx-auto mb-4"
|
||||
/>
|
||||
<h1 class="text-h5 text-sm-h4 font-weight-bold text-center mb-1">
|
||||
Iniciar Sesión
|
||||
</h1>
|
||||
<p class="text-body-2 text-medium-emphasis text-center mb-6">
|
||||
Ingresa tus credenciales para acceder
|
||||
</p>
|
||||
|
||||
<v-btn type="submit" color="primary">Entrar</v-btn>
|
||||
<v-form ref="loginForm" @submit.prevent="onSubmit">
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
label="Usuario"
|
||||
prepend-inner-icon="mdi-account"
|
||||
:rules="[requiredRule]"
|
||||
variant="outlined"
|
||||
required
|
||||
class="mb-2"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
label="Contraseña"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
type="password"
|
||||
:rules="[requiredRule]"
|
||||
variant="outlined"
|
||||
required
|
||||
class="mb-4"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<v-alert v-if="error" type="error" class="mt-2">{{ error }}</v-alert>
|
||||
</v-form>
|
||||
<v-btn
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="isSubmitting"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Entrar
|
||||
</v-btn>
|
||||
|
||||
<v-alert
|
||||
v-if="error"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
closable
|
||||
@click:close="error = ''"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AuthService from '@/services/auth';
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AuthService from '@/services/auth'
|
||||
import logo from '@/assets/logo_colorful.png'
|
||||
|
||||
export default {
|
||||
name: 'DonConfiao',
|
||||
const router = useRouter()
|
||||
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const loginForm = ref(null)
|
||||
|
||||
methods: {
|
||||
requiredRule(value) {
|
||||
return !!value || 'Este campo es obligatorio';
|
||||
},
|
||||
function requiredRule(value) {
|
||||
return !!value || 'Este campo es obligatorio'
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
this.error = '';
|
||||
async function onSubmit() {
|
||||
error.value = ''
|
||||
isSubmitting.value = true
|
||||
|
||||
const form = this.$refs.loginForm;
|
||||
const isValid = await form.validate();
|
||||
try {
|
||||
const form = loginForm.value
|
||||
if (form) {
|
||||
const { valid } = await form.validate()
|
||||
if (!valid) return
|
||||
}
|
||||
|
||||
if (!isValid) return;
|
||||
if (!username.value || !password.value) {
|
||||
error.value = 'Usuario y contraseña son obligatorios'
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.username || !this.password) {
|
||||
this.error = 'Usuario y contraseña son obligatorios';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AuthService.login({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
});
|
||||
this.$router.push({ path: '/' });
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message ?? e.message;
|
||||
this.error = msg ?? 'Error al iniciar sesión';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
await AuthService.login({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
})
|
||||
router.push({ path: '/' })
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message ?? e.message
|
||||
error.value = msg ?? 'Error al iniciar sesión'
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero-section {
|
||||
min-height: calc(100vh - 80px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: #f8fafc !important;
|
||||
}
|
||||
|
||||
.glow-bubble {
|
||||
position: absolute;
|
||||
border-radius: 10%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
.bubble-blue {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(66, 165, 245, 0.8) 10%,
|
||||
rgba(66, 165, 245, 0) 80%
|
||||
);
|
||||
filter: blur(100px);
|
||||
top: -180px;
|
||||
left: -150px;
|
||||
animation: floatCornerTL 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.bubble-green {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(0, 200, 83, 0.7) 10%,
|
||||
rgba(0, 200, 83, 0) 80%
|
||||
);
|
||||
filter: blur(100px);
|
||||
bottom: -150px;
|
||||
right: -120px;
|
||||
animation: floatCornerBR 9s ease-in-out infinite;
|
||||
animation-delay: 1.5s;
|
||||
}
|
||||
|
||||
.bubble-yellow {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 213, 0, 0.3) 20%,
|
||||
rgba(255, 193, 7, 0) 1000%
|
||||
);
|
||||
filter: blur(80px);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
animation: glowPulseCenter 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.bubble-red {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(239, 83, 80, 0.7) 10%,
|
||||
rgba(239, 83, 80, 0) 80%
|
||||
);
|
||||
filter: blur(100px);
|
||||
top: -150px;
|
||||
right: -120px;
|
||||
animation: floatCornerTR 8s ease-in-out infinite;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
padding: 2.5rem 2rem;
|
||||
background: rgba(255, 255, 255, 0.25) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 10px 40px -10px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.hero-section {
|
||||
padding: 1rem;
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 1.5rem 1.25rem;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatCornerTL {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
transform: scale(0.9) translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.25) translate(40px, 30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatCornerTR {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
transform: scale(1.2) translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.95) translate(-30px, 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floatCornerBR {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
transform: scale(0.85) translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.75;
|
||||
transform: scale(1.15) translate(-40px, -30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowPulseCenter {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.08;
|
||||
transform: translate(-50%, -50%) scale(0.85);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.25;
|
||||
transform: translate(-50%, -50%) scale(1.3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,13 +63,16 @@
|
||||
<v-list-item prepend-icon="mdi-cog" title="Administracion" @click="toggleAdminMenu()" v-if="isAuthenticated && isAdmin"></v-list-item>
|
||||
<v-list-item v-if="isAuthenticated && isAdmin && showAdminMenu">
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="item in menuAdminItems"
|
||||
:key="item.title"
|
||||
:title="item.title"
|
||||
:prepend-icon="item.icon"
|
||||
@click="navigateAdmin(item.route)"
|
||||
></v-list-item>
|
||||
<template v-for="(item, index) in menuAdminItems" :key="index">
|
||||
<v-divider v-if="item.divider"></v-divider>
|
||||
<v-list-subheader v-else-if="item.header">{{ item.header }}</v-list-subheader>
|
||||
<v-list-item
|
||||
v-else
|
||||
:title="item.title"
|
||||
:prepend-icon="item.icon"
|
||||
@click="navigateAdmin(item.route)"
|
||||
></v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -77,7 +80,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import trytonIcon from '../assets/icons/tryton-icon.svg';
|
||||
import AuthService from '@/services/auth';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { inject } from 'vue';
|
||||
@@ -104,11 +106,15 @@
|
||||
{ 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: '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'}
|
||||
{ title: 'Gestión de Productos', route: '/admin/products', icon: 'mdi-package-variant'},
|
||||
{ title: 'Imágenes de Catálogo', route: '/admin/catalogue-images', icon: 'mdi-image-multiple'},
|
||||
{ title: 'Ver Ventas por Catálogo', route: '/admin/catalog-sales', icon: 'mdi-cart-arrow-down'},
|
||||
{ divider: true },
|
||||
{ header: 'Sincronización Tryton' },
|
||||
{ title: 'Importar Productos', route: '/sincronizar_productos_tryton', icon: 'mdi-download'},
|
||||
{ title: 'Importar Clientes', route: '/sincronizar_clientes_tryton', icon: 'mdi-download'},
|
||||
{ title: 'Exportar Ventas', route: '/sincronizar_ventas_tryton', icon: 'mdi-upload'},
|
||||
{ title: 'Exportar Ventas Catálogo', route: '/sincronizar_catalog_sales_tryton', icon: 'mdi-upload'}
|
||||
],
|
||||
}),
|
||||
computed: {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
>
|
||||
Activar seleccionados
|
||||
</v-btn>
|
||||
|
||||
|
||||
<!-- Botón Desactivar: solo visible en filtro "Activos" -->
|
||||
<v-btn
|
||||
v-if="activeFilter === 'true'"
|
||||
@@ -158,7 +158,7 @@ import { ref, watch, inject, onMounted, computed } from "vue";
|
||||
|
||||
// Estado
|
||||
const api = inject("api");
|
||||
const activeFilter = ref("all");
|
||||
const activeFilter = ref("false");
|
||||
const products = ref([]);
|
||||
const selected = ref([]);
|
||||
const loading = ref(false);
|
||||
@@ -178,10 +178,10 @@ 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)
|
||||
return products.value.filter((product) =>
|
||||
product.name.toLowerCase().includes(query),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
:rules="[rules.required]"
|
||||
prefix="$"
|
||||
required
|
||||
readonly
|
||||
:readonly="!isAdmin"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
@@ -323,14 +323,18 @@
|
||||
import CasherModal from './CasherModal.vue';
|
||||
import { inject } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'DonConfiao',
|
||||
components: {
|
||||
CustomerForm,
|
||||
CasherModal,
|
||||
export default {
|
||||
name: 'DonConfiao',
|
||||
components: {
|
||||
CustomerForm,
|
||||
CasherModal,
|
||||
},
|
||||
props: {
|
||||
msg: String
|
||||
msg: String,
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
name: 'SummaryPurchase',
|
||||
props: {
|
||||
msg: String,
|
||||
id: Number
|
||||
id: Number,
|
||||
type: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -102,7 +103,11 @@
|
||||
},
|
||||
methods: {
|
||||
fetchPurchase(purchaseId) {
|
||||
this.api.getSummaryPurchase(purchaseId)
|
||||
const apiMethod = this.type === 'catalog'
|
||||
? this.api.getSummaryCatalogPurchase(purchaseId)
|
||||
: this.api.getSummaryPurchase(purchaseId);
|
||||
|
||||
apiMethod
|
||||
.then(data => {
|
||||
this.purchase = data;
|
||||
})
|
||||
|
||||
@@ -87,16 +87,6 @@
|
||||
<v-col cols="12" class="text-center">
|
||||
<h2 class="text-h5 font-weight-bold mb-4">¿Qué deseas hacer?</h2>
|
||||
<div class="d-flex flex-wrap justify-center ga-4">
|
||||
<v-btn
|
||||
:to="{ path: 'comprar' }"
|
||||
color="green"
|
||||
size="x-large"
|
||||
prepend-icon="mdi-cart"
|
||||
variant="elevated"
|
||||
class="px-8"
|
||||
>
|
||||
Ir a Comprar
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:to="{ path: 'catalog' }"
|
||||
color="primary"
|
||||
@@ -107,6 +97,17 @@
|
||||
>
|
||||
Ver Catálogo
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="authStore.isAuthenticated && authStore.isAdmin"
|
||||
:to="{ path: 'comprar' }"
|
||||
color="green"
|
||||
size="x-large"
|
||||
prepend-icon="mdi-cart"
|
||||
variant="elevated"
|
||||
class="px-8"
|
||||
>
|
||||
Ir a Comprar
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -116,7 +117,10 @@
|
||||
|
||||
<script setup>
|
||||
import ResaltedText from "@/components/ResaltedText.vue";
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import logo from "@/assets/logo_colorful.png";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
10
src/pages/admin/catalogue-images.vue
Normal file
10
src/pages/admin/catalogue-images.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<CatalogueImagesManagement v-if="authStore.isAdmin" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import CatalogueImagesManagement from '@/components/CatalogueImagesManagement.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
@@ -3,5 +3,5 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
import Login from '@/components/Login.vue'
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
<v-icon size="28" color="primary" class="mr-2 d-sm-none flex-shrink-0"
|
||||
>mdi-store</v-icon
|
||||
>
|
||||
<v-icon size="36" color="primary" class="mr-3 d-none d-sm-inline flex-shrink-0"
|
||||
<v-icon
|
||||
size="36"
|
||||
color="primary"
|
||||
class="mr-3 d-none d-sm-inline flex-shrink-0"
|
||||
>mdi-store</v-icon
|
||||
>
|
||||
<div
|
||||
@@ -235,6 +238,7 @@ import Cart from "@/components/catalog/Cart.vue";
|
||||
import PaginationControls from "@/components/catalog/PaginationControls.vue";
|
||||
import { useCartStore } from "@/stores/cart";
|
||||
import { inject, ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import not_image_product from "@/assets/not_image_for_product.jpeg";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -375,9 +379,7 @@ export default {
|
||||
this.items = data.map((product) => ({
|
||||
...product,
|
||||
quantity: 0,
|
||||
img:
|
||||
product.img ||
|
||||
`https://picsum.photos/600/600?random=${product.id}`,
|
||||
img: (product.catalogue_images?.length > 0) ? product.catalogue_images[0] : (product.img || not_image_product),
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -468,7 +470,10 @@ export default {
|
||||
this.personalDataDialog = false;
|
||||
this.$router.push({
|
||||
path: "/summary_purchase",
|
||||
query: { id: parseInt(data.id) },
|
||||
query: {
|
||||
id: parseInt(data.id),
|
||||
type: 'catalog'
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -581,7 +586,7 @@ export default {
|
||||
.page-header .search-field :deep(.v-field__input) {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
|
||||
.page-header .search-field {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
@@ -669,7 +674,7 @@ export default {
|
||||
.product-grid {
|
||||
margin: 0 -6px;
|
||||
}
|
||||
|
||||
|
||||
.product-col {
|
||||
padding: 6px;
|
||||
}
|
||||
@@ -680,7 +685,7 @@ export default {
|
||||
.product-grid {
|
||||
margin: 0 -8px;
|
||||
}
|
||||
|
||||
|
||||
.product-col {
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -691,7 +696,7 @@ export default {
|
||||
.product-grid {
|
||||
margin: 0 -12px;
|
||||
}
|
||||
|
||||
|
||||
.product-col {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<AdminPurchase v-if="authStore.isAdmin"/>
|
||||
<Purchase v-if="authStore.isAdmin" :isAdmin="true" />
|
||||
</template>
|
||||
|
||||
<script >
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
<script setup>
|
||||
import Purchase from '@/components/Purchase.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const authStore = useAuthStore();
|
||||
return { authStore };
|
||||
},
|
||||
const authStore = useAuthStore();
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<Purchase />
|
||||
<Purchase :isAdmin="false" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Purchase from '@/components/Purchase.vue';
|
||||
|
||||
definePage({
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
|
||||
120
src/pages/sincronizar_catalog_sales_tryton.vue
Normal file
120
src/pages/sincronizar_catalog_sales_tryton.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<v-container v-if="authStore.isAdmin" class="fill-height">
|
||||
<v-row v-if="!result && !loading" justify="center">
|
||||
<v-col cols="12" md="8">
|
||||
<v-card class="pa-6" elevation="4">
|
||||
<v-card-title class="text-h5 font-weight-bold text-center">
|
||||
🔄 Sincronización de Ventas de Catálogo
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p>
|
||||
Esta acción sincronizará las <strong>ventas de catálogo</strong> desde el sistema
|
||||
<strong>Tryton</strong> hacia la plataforma.
|
||||
</p>
|
||||
<v-alert type="warning" dense border="start" border-color="warning" class="mt-4">
|
||||
<strong>Advertencia:</strong> Este proceso podría tardar varios minutos
|
||||
y reemplazar datos existentes en la plataforma.
|
||||
Asegúrese de que la información en Tryton esté actualizada antes de
|
||||
continuar.
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn color="primary" @click="startSync">
|
||||
Iniciar Sincronización
|
||||
</v-btn>
|
||||
<v-btn text @click="$router.push('/')">
|
||||
Cancelar
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else-if="loading" justify="center" align="center">
|
||||
<v-col cols="12" class="text-center">
|
||||
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
||||
<p class="mt-4 text-h6">Sincronizando ventas de catálogo...</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col cols="12">
|
||||
<v-alert type="success" variant="tonal" class="mb-4">
|
||||
<strong>Sincronización completada</strong>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card elevation="2">
|
||||
<v-card-title class="bg-error text-white">❌ Fallidos ({{ result.failed?.length || 0 }})</v-card-title>
|
||||
<v-card-text>
|
||||
<v-data-table
|
||||
:items="formatItems(result.failed)"
|
||||
density="compact"
|
||||
:headers="[{ title: 'ID', key: 'id' }]"
|
||||
></v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card elevation="2">
|
||||
<v-card-title class="bg-success text-white">✅ Exitosos ({{ result.successful?.length || 0 }})</v-card-title>
|
||||
<v-card-text>
|
||||
<v-data-table
|
||||
:items="formatItems(result.successful)"
|
||||
density="compact"
|
||||
:headers="[{ title: 'ID', key: 'id' }]"
|
||||
></v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" class="text-center mt-4">
|
||||
<v-btn color="primary" @click="$router.push('/')">
|
||||
Volver al inicio
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { inject } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'CatalogSalesToTryton',
|
||||
setup() {
|
||||
const authStore = useAuthStore();
|
||||
return { authStore };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
api: inject('api'),
|
||||
loading: false,
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatItems(ids) {
|
||||
if (!ids || ids.length === 0) return [];
|
||||
return ids.map(id => ({ id }));
|
||||
},
|
||||
startSync() {
|
||||
this.loading = true;
|
||||
this.api.sendCatalogSalesToTryton()
|
||||
.then(response => {
|
||||
this.result = response;
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error al sincronizar ventas de catálogo:', error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<SummaryPurchase :id="$route.query.id"/>
|
||||
<SummaryPurchase :id="$route.query.id" :type="$route.query.type"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -21,6 +21,7 @@ const ADMIN_ROUTES = [
|
||||
'/cuadrar_tarro',
|
||||
'/admin/products',
|
||||
'/admin/catalog-sales',
|
||||
'/admin/catalogue-images',
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -23,6 +23,10 @@ class Api {
|
||||
return this.apiImplementation.getSummaryPurchase(purchaseId);
|
||||
}
|
||||
|
||||
getSummaryCatalogPurchase(purchaseId) {
|
||||
return this.apiImplementation.getSummaryCatalogPurchase(purchaseId);
|
||||
}
|
||||
|
||||
getPurchasesForReconciliation() {
|
||||
return this.apiImplementation.getPurchasesForReconciliation();
|
||||
}
|
||||
@@ -67,6 +71,10 @@ class Api {
|
||||
return this.apiImplementation.sendSalesToTryton();
|
||||
}
|
||||
|
||||
sendCatalogSalesToTryton() {
|
||||
return this.apiImplementation.sendCatalogSalesToTryton();
|
||||
}
|
||||
|
||||
getCatalogSales() {
|
||||
return this.apiImplementation.getCatalogSales();
|
||||
}
|
||||
@@ -74,6 +82,22 @@ class Api {
|
||||
getCurrentUser() {
|
||||
return this.apiImplementation.getCurrentUser();
|
||||
}
|
||||
|
||||
getCatalogueImages() {
|
||||
return this.apiImplementation.getCatalogueImages();
|
||||
}
|
||||
|
||||
createCatalogueImage(data) {
|
||||
return this.apiImplementation.createCatalogueImage(data);
|
||||
}
|
||||
|
||||
updateCatalogueImage(id, data) {
|
||||
return this.apiImplementation.updateCatalogueImage(id, data);
|
||||
}
|
||||
|
||||
deleteCatalogueImage(id) {
|
||||
return this.apiImplementation.deleteCatalogueImage(id);
|
||||
}
|
||||
}
|
||||
|
||||
export default Api;
|
||||
|
||||
@@ -51,6 +51,12 @@ class DjangoApi {
|
||||
return this.getRequest(url);
|
||||
}
|
||||
|
||||
getSummaryCatalogPurchase(purchaseId) {
|
||||
const url =
|
||||
this.base + `/don_confiao/resumen_compra_catalogo_json/${purchaseId}`;
|
||||
return this.getRequest(url);
|
||||
}
|
||||
|
||||
getPurchasesForReconciliation() {
|
||||
const url = this.base + "/don_confiao/purchases/for_reconciliation";
|
||||
return this.getRequest(url);
|
||||
@@ -110,6 +116,11 @@ class DjangoApi {
|
||||
return this.postRequest(url, {});
|
||||
}
|
||||
|
||||
sendCatalogSalesToTryton() {
|
||||
const url = this.base + "/don_confiao/api/enviar_catalog_sales_a_tryton";
|
||||
return this.postRequest(url, {});
|
||||
}
|
||||
|
||||
getCatalogSales() {
|
||||
const url = this.base + "/don_confiao/api/catalog_sales/";
|
||||
return this.getRequest(url);
|
||||
@@ -119,6 +130,30 @@ class DjangoApi {
|
||||
const url = this.base + "/api/users/me/";
|
||||
return this.getRequest(url);
|
||||
}
|
||||
|
||||
getCatalogueImages() {
|
||||
const url = this.base + "/don_confiao/api/catalogue_images/";
|
||||
return this.getRequest(url);
|
||||
}
|
||||
|
||||
createCatalogueImage(data) {
|
||||
const url = this.base + "/don_confiao/api/catalogue_images/";
|
||||
return http.post(url, data, {
|
||||
headers: { 'Content-Type': undefined },
|
||||
}).then((r) => r.data);
|
||||
}
|
||||
|
||||
updateCatalogueImage(id, data) {
|
||||
const url = this.base + `/don_confiao/api/catalogue_images/${id}/`;
|
||||
return http.put(url, data, {
|
||||
headers: { 'Content-Type': undefined },
|
||||
}).then((r) => r.data);
|
||||
}
|
||||
|
||||
deleteCatalogueImage(id) {
|
||||
const url = this.base + `/don_confiao/api/catalogue_images/${id}/`;
|
||||
return http.delete(url).then((r) => r.data);
|
||||
}
|
||||
}
|
||||
|
||||
export default DjangoApi;
|
||||
|
||||
Reference in New Issue
Block a user