Compare commits
26 Commits
e2604a1837
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb26f55829 | |||
|
|
5397ab2255 | ||
|
|
a2ab4fceb7 | ||
| 78dfea8714 | |||
| 490cb7b53d | |||
| 5e86595831 | |||
| 897cbb3efc | |||
| 925fadba2d | |||
| d049357231 | |||
| a36fbd289e | |||
| 368b7007f6 | |||
| f79197baf5 | |||
| 3f0f1fe09a | |||
| ed42eb324c | |||
| d5e30c92b0 | |||
| 6aecbd37d2 | |||
| cb79ad6d45 | |||
| 38c1d8c17c | |||
| 398a4cf79d | |||
| 6970867f7b | |||
| 196a5e2068 | |||
| 619590adcc | |||
| da45c4c1f7 | |||
| 690c8ff288 | |||
| df291df451 | |||
| 9ea01eed39 |
@@ -1,19 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<NavBar />
|
|
||||||
<v-main>
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</v-main>
|
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NavBar from './components/NavBar.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
|
||||||
NavBar,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
BIN
src/assets/logo_colorful.png
Normal file
BIN
src/assets/logo_colorful.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 189 KiB |
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>
|
|
||||||
527
src/components/CatalogSalesManagement.vue
Normal file
527
src/components/CatalogSalesManagement.vue
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<!-- Header Principal -->
|
||||||
|
<v-row align="center" class="mb-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<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"
|
||||||
|
: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 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
|
||||||
|
|
||||||
|
// 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' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ventas pendientes filtradas
|
||||||
|
const filteredPendingSales = computed(() => {
|
||||||
|
return applyFilters(pendingSales.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ventas sincronizadas filtradas
|
||||||
|
const filteredSyncedSales = computed(() => {
|
||||||
|
return applyFilters(syncedSales.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
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>
|
<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>
|
||||||
|
|
||||||
|
<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-form ref="loginForm" @submit.prevent="onSubmit">
|
<v-form ref="loginForm" @submit.prevent="onSubmit">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="username"
|
v-model="username"
|
||||||
label="Usuario"
|
label="Usuario"
|
||||||
|
prepend-inner-icon="mdi-account"
|
||||||
:rules="[requiredRule]"
|
:rules="[requiredRule]"
|
||||||
|
variant="outlined"
|
||||||
required
|
required
|
||||||
|
class="mb-2"
|
||||||
|
autocomplete="username"
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="password"
|
v-model="password"
|
||||||
label="Contraseña"
|
label="Contraseña"
|
||||||
|
prepend-inner-icon="mdi-lock"
|
||||||
type="password"
|
type="password"
|
||||||
:rules="[requiredRule]"
|
:rules="[requiredRule]"
|
||||||
|
variant="outlined"
|
||||||
required
|
required
|
||||||
|
class="mb-4"
|
||||||
|
autocomplete="current-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-btn type="submit" color="primary">Entrar</v-btn>
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Entrar
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<v-alert v-if="error" type="error" class="mt-2">{{ error }}</v-alert>
|
<v-alert
|
||||||
|
v-if="error"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
class="mt-4"
|
||||||
|
closable
|
||||||
|
@click:close="error = ''"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</v-alert>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
</div>
|
||||||
|
</v-sheet>
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import AuthService from '@/services/auth';
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import AuthService from '@/services/auth'
|
||||||
|
import logo from '@/assets/logo_colorful.png'
|
||||||
|
|
||||||
export default {
|
const router = useRouter()
|
||||||
name: 'DonConfiao',
|
|
||||||
|
|
||||||
data() {
|
const username = ref('')
|
||||||
return {
|
const password = ref('')
|
||||||
username: '',
|
const error = ref('')
|
||||||
password: '',
|
const isSubmitting = ref(false)
|
||||||
error: '',
|
const loginForm = ref(null)
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
function requiredRule(value) {
|
||||||
requiredRule(value) {
|
return !!value || 'Este campo es obligatorio'
|
||||||
return !!value || 'Este campo es obligatorio';
|
|
||||||
},
|
|
||||||
|
|
||||||
async onSubmit() {
|
|
||||||
this.error = '';
|
|
||||||
|
|
||||||
const form = this.$refs.loginForm;
|
|
||||||
const isValid = await form.validate();
|
|
||||||
|
|
||||||
if (!isValid) return;
|
|
||||||
|
|
||||||
if (!this.username || !this.password) {
|
|
||||||
this.error = 'Usuario y contraseña son obligatorios';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
error.value = ''
|
||||||
|
isSubmitting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AuthService.login({
|
const form = loginForm.value
|
||||||
username: this.username,
|
if (form) {
|
||||||
password: this.password,
|
const { valid } = await form.validate()
|
||||||
});
|
if (!valid) return
|
||||||
this.$router.push({ path: '/' });
|
}
|
||||||
} catch (e) {
|
|
||||||
const msg = e?.response?.data?.message ?? e.message;
|
if (!username.value || !password.value) {
|
||||||
this.error = msg ?? 'Error al iniciar sesión';
|
error.value = 'Usuario y contraseña son obligatorios'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</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>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app-bar color="primary" prominent>
|
<v-app-bar color="primary" prominent app>
|
||||||
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
||||||
<v-toolbar-title>Menu</v-toolbar-title>
|
<v-toolbar-title>Menu</v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
@@ -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 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-item v-if="isAuthenticated && isAdmin && showAdminMenu">
|
||||||
<v-list>
|
<v-list>
|
||||||
|
<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-list-item
|
||||||
v-for="item in menuAdminItems"
|
v-else
|
||||||
:key="item.title"
|
|
||||||
:title="item.title"
|
:title="item.title"
|
||||||
:prepend-icon="item.icon"
|
:prepend-icon="item.icon"
|
||||||
@click="navigateAdmin(item.route)"
|
@click="navigateAdmin(item.route)"
|
||||||
></v-list-item>
|
></v-list-item>
|
||||||
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
@@ -77,7 +80,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import trytonIcon from '../assets/icons/tryton-icon.svg';
|
|
||||||
import AuthService from '@/services/auth';
|
import AuthService from '@/services/auth';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
@@ -104,9 +106,15 @@
|
|||||||
{ title: 'Cuadres de tarro', route: '/cuadres_de_tarro', icon: 'mdi-chart-bar'},
|
{ title: 'Cuadres de tarro', route: '/cuadres_de_tarro', icon: 'mdi-chart-bar'},
|
||||||
{ title: 'CSV Tryton', route: '/ventas_para_tryton', icon: 'mdi-file-table'},
|
{ title: 'CSV Tryton', route: '/ventas_para_tryton', icon: 'mdi-file-table'},
|
||||||
{ title: 'Compra adm', route: '/compra_admin', icon: 'mdi-cart'},
|
{ title: 'Compra adm', route: '/compra_admin', icon: 'mdi-cart'},
|
||||||
{ title: 'Actualizar Productos De Tryton', route: '/sincronizar_productos_tryton', icon: 'trytonIcon'},
|
{ title: 'Gestión de Productos', route: '/admin/products', icon: 'mdi-package-variant'},
|
||||||
{ title: 'Actualizar Clientes De Tryton', route: '/sincronizar_clientes_tryton', icon: 'trytonIcon'},
|
{ title: 'Imágenes de Catálogo', route: '/admin/catalogue-images', icon: 'mdi-image-multiple'},
|
||||||
{ title: 'Actualizar Ventas Tryton', route: '/sincronizar_ventas_tryton', icon: 'trytonIcon'}
|
{ 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: {
|
computed: {
|
||||||
@@ -163,3 +171,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
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("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>
|
||||||
@@ -13,10 +13,15 @@
|
|||||||
|
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
<template v-if="loading">
|
<template v-if="loading">
|
||||||
<v-skeleton-loader
|
<v-sheet class="d-flex flex-column align-center justify-center pa-12 rounded-lg" elevation="2">
|
||||||
class="mb-4 rounded-lg"
|
<v-progress-circular
|
||||||
type="card-heading, list-item-two-line, list-item-two-line"
|
indeterminate
|
||||||
></v-skeleton-loader>
|
color="primary"
|
||||||
|
size="64"
|
||||||
|
width="6"
|
||||||
|
></v-progress-circular>
|
||||||
|
<p class="text-body-1 text-grey mt-4">Cargando datos...</p>
|
||||||
|
</v-sheet>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -168,7 +173,7 @@
|
|||||||
:rules="[rules.required]"
|
:rules="[rules.required]"
|
||||||
prefix="$"
|
prefix="$"
|
||||||
required
|
required
|
||||||
readonly
|
:readonly="!isAdmin"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
@@ -246,7 +251,7 @@
|
|||||||
<v-row align="center">
|
<v-row align="center">
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-select
|
<v-select
|
||||||
:items="payment_methods"
|
:items="payment_methods || []"
|
||||||
v-model="purchase.payment_method"
|
v-model="purchase.payment_method"
|
||||||
item-title="text"
|
item-title="text"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
@@ -325,7 +330,11 @@
|
|||||||
CasherModal,
|
CasherModal,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
msg: String
|
msg: String,
|
||||||
|
isAdmin: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -337,7 +346,7 @@
|
|||||||
show_alert_purchase: false,
|
show_alert_purchase: false,
|
||||||
client_search: '',
|
client_search: '',
|
||||||
product_search: '',
|
product_search: '',
|
||||||
payment_methods: null,
|
payment_methods: [],
|
||||||
purchase: {
|
purchase: {
|
||||||
date: this.getCurrentDate(),
|
date: this.getCurrentDate(),
|
||||||
customer: null,
|
customer: null,
|
||||||
@@ -471,15 +480,13 @@
|
|||||||
formatPrice(price) {
|
formatPrice(price) {
|
||||||
return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', minimumFractionDigits: 0 }).format(price);
|
return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', minimumFractionDigits: 0 }).format(price);
|
||||||
},
|
},
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fetchClients();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.page-header {
|
.page-header {
|
||||||
background: linear-gradient(135deg, #1565C0 0%, #0D47A1 100%);
|
background: linear-gradient(135deg, #1565C0 0%, #0D47A1 100%) !important;
|
||||||
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-line:nth-child(odd) {
|
.product-line:nth-child(odd) {
|
||||||
|
|||||||
@@ -72,7 +72,8 @@
|
|||||||
name: 'SummaryPurchase',
|
name: 'SummaryPurchase',
|
||||||
props: {
|
props: {
|
||||||
msg: String,
|
msg: String,
|
||||||
id: Number
|
id: Number,
|
||||||
|
type: String
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -102,7 +103,11 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchPurchase(purchaseId) {
|
fetchPurchase(purchaseId) {
|
||||||
this.api.getSummaryPurchase(purchaseId)
|
const apiMethod = this.type === 'catalog'
|
||||||
|
? this.api.getSummaryCatalogPurchase(purchaseId)
|
||||||
|
: this.api.getSummaryPurchase(purchaseId);
|
||||||
|
|
||||||
|
apiMethod
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.purchase = data;
|
this.purchase = data;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,17 +3,19 @@
|
|||||||
<v-sheet
|
<v-sheet
|
||||||
class="hero-section d-flex align-center justify-center text-center pa-8"
|
class="hero-section d-flex align-center justify-center text-center pa-8"
|
||||||
>
|
>
|
||||||
<div>
|
<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>
|
||||||
|
<div class="hero-content">
|
||||||
<v-img
|
<v-img
|
||||||
:src="logo"
|
:src="logo"
|
||||||
alt="Don Confiao"
|
alt="Don Confiao"
|
||||||
max-width="180"
|
max-width="180"
|
||||||
class="mx-auto mb-4"
|
class="mx-auto mb-4"
|
||||||
/>
|
/>
|
||||||
<h1 class="text-h4 font-weight-bold text-white mb-2">
|
<h1 class="text-h4 font-weight-bold mb-2">Don Confiao te atiende</h1>
|
||||||
Don Confiao te atiende
|
<p class="text-subtitle-1 font-italic font-weight-bold">
|
||||||
</h1>
|
|
||||||
<p class="text-subtitle-1 font-italic font-weight-bold text-white">
|
|
||||||
Economía solidaria, mercado justo, alimentación sana
|
Economía solidaria, mercado justo, alimentación sana
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,10 +29,14 @@
|
|||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon color="green" size="48">mdi-hand-heart</v-icon>
|
<v-icon color="green" size="48">mdi-hand-heart</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-card-title class="font-weight-bold">Nuestra Tienda</v-card-title>
|
<v-card-title class="font-weight-bold"
|
||||||
|
>Nuestra Tienda</v-card-title
|
||||||
|
>
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
Hacer parte de la tienda la ilusión. Participando de esta tienda le apuestas a la economía solidaria, al mercado justo, a la alimentación sana, al campesinado colombiano y a un mundo mejor.
|
Hacer parte de la tienda la ilusión. Participando de esta tienda
|
||||||
|
le apuestas a la economía solidaria, al mercado justo, a la
|
||||||
|
alimentación sana, al campesinado colombiano y a un mundo mejor.
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -39,14 +45,23 @@
|
|||||||
<v-card class="h-100" elevation="2">
|
<v-card class="h-100" elevation="2">
|
||||||
<v-card-item>
|
<v-card-item>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon color="orange-darken-2" size="48">mdi-progress-wrench</v-icon>
|
<v-icon color="orange-darken-2" size="48"
|
||||||
|
>mdi-progress-wrench</v-icon
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<v-card-title class="font-weight-bold">En Desarrollo</v-card-title>
|
<v-card-title class="font-weight-bold"
|
||||||
|
>En Desarrollo</v-card-title
|
||||||
|
>
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
Don Confiao apenas está entendiendo cómo funciona esta tienda y por ahora <ResaltedText>solo puede atender las compras de contado</ResaltedText>, ya sea en efectivo o consignación.
|
Don Confiao apenas está entendiendo cómo funciona esta tienda y
|
||||||
|
por ahora
|
||||||
|
<ResaltedText
|
||||||
|
>solo puede atender las compras de contado</ResaltedText
|
||||||
|
>, ya sea en efectivo o consignación.
|
||||||
<v-alert type="warning" class="mt-3" density="compact">
|
<v-alert type="warning" class="mt-3" density="compact">
|
||||||
Si no vas a pagar tu compra recuerda que debes hacerlo en la planilla manual
|
Si no vas a pagar tu compra recuerda que debes hacerlo en la
|
||||||
|
planilla manual
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -61,7 +76,8 @@
|
|||||||
<v-card-title class="font-weight-bold">Catálogo</v-card-title>
|
<v-card-title class="font-weight-bold">Catálogo</v-card-title>
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
Explora nuestro catálogo de productos disponibles. Encuentra todo lo que necesitas y arma tu pedido fácilmente.
|
Explora nuestro catálogo de productos disponibles. Encuentra todo
|
||||||
|
lo que necesitas y arma tu pedido fácilmente.
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -71,16 +87,6 @@
|
|||||||
<v-col cols="12" class="text-center">
|
<v-col cols="12" class="text-center">
|
||||||
<h2 class="text-h5 font-weight-bold mb-4">¿Qué deseas hacer?</h2>
|
<h2 class="text-h5 font-weight-bold mb-4">¿Qué deseas hacer?</h2>
|
||||||
<div class="d-flex flex-wrap justify-center ga-4">
|
<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
|
<v-btn
|
||||||
:to="{ path: 'catalog' }"
|
:to="{ path: 'catalog' }"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -91,6 +97,17 @@
|
|||||||
>
|
>
|
||||||
Ver Catálogo
|
Ver Catálogo
|
||||||
</v-btn>
|
</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>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@@ -99,14 +116,176 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ResaltedText from '@/components/ResaltedText.vue'
|
import ResaltedText from "@/components/ResaltedText.vue";
|
||||||
import logo from '@/assets/logo.png'
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import logo from "@/assets/logo_colorful.png";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.hero-section {
|
.hero-section {
|
||||||
min-height: 320px;
|
min-height: 500px; /* Un poco más de aire vertical */
|
||||||
background: linear-gradient(135deg, #1B5E20 0%, #2E7D32 30%, #E65100 100%) !important;
|
display: flex;
|
||||||
color: white;
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
/* Fondo blanco tiza ultra limpio para que los colores pastel floten */
|
||||||
|
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.85) 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- CONTENEDOR ELEGANTE (Glassmorphism) --- */
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
color: #0f172a !important; /* Azul pizarra profundo de alta costura */
|
||||||
|
text-align: center;
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 3rem 2.5rem;
|
||||||
|
|
||||||
|
/* El secreto elegante: un sutil escudo de cristal que desenfoca el fondo */
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
|
box-shadow: 0 10px 40px -10px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eliminamos las sombras de texto redundantes gracias al escudo protector de cristal */
|
||||||
|
.hero-content h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em; /* Tipografía compacta estilo Apple */
|
||||||
|
line-height: 1.15;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content p {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
color: #475569; /* Gris suavizado elegante para el subtítulo */
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ANIMACIONES Ralentizadas y Cinemáticas --- */
|
||||||
|
/* --- 1. Esquina Superior Izquierda (Azul) --- */
|
||||||
|
@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); /* Movimiento fluido y orgánico */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 2. Esquina Superior Derecha (Rojo) --- */
|
||||||
|
@keyframes floatCornerTR {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.15;
|
||||||
|
transform: scale(1.2) translate(0, 0); /* Corregido: Escala inicial estable */
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(0.95) translate(-30px, 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 3. Esquina Inferior Derecha (Verde) - CORREGIDA --- */
|
||||||
|
@keyframes floatCornerBR {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.1; /* Corregido: Cambiado de 0 a un estado tenue elegante */
|
||||||
|
transform: scale(0.85) translate(0, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.75; /* Corregido: Cambiado de 0 a visible */
|
||||||
|
transform: scale(1.15) translate(-40px, -30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 4. Centro (Amarillo) - Optimizado en 3 puntos para máxima fluidez --- */
|
||||||
|
@keyframes glowPulseCenter {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.25;
|
||||||
|
transform: translate(-50%, -50%) scale(0.85);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.9; /* Simplificado a 50% para sincronía perfecta con las esquinas */
|
||||||
|
transform: translate(-50%, -50%) scale(1.3); /* Un destello central controlado */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,64 +1,107 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card class="product-card">
|
<v-card class="product-card" elevation="2" rounded="lg">
|
||||||
<v-row class="product-card-row">
|
<!-- Imagen del Producto -->
|
||||||
<!-- Columna de Imagen -->
|
<div class="product-image-container">
|
||||||
<v-col cols="12" md="4" class="image-column">
|
<v-img
|
||||||
<v-img :src="product.img" class="product-img" contain></v-img>
|
:src="product.img"
|
||||||
</v-col>
|
:alt="product.name"
|
||||||
|
class="product-img"
|
||||||
|
cover
|
||||||
|
max-height="300"
|
||||||
|
aspect-ratio="1"
|
||||||
|
>
|
||||||
|
<template v-slot:placeholder>
|
||||||
|
<div class="d-flex align-center justify-center fill-height">
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
size="32"
|
||||||
|
></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-img>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Columna de Detalles -->
|
<!-- Contenido de la Tarjeta -->
|
||||||
<v-col cols="12" md="5" class="details-column">
|
<v-card-text class="product-content pa-3 text-center">
|
||||||
<div class="product-details-content">
|
<!-- Título del Producto -->
|
||||||
<v-tooltip location="top" :text="product.name">
|
<v-tooltip location="top" :text="product.name">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-card-title class="product-name" v-bind="props" :title="product.name">
|
<h3
|
||||||
|
class="product-name text-subtitle-1 font-weight-medium mb-2"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
{{ product.name }}
|
{{ product.name }}
|
||||||
</v-card-title>
|
</h3>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<v-card-subtitle class="product-description">{{
|
|
||||||
product.description
|
|
||||||
}}</v-card-subtitle>
|
|
||||||
|
|
||||||
<div class="prices">
|
<!-- Sección de Precios -->
|
||||||
<div class="price-unit">
|
<div class="prices-section mb-2">
|
||||||
<span class="price-label">Precio unitario</span>
|
<!-- Precio Unitario -->
|
||||||
<span class="price-value">{{ currency(product.price) }}</span>
|
<div class="price-row mb-1">
|
||||||
|
<span class="price-label text-caption">Precio unitario</span>
|
||||||
|
<div class="price-value text-body-1 font-weight-bold text-primary">
|
||||||
|
{{ currency(product.price) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="price-total">
|
<!-- Precio Total -->
|
||||||
<span class="price-label">Precio total</span>
|
<div class="price-row">
|
||||||
<span class="price-value total"
|
<span class="price-label text-caption">Precio total</span>
|
||||||
>{{ currency(product.price * product.quantity) }}
|
<v-chip
|
||||||
</span>
|
color="success"
|
||||||
|
variant="flat"
|
||||||
|
size="small"
|
||||||
|
class="price-total-chip font-weight-bold mt-1"
|
||||||
|
>
|
||||||
|
{{ currency(product.price * product.quantity) }}
|
||||||
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</v-card-text>
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<!-- Columna de Controles -->
|
<!-- Footer con Controles de Cantidad -->
|
||||||
<v-col cols="12" md="3" class="controls-column">
|
<v-card-actions class="product-actions pa-2 pb-3 justify-center">
|
||||||
<div class="quantity-controls">
|
<div class="quantity-controls">
|
||||||
<v-btn icon small class="qty-btn" @click="decrease(product)">
|
<v-btn
|
||||||
<v-icon small>mdi-minus</v-icon>
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
color="error"
|
||||||
|
class="qty-btn"
|
||||||
|
@click="decrease(product)"
|
||||||
|
:disabled="product.quantity === 0"
|
||||||
|
>
|
||||||
|
<v-icon size="20">mdi-minus</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model.number="product.quantity"
|
v-model.number="product.quantity"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
class="quantity-input"
|
class="quantity-input mx-1"
|
||||||
dense
|
variant="solo-filled"
|
||||||
outlined
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
|
single-line
|
||||||
|
flat
|
||||||
aria-label="Cantidad"
|
aria-label="Cantidad"
|
||||||
@input="handleQuantityChange"
|
@input="handleQuantityChange"
|
||||||
/>
|
/>
|
||||||
<v-btn icon small class="qty-btn qty-btn-add" @click="handleIncrease">
|
|
||||||
<v-icon small>mdi-plus</v-icon>
|
<v-btn
|
||||||
|
icon
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
color="success"
|
||||||
|
class="qty-btn"
|
||||||
|
@click="handleIncrease"
|
||||||
|
>
|
||||||
|
<v-icon size="20">mdi-plus</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-card-actions>
|
||||||
</v-row>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -102,400 +145,454 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* ===== ESTILOS BASE - MOBILE FIRST (< 560px) ===== */
|
/* ============================================
|
||||||
|
CARD CONTAINER
|
||||||
/* Card Container */
|
============================================ */
|
||||||
.product-card {
|
.product-card {
|
||||||
border: 1px solid #e0e0e0;
|
height: 100%;
|
||||||
border-radius: 8px;
|
display: flex;
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
flex-direction: column;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
background: #ffffff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-card:hover {
|
.product-card:hover {
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
transform: translateY(-6px);
|
||||||
transform: translateY(-2px);
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12) !important;
|
||||||
border-color: #bdbdbd;
|
border-color: rgba(33, 150, 243, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Row Container */
|
/* ============================================
|
||||||
.product-card-row {
|
IMAGEN DEL PRODUCTO
|
||||||
padding: 12px 8px;
|
============================================ */
|
||||||
margin: 0;
|
.product-image-container {
|
||||||
}
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
/* === COLUMNA DE IMAGEN === */
|
height: 100%;
|
||||||
.image-column {
|
overflow: hidden;
|
||||||
display: flex;
|
background: linear-gradient(135deg, #fafafa 0%, #ffffff 100%);
|
||||||
justify-content: center;
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
align-items: center;
|
|
||||||
padding: 0 0 16px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-img {
|
.product-img {
|
||||||
width: 280px;
|
width: 100%;
|
||||||
height: 280px;
|
height: 100%;
|
||||||
border-radius: 12px;
|
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
object-fit: cover;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-card:hover .product-img {
|
.product-card:hover .product-img {
|
||||||
transform: scale(1.03);
|
transform: scale(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === COLUMNA DE DETALLES === */
|
/* ============================================
|
||||||
.details-column {
|
CONTENIDO DE LA TARJETA
|
||||||
padding: 0;
|
============================================ */
|
||||||
|
.product-content {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
align-items: center;
|
||||||
}
|
justify-content: flex-start;
|
||||||
|
|
||||||
.product-details-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
color: #2c3e50;
|
color: #1a1a1a;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
word-break: break-word;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #7f8c8d;
|
|
||||||
line-height: 1.4;
|
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-overflow: ellipsis;
|
||||||
padding: 0;
|
min-height: 2.5rem;
|
||||||
margin-bottom: 4px;
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === PRECIOS === */
|
/* ============================================
|
||||||
.prices {
|
SECCIÓN DE PRECIOS
|
||||||
|
============================================ */
|
||||||
|
.prices-section {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-unit,
|
.price-row {
|
||||||
.price-total {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 2px;
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-label {
|
.price-label {
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #95a5a6;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.8px;
|
||||||
white-space: nowrap;
|
font-size: 0.65rem;
|
||||||
|
color: #9e9e9e;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-value {
|
.price-value {
|
||||||
font-size: 0.85rem;
|
color: #1565c0;
|
||||||
font-weight: 600;
|
letter-spacing: 0.02em;
|
||||||
color: #2c3e50;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-value.total {
|
.price-total-chip {
|
||||||
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
letter-spacing: 0.03em;
|
||||||
color: white;
|
font-size: 0.9rem;
|
||||||
padding: 3px 10px;
|
padding: 0 12px;
|
||||||
border-radius: 20px;
|
height: 26px;
|
||||||
font-size: 0.95rem;
|
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.25);
|
||||||
box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === COLUMNA DE CONTROLES === */
|
/* ============================================
|
||||||
.controls-column {
|
CONTROLES DE CANTIDAD
|
||||||
display: flex;
|
============================================ */
|
||||||
align-items: center;
|
.product-actions {
|
||||||
justify-content: center;
|
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
padding: 16px 0 0 0;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quantity-controls {
|
.quantity-controls {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 25px;
|
|
||||||
padding: 6px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-input {
|
|
||||||
max-width: 60px;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-input input {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qty-btn {
|
|
||||||
min-width: 32px !important;
|
|
||||||
width: 32px !important;
|
|
||||||
height: 32px !important;
|
|
||||||
border-radius: 50% !important;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qty-btn:hover {
|
|
||||||
background: #e0e0e0;
|
|
||||||
transform: scale(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.qty-btn-add {
|
|
||||||
background: #27ae60;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qty-btn-add:hover {
|
|
||||||
background: #219a52 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== RESOLUCIÓN 375-559px (Mobile Standard) ===== */
|
|
||||||
@media (min-width: 375px) and (max-width: 559px) {
|
|
||||||
.product-img {
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card-row {
|
|
||||||
padding: 14px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-name {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quantity-input {
|
.quantity-input {
|
||||||
max-width: 65px;
|
max-width: 65px;
|
||||||
min-width: 65px;
|
min-width: 65px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quantity-input :deep(.v-field) {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== RESOLUCIÓN 560-959px (Tablet) ===== */
|
.quantity-input :deep(.v-field__input) {
|
||||||
@media (min-width: 560px) and (max-width: 959px) {
|
text-align: center;
|
||||||
.product-card-row {
|
font-weight: 700;
|
||||||
padding: 16px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-column {
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-img {
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-details-content {
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-name {
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prices {
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-value {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-value.total {
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 4px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-column {
|
|
||||||
padding-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-input {
|
|
||||||
max-width: 70px;
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qty-btn {
|
|
||||||
min-width: 34px !important;
|
|
||||||
width: 34px !important;
|
|
||||||
height: 34px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== RESOLUCIÓN ≥960px (Desktop) ===== */
|
|
||||||
@media (min-width: 960px) {
|
|
||||||
.product-card {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover {
|
|
||||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover .product-img {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card-row {
|
|
||||||
padding: 16px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-column {
|
|
||||||
padding: 0 24px 0 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
max-width: 380px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-img {
|
|
||||||
width: 350px;
|
|
||||||
height: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-column {
|
|
||||||
padding: 0 24px 0 0;
|
|
||||||
border-right: 1px solid #e0e0e0;
|
|
||||||
flex: 1;
|
|
||||||
align-items: flex-start;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-details-content {
|
|
||||||
gap: 16px;
|
|
||||||
align-items: flex-start;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-name {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-description {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prices {
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-unit,
|
|
||||||
.price-total {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-value {
|
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
padding: 4px 0;
|
||||||
|
min-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-value.total {
|
.quantity-input :deep(.v-field__field) {
|
||||||
font-size: 1.05rem;
|
padding: 0;
|
||||||
padding: 4px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-column {
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0 0 0 16px;
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-controls {
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity-input {
|
|
||||||
max-width: 70px;
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qty-btn {
|
.qty-btn {
|
||||||
min-width: 36px !important;
|
flex-shrink: 0;
|
||||||
width: 36px !important;
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
height: 36px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qty-btn:hover {
|
.qty-btn:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qty-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== RESOLUCIÓN ≥1280px (Desktop Large) ===== */
|
/* ============================================
|
||||||
@media (min-width: 1280px) {
|
RESPONSIVE BREAKPOINTS
|
||||||
.product-card-row {
|
============================================ */
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-column {
|
/* Móvil pequeño (< 375px) */
|
||||||
padding-right: 28px;
|
@media (max-width: 374px) {
|
||||||
}
|
.product-content {
|
||||||
|
padding: 10px;
|
||||||
.details-column {
|
|
||||||
padding-right: 28px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-name {
|
.product-name {
|
||||||
font-size: 1.2rem;
|
font-size: 0.9rem;
|
||||||
|
min-height: 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-description {
|
.price-value {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
max-width: 60px;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Móvil estándar (375px - 559px) */
|
||||||
|
@media (min-width: 375px) and (max-width: 559px) {
|
||||||
|
.product-name {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-column {
|
.price-value {
|
||||||
padding-left: 20px;
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet (560px - 959px) */
|
||||||
|
@media (min-width: 560px) and (max-width: 959px) {
|
||||||
|
.product-content {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 1rem;
|
||||||
|
min-height: 2.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-total-chip {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
max-width: 70px;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop (≥ 960px) */
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.product-content {
|
||||||
|
padding: 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
min-height: 2.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-total-chip {
|
||||||
|
font-size: 1rem;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-actions {
|
||||||
|
padding: 8px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
max-width: 70px;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop Large (≥ 1280px) */
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.product-content {
|
||||||
|
padding: 18px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
min-height: 2.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-total-chip {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop Extra Large (≥ 1920px) */
|
||||||
|
@media (min-width: 1920px) {
|
||||||
|
.product-name {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-height: 1.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
max-width: 50px;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Móvil estándar (375px - 559px) */
|
||||||
|
@media (min-width: 375px) and (max-width: 559px) {
|
||||||
|
.product-image-container {
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet (560px - 959px) */
|
||||||
|
@media (min-width: 560px) and (max-width: 959px) {
|
||||||
|
.product-image-container {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-height: 2.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-total-chip {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
max-width: 58px;
|
||||||
|
min-width: 58px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop (≥ 960px) */
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.product-image-container {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-content {
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
min-height: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-size: 0.63rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-total-chip {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
height: 25px;
|
||||||
|
padding: 0 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-actions {
|
||||||
|
padding: 6px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
max-width: 58px;
|
||||||
|
min-width: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop Large (≥ 1280px) */
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.product-image-container {
|
||||||
|
height: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-content {
|
||||||
|
padding: 14px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 2rem;
|
||||||
|
min-height: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-total-chip {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop Extra Large (≥ 1920px) */
|
||||||
|
@media (min-width: 1920px) {
|
||||||
|
.product-image-container {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<v-card-title
|
<v-card-title
|
||||||
class="d-flex align-center cart-title"
|
class="d-flex align-center cart-title"
|
||||||
:class="{ 'cart-header-mobile': isMobile, 'cart-header-desktop': !isMobile }"
|
:class="{ 'cart-header-mobile': isMobile, 'cart-header-desktop': !isMobile }"
|
||||||
>
|
@click="isMobile && $emit('toggle-collapse')">
|
||||||
<!-- Icono del carrito - SIEMPRE VISIBLE -->
|
<!-- Icono del carrito - SIEMPRE VISIBLE -->
|
||||||
<v-icon class="mr-2">mdi-cart</v-icon>
|
<v-icon class="mr-2">mdi-cart</v-icon>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<div>
|
||||||
|
<NavBar />
|
||||||
<v-main>
|
<v-main>
|
||||||
<router-view />
|
<router-view />
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
</v-app>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
//
|
import NavBar from '@/components/NavBar.vue';
|
||||||
|
import AppFooter from '@/components/AppFooter.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
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>
|
||||||
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>
|
||||||
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>
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
//
|
import Login from '@/components/Login.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,14 +8,50 @@
|
|||||||
></div>
|
></div>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="9" lg="7" :class="{ 'pb-mobile-cart': isMobile }">
|
<v-col cols="12" md="10" lg="9" :class="{ 'pb-mobile-cart': isMobile }">
|
||||||
<v-card-title>
|
<v-sheet
|
||||||
<span class="headline">Catálogo</span>
|
class="page-header d-flex align-center pa-3 pa-sm-4 pa-md-6 mb-3 mb-sm-4 rounded-lg"
|
||||||
</v-card-title>
|
>
|
||||||
|
<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"
|
||||||
|
>mdi-store</v-icon
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex flex-column flex-sm-row align-start align-sm-center w-100 ga-2 ga-sm-4"
|
||||||
|
>
|
||||||
|
<div class="flex-shrink-0 d-none d-sm-block">
|
||||||
|
<h1
|
||||||
|
class="text-h6 text-sm-h5 text-md-h4 font-weight-bold text-primary mb-0"
|
||||||
|
>
|
||||||
|
Catálogo
|
||||||
|
</h1>
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||||
|
Explora y agrega productos a tu compra
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<v-spacer class="d-none d-sm-flex"></v-spacer>
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchQuery"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
label="Buscar producto..."
|
||||||
|
variant="solo-filled"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
class="search-field flex-grow-1 flex-sm-grow-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-sheet>
|
||||||
|
|
||||||
<!-- Controles de paginación superiores -->
|
<!-- Controles de paginación superiores -->
|
||||||
<PaginationControls
|
<PaginationControls
|
||||||
v-if="items.length > 0"
|
v-if="filteredItems.length > 0"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:total-pages="totalPages"
|
:total-pages="totalPages"
|
||||||
:items-per-page="itemsPerPage"
|
:items-per-page="itemsPerPage"
|
||||||
@@ -27,11 +63,16 @@
|
|||||||
position="top"
|
position="top"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Lista de productos paginados -->
|
<!-- Grid de productos paginados -->
|
||||||
<v-list-item
|
<v-row class="product-grid" v-if="paginatedItems.length > 0">
|
||||||
|
<v-col
|
||||||
v-for="item in paginatedItems"
|
v-for="item in paginatedItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="catalog-item"
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="6"
|
||||||
|
lg="4"
|
||||||
|
class="product-col"
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
:product="item"
|
:product="item"
|
||||||
@@ -41,7 +82,8 @@
|
|||||||
:updateQuantity="updateQuantity"
|
:updateQuantity="updateQuantity"
|
||||||
@add-to-cart="addToCart"
|
@add-to-cart="addToCart"
|
||||||
/>
|
/>
|
||||||
</v-list-item>
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<!-- Mensaje cuando no hay productos -->
|
<!-- Mensaje cuando no hay productos -->
|
||||||
<v-alert
|
<v-alert
|
||||||
@@ -52,10 +94,18 @@
|
|||||||
>
|
>
|
||||||
No hay productos disponibles en el catálogo
|
No hay productos disponibles en el catálogo
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
<v-alert
|
||||||
|
v-else-if="filteredItems.length === 0"
|
||||||
|
type="warning"
|
||||||
|
class="my-4"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
No se encontraron productos con ese nombre
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
<!-- Controles de paginación inferiores -->
|
<!-- Controles de paginación inferiores -->
|
||||||
<PaginationControls
|
<PaginationControls
|
||||||
v-if="items.length > 0"
|
v-if="filteredItems.length > 0"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:total-pages="totalPages"
|
:total-pages="totalPages"
|
||||||
:items-per-page="itemsPerPage"
|
:items-per-page="itemsPerPage"
|
||||||
@@ -68,10 +118,10 @@
|
|||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="3" lg="5">
|
<v-col cols="12" md="2" lg="3">
|
||||||
<div
|
<div
|
||||||
class="cart-sidebar"
|
class="cart-sidebar"
|
||||||
:class="{ collapsed: cartCollapsed && isMobile }"
|
:class="{ 'cart-is-collapsed': cartCollapsed && isMobile }"
|
||||||
>
|
>
|
||||||
<Cart
|
<Cart
|
||||||
:cart-items="cartItems"
|
:cart-items="cartItems"
|
||||||
@@ -188,6 +238,7 @@ import Cart from "@/components/catalog/Cart.vue";
|
|||||||
import PaginationControls from "@/components/catalog/PaginationControls.vue";
|
import PaginationControls from "@/components/catalog/PaginationControls.vue";
|
||||||
import { useCartStore } from "@/stores/cart";
|
import { useCartStore } from "@/stores/cart";
|
||||||
import { inject, ref, computed, onMounted, onUnmounted } from "vue";
|
import { inject, ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
|
import not_image_product from "@/assets/not_image_for_product.jpeg";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -225,6 +276,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
api: inject("api"),
|
api: inject("api"),
|
||||||
items: [],
|
items: [],
|
||||||
|
searchQuery: "",
|
||||||
// Paginación
|
// Paginación
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 20,
|
itemsPerPage: 20,
|
||||||
@@ -257,14 +309,22 @@ export default {
|
|||||||
cartCount() {
|
cartCount() {
|
||||||
return this.cartStore.cartCount;
|
return this.cartStore.cartCount;
|
||||||
},
|
},
|
||||||
|
// Búsqueda
|
||||||
|
filteredItems() {
|
||||||
|
if (!this.searchQuery) return this.items;
|
||||||
|
const query = this.searchQuery.toLowerCase();
|
||||||
|
return this.items.filter((item) =>
|
||||||
|
item.name.toLowerCase().includes(query),
|
||||||
|
);
|
||||||
|
},
|
||||||
// Paginación
|
// Paginación
|
||||||
paginatedItems() {
|
paginatedItems() {
|
||||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||||
const end = start + this.itemsPerPage;
|
const end = start + this.itemsPerPage;
|
||||||
return this.items.slice(start, end);
|
return this.filteredItems.slice(start, end);
|
||||||
},
|
},
|
||||||
totalPages() {
|
totalPages() {
|
||||||
return Math.ceil(this.items.length / this.itemsPerPage);
|
return Math.ceil(this.filteredItems.length / this.itemsPerPage);
|
||||||
},
|
},
|
||||||
paginationInfo() {
|
paginationInfo() {
|
||||||
const start = (this.currentPage - 1) * this.itemsPerPage + 1;
|
const start = (this.currentPage - 1) * this.itemsPerPage + 1;
|
||||||
@@ -275,7 +335,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
total: this.items.length,
|
total: this.filteredItems.length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Computed para total-visible dinámico y responsive (usado por ambos PaginationControls)
|
// Computed para total-visible dinámico y responsive (usado por ambos PaginationControls)
|
||||||
@@ -306,6 +366,11 @@ export default {
|
|||||||
this.loadItemsPerPagePreference();
|
this.loadItemsPerPagePreference();
|
||||||
this.fetchProducts();
|
this.fetchProducts();
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
searchQuery() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchProducts() {
|
fetchProducts() {
|
||||||
this.api
|
this.api
|
||||||
@@ -314,9 +379,7 @@ export default {
|
|||||||
this.items = data.map((product) => ({
|
this.items = data.map((product) => ({
|
||||||
...product,
|
...product,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
img:
|
img: (product.catalogue_images?.length > 0) ? product.catalogue_images[0] : (product.img || not_image_product),
|
||||||
product.img ||
|
|
||||||
`https://picsum.photos/300/200?random=${product.id}`,
|
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -388,7 +451,7 @@ export default {
|
|||||||
customer: 1,
|
customer: 1,
|
||||||
notes: "",
|
notes: "",
|
||||||
payment_method: "CASH",
|
payment_method: "CASH",
|
||||||
saleline_set: this.cartItems.map((item) => ({
|
catalogsaleline_set: this.cartItems.map((item) => ({
|
||||||
product: item.id,
|
product: item.id,
|
||||||
unit_price: item.price,
|
unit_price: item.price,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
@@ -407,7 +470,10 @@ export default {
|
|||||||
this.personalDataDialog = false;
|
this.personalDataDialog = false;
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: "/summary_purchase",
|
path: "/summary_purchase",
|
||||||
query: { id: parseInt(data.id) },
|
query: {
|
||||||
|
id: parseInt(data.id),
|
||||||
|
type: 'catalog'
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -446,7 +512,7 @@ export default {
|
|||||||
},
|
},
|
||||||
scrollToTop() {
|
scrollToTop() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const catalogHeader = this.$el?.querySelector(".headline");
|
const catalogHeader = this.$el?.querySelector(".page-header");
|
||||||
if (catalogHeader) {
|
if (catalogHeader) {
|
||||||
catalogHeader.scrollIntoView({
|
catalogHeader.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
@@ -468,29 +534,97 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.headline {
|
/* ============================================
|
||||||
font-weight: bold;
|
CABECERA STICKY CON BÚSQUEDA
|
||||||
font-size: 1.25rem;
|
============================================ */
|
||||||
|
.page-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 80px;
|
||||||
|
z-index: 5;
|
||||||
|
background: white !important;
|
||||||
|
color: #1565c0 !important;
|
||||||
|
overflow: visible;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === LISTADO DE PRODUCTOS === */
|
/* Mobile: Header sticky compensating for NavBar height, z-index menor que el cart */
|
||||||
.catalog-item {
|
@media (max-width: 959px) {
|
||||||
padding: 0;
|
.page-header {
|
||||||
margin-bottom: 12px;
|
top: 64px;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
z-index: 900;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-item:last-child {
|
@media (max-width: 559px) {
|
||||||
margin-bottom: 0;
|
.page-header {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile First: Estilos base (< 960px) */
|
/* Estilos profundos para el campo de búsqueda de Vuetify */
|
||||||
|
.page-header :deep(.v-field) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header :deep(.v-field:hover),
|
||||||
|
.page-header :deep(.v-field--focused) {
|
||||||
|
background-color: #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header :deep(.v-field__input) {
|
||||||
|
color: #1565c0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header :deep(.v-field__input::placeholder) {
|
||||||
|
color: rgba(0, 0, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 559px) {
|
||||||
|
.page-header .search-field :deep(.v-field__input) {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .search-field {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 560px) {
|
||||||
|
.page-header .search-field {
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.page-header .search-field {
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 460px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
CARRITO FLOTANTE (MOBILE FIRST)
|
||||||
|
============================================ */
|
||||||
.cart-sidebar {
|
.cart-sidebar {
|
||||||
|
--footer-height: 40px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: var(--footer-height);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cuando está colapsado en mobile, solo muestra el header (60px) */
|
||||||
|
.cart-sidebar.cart-is-collapsed {
|
||||||
|
transform: translateY(calc(100% - 60px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-backdrop {
|
.cart-backdrop {
|
||||||
@@ -501,7 +635,7 @@ export default {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
animation: fadeIn 0.3s ease-in-out;
|
animation: fadeIn 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
@@ -513,21 +647,74 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Padding para el contenido del catálogo en mobile */
|
/* Espacio inferior para evitar que productos queden ocultos bajo el cart */
|
||||||
.pb-mobile-cart {
|
.pb-mobile-cart {
|
||||||
padding-bottom: 76px !important; /* 60px sidebar + 16px margen */
|
padding-bottom: 100px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop (≥ 960px) - Sidebar sticky con scroll */
|
/* ============================================
|
||||||
/* Layout: 960-1023px = md breakpoint (75% productos / 25% cart) */
|
GRID DE PRODUCTOS
|
||||||
/* Layout: ≥1024px = lg breakpoint (60% productos / 40% cart) */
|
============================================ */
|
||||||
|
.product-grid {
|
||||||
|
margin: 0 -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-col {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Asegurar que las cards ocupen toda la altura */
|
||||||
|
.product-col :deep(.product-card) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile: mayor espaciado vertical */
|
||||||
|
@media (max-width: 559px) {
|
||||||
|
.product-grid {
|
||||||
|
margin: 0 -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-col {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet: espaciado medio */
|
||||||
|
@media (min-width: 560px) and (max-width: 959px) {
|
||||||
|
.product-grid {
|
||||||
|
margin: 0 -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-col {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop: espaciado óptimo */
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
.product-grid {
|
||||||
|
margin: 0 -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-col {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
DISEÑO DESKTOP (>= 960px)
|
||||||
|
============================================ */
|
||||||
@media (min-width: 960px) {
|
@media (min-width: 960px) {
|
||||||
.cart-sidebar {
|
.cart-sidebar {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 80px;
|
top: 96px;
|
||||||
z-index: auto;
|
z-index: 1;
|
||||||
max-height: calc(100vh - 100px);
|
max-height: calc(100vh - 120px);
|
||||||
overflow-y: visible;
|
overflow-y: auto;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-backdrop {
|
.cart-backdrop {
|
||||||
@@ -537,30 +724,11 @@ export default {
|
|||||||
.pb-mobile-cart {
|
.pb-mobile-cart {
|
||||||
padding-bottom: 16px !important;
|
padding-bottom: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-item {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resolución <560px - Mobile extra small */
|
|
||||||
@media (max-width: 559px) {
|
|
||||||
.headline {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.catalog-item {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resolución 560-959px - Tablet */
|
|
||||||
@media (min-width: 560px) and (max-width: 959px) {
|
|
||||||
.catalog-item {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
MODALES
|
||||||
|
============================================ */
|
||||||
.product-list-scroll {
|
.product-list-scroll {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<AdminPurchase v-if="authStore.isAdmin"/>
|
<Purchase v-if="authStore.isAdmin" :isAdmin="true" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script >
|
<script setup>
|
||||||
|
import Purchase from '@/components/Purchase.vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
export default {
|
|
||||||
setup() {
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
return { authStore };
|
|
||||||
},
|
definePage({
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Purchase />
|
<Purchase :isAdmin="false" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import Purchase from '@/components/Purchase.vue';
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true
|
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>
|
<template>
|
||||||
<SummaryPurchase :id="$route.query.id"/>
|
<SummaryPurchase :id="$route.query.id" :type="$route.query.type"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ const ADMIN_ROUTES = [
|
|||||||
'/cuadres_de_tarro',
|
'/cuadres_de_tarro',
|
||||||
'/compra_admin',
|
'/compra_admin',
|
||||||
'/cuadrar_tarro',
|
'/cuadrar_tarro',
|
||||||
|
'/admin/products',
|
||||||
|
'/admin/catalog-sales',
|
||||||
|
'/admin/catalogue-images',
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -35,7 +38,7 @@ router.beforeEach((to, from, next) => {
|
|||||||
|
|
||||||
if (requiresAuth && !isAuthenticated) {
|
if (requiresAuth && !isAuthenticated) {
|
||||||
next({ path: '/autenticarse', replace: true })
|
next({ path: '/autenticarse', replace: true })
|
||||||
} else if (requiresAdmin && !authStore.isAdmin) {
|
} else if (requiresAdmin && !authStore.isAdmin && authStore.user) {
|
||||||
next({ path: '/', replace: true })
|
next({ path: '/', replace: true })
|
||||||
} else {
|
} else {
|
||||||
next()
|
next()
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ class Api {
|
|||||||
return this.apiImplementation.getCustomers();
|
return this.apiImplementation.getCustomers();
|
||||||
}
|
}
|
||||||
|
|
||||||
getProducts() {
|
getProducts(active = 'all') {
|
||||||
return this.apiImplementation.getProducts();
|
return this.apiImplementation.getProducts(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProduct(productId, data) {
|
||||||
|
return this.apiImplementation.updateProduct(productId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPaymentMethods() {
|
getPaymentMethods() {
|
||||||
@@ -19,6 +23,10 @@ class Api {
|
|||||||
return this.apiImplementation.getSummaryPurchase(purchaseId);
|
return this.apiImplementation.getSummaryPurchase(purchaseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSummaryCatalogPurchase(purchaseId) {
|
||||||
|
return this.apiImplementation.getSummaryCatalogPurchase(purchaseId);
|
||||||
|
}
|
||||||
|
|
||||||
getPurchasesForReconciliation() {
|
getPurchasesForReconciliation() {
|
||||||
return this.apiImplementation.getPurchasesForReconciliation();
|
return this.apiImplementation.getPurchasesForReconciliation();
|
||||||
}
|
}
|
||||||
@@ -32,11 +40,11 @@ class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createPurchase(purchase) {
|
createPurchase(purchase) {
|
||||||
return this.apiImplementation.createCatalogPurchase(purchase);
|
return this.apiImplementation.createPurchase(purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
createCatalogPurchase(purchase) {
|
createCatalogPurchase(purchase) {
|
||||||
return this.apiImplementation.createPurchase(purchase);
|
return this.apiImplementation.createCatalogPurchase(purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
createReconciliationJar(reconciliation) {
|
createReconciliationJar(reconciliation) {
|
||||||
@@ -63,9 +71,33 @@ class Api {
|
|||||||
return this.apiImplementation.sendSalesToTryton();
|
return this.apiImplementation.sendSalesToTryton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendCatalogSalesToTryton() {
|
||||||
|
return this.apiImplementation.sendCatalogSalesToTryton();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCatalogSales() {
|
||||||
|
return this.apiImplementation.getCatalogSales();
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentUser() {
|
getCurrentUser() {
|
||||||
return this.apiImplementation.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;
|
export default Api;
|
||||||
|
|||||||
@@ -14,16 +14,31 @@ class DjangoApi {
|
|||||||
return http.post(url, payload).then((r) => r.data);
|
return http.post(url, payload).then((r) => r.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchRequest(url, payload) {
|
||||||
|
return http.patch(url, payload).then((r) => r.data);
|
||||||
|
}
|
||||||
|
|
||||||
getCustomers() {
|
getCustomers() {
|
||||||
const url = this.base + "/don_confiao/api/customers/";
|
const url = this.base + "/don_confiao/api/customers/";
|
||||||
return this.getRequest(url);
|
return this.getRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProducts() {
|
getProducts(active = 'all') {
|
||||||
const url = this.base + "/don_confiao/api/products/";
|
let url = this.base + "/don_confiao/api/products/";
|
||||||
|
|
||||||
|
// Agregar query parameter según filtro
|
||||||
|
if (active !== 'all') {
|
||||||
|
url += `?active=${active}`;
|
||||||
|
}
|
||||||
|
|
||||||
return this.getRequest(url);
|
return this.getRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateProduct(productId, data) {
|
||||||
|
const url = this.base + `/don_confiao/api/products/${productId}/`;
|
||||||
|
return this.patchRequest(url, data);
|
||||||
|
}
|
||||||
|
|
||||||
getPaymentMethods() {
|
getPaymentMethods() {
|
||||||
const url =
|
const url =
|
||||||
this.base + "/don_confiao/payment_methods/all/select_format";
|
this.base + "/don_confiao/payment_methods/all/select_format";
|
||||||
@@ -36,6 +51,12 @@ class DjangoApi {
|
|||||||
return this.getRequest(url);
|
return this.getRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSummaryCatalogPurchase(purchaseId) {
|
||||||
|
const url =
|
||||||
|
this.base + `/don_confiao/resumen_compra_catalogo_json/${purchaseId}`;
|
||||||
|
return this.getRequest(url);
|
||||||
|
}
|
||||||
|
|
||||||
getPurchasesForReconciliation() {
|
getPurchasesForReconciliation() {
|
||||||
const url = this.base + "/don_confiao/purchases/for_reconciliation";
|
const url = this.base + "/don_confiao/purchases/for_reconciliation";
|
||||||
return this.getRequest(url);
|
return this.getRequest(url);
|
||||||
@@ -95,10 +116,44 @@ class DjangoApi {
|
|||||||
return this.postRequest(url, {});
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentUser() {
|
getCurrentUser() {
|
||||||
const url = this.base + "/api/users/me/";
|
const url = this.base + "/api/users/me/";
|
||||||
return this.getRequest(url);
|
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;
|
export default DjangoApi;
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import AuthService from '@/services/auth';
|
import AuthService from "@/services/auth";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
baseURL: import.meta.env.VITE_DJANGO_BASE_URL,
|
baseURL: import.meta.env.VITE_DJANGO_BASE_URL,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
http.interceptors.request.use(
|
http.interceptors.request.use(
|
||||||
config => {
|
(config) => {
|
||||||
const token = AuthService.getAccessToken();
|
const token = AuthService.getAccessToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
error => Promise.reject(error)
|
(error) => Promise.reject(error),
|
||||||
);
|
);
|
||||||
|
|
||||||
http.interceptors.response.use(
|
http.interceptors.response.use(
|
||||||
response => response,
|
(response) => response,
|
||||||
async error => {
|
async (error) => {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
|
|
||||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
@@ -32,13 +33,13 @@ http.interceptors.response.use(
|
|||||||
return http.request(originalRequest);
|
return http.request(originalRequest);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
AuthService.logout();
|
AuthService.logout();
|
||||||
window.location.href = '/autenticarse';
|
router.push("/autenticarse");
|
||||||
return Promise.reject(refreshError);
|
return Promise.reject(refreshError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default http;
|
export default http;
|
||||||
|
|||||||
Reference in New Issue
Block a user