12 Commits

Author SHA1 Message Date
mono
523d0b9d08 #45 feat: show 'Resumen del pedido' and hide planilla alert for catalog orders 2026-07-11 19:52:34 -05:00
mono
0806731a1f #45 feat: rename checkout to 'Finalizar Pedido' and add contact info in checkout modal 2026-07-11 19:47:04 -05:00
mono
beb5e153f3 #45 feat: update login prompt message with contact info and icons 2026-07-11 19:39:36 -05:00
mono
3e19b3d272 #45 feat: hide total price when quantity is 0 2026-07-11 19:05:07 -05:00
mono
b21d3a0ca6 #45 fix: cart toggle button not working on mobile 2026-07-11 18:59:46 -05:00
mono
3029e6d4b9 #45 feat: clean catalog view on mobile devices 2026-07-11 18:56:45 -05:00
92483a110a Merge pull request 'fix/catalog-image-overlap' (#44) from fix/catalog-image-overlap into main
Reviewed-on: #44
2026-07-03 19:21:40 -05:00
mono
a89af0f0bb #fix: búsqueda insensible a tildes en el catálogo
Normaliza los strings eliminando diacríticos (NFD) antes de comparar,
para que 'cafe' encuentre 'café' y viceversa.
2026-07-03 19:06:40 -05:00
mono
b2a47c44dc #fix: scroll al inicio del grid al cambiar página en el catálogo
El método scrollToTop usaba scrollIntoView en un elemento sticky,
lo que no generaba desplazamiento. Ahora calcula la posición
absoluta del grid de productos y hace scroll suave.
2026-07-03 18:58:00 -05:00
mono
2c2082266e #fix: corregir solapamiento de imagen en tarjetas del catálogo
- Cambiar height: 100% por aspect-ratio: 1 en .product-image-container
- Eliminar media queries duplicados y obsoletos
- El contenedor ahora se ajusta al tamaño real del v-img
2026-07-03 18:52:33 -05:00
4b006aaa01 #44 feat: restringir acceso a compras para usuarios con rol publico 2026-06-22 12:50:14 -05:00
762fde2cd0 #44 feat: ocultar carrito y deshabilitar controles para usuarios no autenticados en catálogo 2026-06-22 11:46:50 -05:00
13 changed files with 344 additions and 259 deletions

View File

@@ -1,2 +1,3 @@
VITE_API_IMPLEMENTATION=django VITE_API_IMPLEMENTATION=django
VITE_DJANGO_BASE_URL=http://localhost:7000 VITE_DJANGO_BASE_URL=http://localhost:7000
VITE_CONTACT_PHONE=3023567797

View File

@@ -1,2 +1,3 @@
VITE_API_IMPLEMENTATION=django VITE_API_IMPLEMENTATION=django
VITE_DJANGO_BASE_URL=http://localhost:7000 VITE_DJANGO_BASE_URL=http://localhost:7000
VITE_CONTACT_PHONE=

View File

@@ -3,9 +3,11 @@ FROM node:20-alpine AS builder
ARG VITE_DJANGO_BASE_URL ARG VITE_DJANGO_BASE_URL
ARG VITE_API_IMPLEMENTATION ARG VITE_API_IMPLEMENTATION
ARG VITE_CONTACT_PHONE
ENV VITE_DJANGO_BASE_URL=$VITE_DJANGO_BASE_URL ENV VITE_DJANGO_BASE_URL=$VITE_DJANGO_BASE_URL
ENV VITE_API_IMPLEMENTATION=$VITE_API_IMPLEMENTATION ENV VITE_API_IMPLEMENTATION=$VITE_API_IMPLEMENTATION
ENV VITE_CONTACT_PHONE=$VITE_CONTACT_PHONE
WORKDIR /app WORKDIR /app

View File

@@ -59,6 +59,7 @@ set +a
BUILD_ARGS="--build-arg VITE_DJANGO_BASE_URL=$VITE_DJANGO_BASE_URL" BUILD_ARGS="--build-arg VITE_DJANGO_BASE_URL=$VITE_DJANGO_BASE_URL"
BUILD_ARGS="$BUILD_ARGS --build-arg VITE_API_IMPLEMENTATION=$VITE_API_IMPLEMENTATION" BUILD_ARGS="$BUILD_ARGS --build-arg VITE_API_IMPLEMENTATION=$VITE_API_IMPLEMENTATION"
BUILD_ARGS="$BUILD_ARGS --build-arg VITE_CONTACT_PHONE=$VITE_CONTACT_PHONE"
FULL_IMAGE_NAME="$GITEA_REGISTRY/$GITEA_USER/$IMAGE_NAME" FULL_IMAGE_NAME="$GITEA_REGISTRY/$GITEA_USER/$IMAGE_NAME"

View File

@@ -1,39 +1,62 @@
<template> <template>
<v-footer height="40" app> <div>
<a <!-- Footer completo: visible siempre en desktop, toggle en mobile -->
v-for="item in items" <v-footer
:key="item.title" v-if="expanded || !isMobile"
:href="item.href" height="40"
:title="item.title" app
class="d-inline-block mx-2 social-link" class="app-footer"
rel="noopener noreferrer" :class="{ 'footer-overlay': isMobile }"
target="_blank"
> >
<v-icon
:icon="item.icon"
:size="item.icon === '$vuetify' ? 24 : 16"
/>
</a>
<div
class="text-caption text-disabled"
style="position: absolute; right: 16px;"
>
&copy; 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
—
<a <a
class="text-decoration-none on-surface" v-for="item in items"
href="https://vuetifyjs.com/about/licensing/" :key="item.title"
:href="item.href"
:title="item.title"
class="d-inline-block mx-2 social-link"
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
MIT License <v-icon
:icon="item.icon"
:size="item.icon === '$vuetify' ? 24 : 16"
/>
</a> </a>
</div>
</v-footer> <div
class="text-caption text-disabled"
style="position: absolute; right: 16px;"
>
&copy; 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
—
<a
class="text-decoration-none on-surface"
href="https://vuetifyjs.com/about/licensing/"
rel="noopener noreferrer"
target="_blank"
>
MIT License
</a>
</div>
</v-footer>
<!-- Botón flotante: solo en mobile -->
<v-btn
v-if="isMobile"
icon
size="small"
class="footer-toggle-btn"
:color="expanded ? 'primary' : undefined"
@click="expanded = !expanded"
>
<v-icon :icon="expanded ? 'mdi-close' : '$vuetify'" :size="expanded ? 20 : 24" />
</v-btn>
</div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
const items = [ const items = [
{ {
title: 'Vuetify Documentation', title: 'Vuetify Documentation',
@@ -65,15 +88,48 @@
icon: `mdi-reddit`, icon: `mdi-reddit`,
href: 'https://reddit.com/r/vuetifyjs', href: 'https://reddit.com/r/vuetifyjs',
}, },
] ];
const expanded = ref(false);
const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1200);
const isMobile = computed(() => windowWidth.value < 960);
const onResize = () => {
windowWidth.value = window.innerWidth;
};
onMounted(() => {
window.addEventListener('resize', onResize);
});
onUnmounted(() => {
window.removeEventListener('resize', onResize);
});
</script> </script>
<style scoped lang="sass"> <style scoped lang="sass">
.social-link :deep(.v-icon) .social-link :deep(.v-icon)
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity)) color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
text-decoration: none text-decoration: none
transition: .2s ease-in-out transition: .2s ease-in-out
&:hover &:hover
color: rgba(25, 118, 210, 1) color: rgba(25, 118, 210, 1)
/* Botón flotante para mobile */
.footer-toggle-btn
position: fixed
bottom: 12px
right: 12px
z-index: 2000
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25)
/* Overlay del footer en mobile */
.footer-overlay
position: fixed !important
bottom: 0
left: 0
right: 0
z-index: 1999
</style> </style>

View File

@@ -1,7 +1,13 @@
<template> <template>
<v-dialog v-model="show" max-width="400"> <v-dialog v-model="show" max-width="400">
<v-card> <v-card>
<v-card-title class="headline">Iniciar sesión</v-card-title> <v-card-title class="headline d-flex align-center">
Iniciar sesión
<v-spacer />
<v-btn icon size="small" variant="text" @click="show = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text> <v-card-text>
<v-form ref="form" @submit.prevent="onSubmit"> <v-form ref="form" @submit.prevent="onSubmit">
<v-text-field <v-text-field

View File

@@ -54,7 +54,7 @@
nav nav
> >
<v-list-item <v-list-item
v-for="item in menuItems" v-for="item in filteredMenuItems"
:key="item.title" :key="item.title"
:title="item.title" :title="item.title"
:prepend-icon="item.icon" :prepend-icon="item.icon"
@@ -120,7 +120,13 @@
computed: { computed: {
isAdmin() { isAdmin() {
return this.user?.role === 'administrator'; return this.user?.role === 'administrator';
} },
filteredMenuItems() {
if (this.user?.role === 'publico') {
return this.menuItems.filter(item => item.route !== '/comprar');
}
return this.menuItems;
},
}, },
mounted() { mounted() {
this.checkAuth(); this.checkAuth();

View File

@@ -8,7 +8,7 @@
</v-container> </v-container>
<v-container v-show="id"> <v-container v-show="id">
<v-toolbar> <v-toolbar>
<v-toolbar-title> Resumen de la compra {{ id }}</v-toolbar-title> <v-toolbar-title> {{ type === 'catalog' ? 'Resumen del pedido' : 'Resumen de la compra' }} {{ id }}</v-toolbar-title>
</v-toolbar> </v-toolbar>
<v-list> <v-list>
<v-list-item> <v-list-item>
@@ -39,7 +39,7 @@
{{ currencyFormat(calculateSubtotal(item.unit_price, item.quantity)) }} {{ currencyFormat(calculateSubtotal(item.unit_price, item.quantity)) }}
</template> </template>
</v-data-table-virtual> </v-data-table-virtual>
<v-alert type="info" class="my-4"> <v-alert v-if="type !== 'catalog'" type="info" class="my-4">
Recuerda adicionar a la planilla física lo siguiente Recuerda adicionar a la planilla física lo siguiente
<v-data-table <v-data-table
:headers="headersTemplate" :headers="headersTemplate"

View File

@@ -47,7 +47,7 @@
</div> </div>
<!-- Precio Total --> <!-- Precio Total -->
<div class="price-row"> <div v-if="product.quantity > 0" class="price-row">
<span class="price-label text-caption">Precio total</span> <span class="price-label text-caption">Precio total</span>
<v-chip <v-chip
color="success" color="success"
@@ -63,50 +63,64 @@
<!-- Footer con Controles de Cantidad --> <!-- Footer con Controles de Cantidad -->
<v-card-actions class="product-actions pa-2 pb-3 justify-center"> <v-card-actions class="product-actions pa-2 pb-3 justify-center">
<div class="quantity-controls"> <template v-if="disabled">
<v-btn <v-btn
icon color="primary"
size="small"
variant="tonal" variant="tonal"
color="error" prepend-icon="mdi-cart-plus"
class="qty-btn" class="login-add-btn"
@click="decrease(product)" @click="$emit('request-login')"
:disabled="product.quantity === 0"
> >
<v-icon size="20">mdi-minus</v-icon> Agregar
</v-btn> </v-btn>
</template>
<template v-else>
<div class="quantity-controls">
<v-btn
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-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 mx-1" class="quantity-input mx-1"
variant="solo-filled" variant="solo-filled"
density="compact" density="compact"
hide-details hide-details
single-line single-line
flat flat
aria-label="Cantidad" aria-label="Cantidad"
@input="handleQuantityChange" @input="handleQuantityChange"
/> />
<v-btn <v-btn
icon icon
size="small" size="small"
variant="tonal" variant="tonal"
color="success" color="success"
class="qty-btn" class="qty-btn"
@click="handleIncrease" @click="handleIncrease"
> >
<v-icon size="20">mdi-plus</v-icon> <v-icon size="20">mdi-plus</v-icon>
</v-btn> </v-btn>
</div> </div>
</template>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>
<script> <script>
export default { export default {
emits: ['add-to-cart', 'request-login'],
props: { props: {
product: { product: {
type: Object, type: Object,
@@ -128,6 +142,10 @@ export default {
type: Function, type: Function,
required: true, required: true,
}, },
disabled: {
type: Boolean,
default: false,
},
}, },
methods: { methods: {
handleIncrease() { handleIncrease() {
@@ -170,7 +188,6 @@ export default {
.product-image-container { .product-image-container {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%;
overflow: hidden; overflow: hidden;
background: linear-gradient(135deg, #fafafa 0%, #ffffff 100%); background: linear-gradient(135deg, #fafafa 0%, #ffffff 100%);
border-bottom: 1px solid rgba(0, 0, 0, 0.05); border-bottom: 1px solid rgba(0, 0, 0, 0.05);
@@ -307,6 +324,13 @@ export default {
RESPONSIVE BREAKPOINTS RESPONSIVE BREAKPOINTS
============================================ */ ============================================ */
/* Botón de agregar (no autenticado) */
.login-add-btn {
width: 100%;
font-weight: 600;
letter-spacing: 0.5px;
}
/* Móvil pequeño (< 375px) */ /* Móvil pequeño (< 375px) */
@media (max-width: 374px) { @media (max-width: 374px) {
.product-content { .product-content {
@@ -330,143 +354,6 @@ export default {
/* Móvil estándar (375px - 559px) */ /* Móvil estándar (375px - 559px) */
@media (min-width: 375px) and (max-width: 559px) { @media (min-width: 375px) and (max-width: 559px) {
.product-name {
font-size: 0.95rem;
}
.price-value {
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 { .product-name {
font-size: 0.85rem; font-size: 0.85rem;
} }
@@ -478,10 +365,6 @@ export default {
/* Tablet (560px - 959px) */ /* Tablet (560px - 959px) */
@media (min-width: 560px) and (max-width: 959px) { @media (min-width: 560px) and (max-width: 959px) {
.product-image-container {
height: 150px;
}
.product-content { .product-content {
padding: 10px; padding: 10px;
} }
@@ -512,10 +395,6 @@ export default {
/* Desktop (≥ 960px) */ /* Desktop (≥ 960px) */
@media (min-width: 960px) { @media (min-width: 960px) {
.product-image-container {
height: 160px;
}
.product-content { .product-content {
padding: 12px 10px; padding: 12px 10px;
} }
@@ -557,10 +436,6 @@ export default {
/* Desktop Large (≥ 1280px) */ /* Desktop Large (≥ 1280px) */
@media (min-width: 1280px) { @media (min-width: 1280px) {
.product-image-container {
height: 170px;
}
.product-content { .product-content {
padding: 14px 12px; padding: 14px 12px;
} }
@@ -582,10 +457,6 @@ export default {
/* Desktop Extra Large (≥ 1920px) */ /* Desktop Extra Large (≥ 1920px) */
@media (min-width: 1920px) { @media (min-width: 1920px) {
.product-image-container {
height: 180px;
}
.product-name { .product-name {
font-size: 1.05rem; font-size: 1.05rem;
min-height: 2.5rem; min-height: 2.5rem;

View File

@@ -47,7 +47,7 @@
icon icon
size="small" size="small"
variant="text" variant="text"
@click="$emit('toggle-collapse')" @click.stop="$emit('toggle-collapse')"
:title="isCollapsed ? 'Expandir carrito' : 'Contraer carrito'" :title="isCollapsed ? 'Expandir carrito' : 'Contraer carrito'"
> >
<v-icon>{{ isCollapsed ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon> <v-icon>{{ isCollapsed ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
@@ -133,7 +133,7 @@
<v-card-actions v-if="cartItems.length > 0" class="cart-checkout-section"> <v-card-actions v-if="cartItems.length > 0" class="cart-checkout-section">
<v-btn color="primary" block @click="$emit('checkout')"> <v-btn color="primary" block @click="$emit('checkout')">
Finalizar Compra Finalizar Pedido
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</div> </div>

View File

@@ -2,7 +2,7 @@
<v-container fluid> <v-container fluid>
<!-- Backdrop para mobile cuando el carrito está expandido --> <!-- Backdrop para mobile cuando el carrito está expandido -->
<div <div
v-if="isMobile && !cartCollapsed" v-if="isMobile && !cartCollapsed && isAuthenticated"
class="cart-backdrop" class="cart-backdrop"
@click="cartCollapsed = true" @click="cartCollapsed = true"
></div> ></div>
@@ -49,20 +49,6 @@
</div> </div>
</v-sheet> </v-sheet>
<!-- Controles de paginación superiores -->
<PaginationControls
v-if="filteredItems.length > 0"
:current-page="currentPage"
:total-pages="totalPages"
:items-per-page="itemsPerPage"
:items-per-page-options="itemsPerPageOptions"
:pagination-info="paginationInfo"
:total-visible-pages="totalVisiblePages"
@page-change="handlePageChange"
@items-per-page-change="handleItemsPerPageChange"
position="top"
/>
<!-- Grid de productos paginados --> <!-- Grid de productos paginados -->
<v-row class="product-grid" v-if="paginatedItems.length > 0"> <v-row class="product-grid" v-if="paginatedItems.length > 0">
<v-col <v-col
@@ -80,7 +66,9 @@
:decrease="decrease" :decrease="decrease"
:currency="currency" :currency="currency"
:updateQuantity="updateQuantity" :updateQuantity="updateQuantity"
:disabled="!isAuthenticated"
@add-to-cart="addToCart" @add-to-cart="addToCart"
@request-login="showLogin"
/> />
</v-col> </v-col>
</v-row> </v-row>
@@ -124,6 +112,7 @@
:class="{ 'cart-is-collapsed': cartCollapsed && isMobile }" :class="{ 'cart-is-collapsed': cartCollapsed && isMobile }"
> >
<Cart <Cart
v-if="isAuthenticated"
:cart-items="cartItems" :cart-items="cartItems"
:currency="currency" :currency="currency"
:is-collapsed="cartCollapsed" :is-collapsed="cartCollapsed"
@@ -134,6 +123,63 @@
@update-quantity="updateCartQuantity" @update-quantity="updateCartQuantity"
@toggle-collapse="toggleCart" @toggle-collapse="toggleCart"
/> />
<v-card v-else-if="!isMobile || showLoginPrompt" class="login-prompt-card pa-4 text-center">
<v-btn
icon
size="x-small"
variant="text"
class="login-prompt-close"
@click="showLoginPrompt = false"
>
<v-icon size="18">mdi-close</v-icon>
</v-btn>
<v-icon size="48" color="primary" class="mb-2">mdi-cart-lock</v-icon>
<p class="text-body-2 text-medium-emphasis mb-3">
Para hacer pedidos debes de tener usuario y clave, si no lo tienes comunicate con nosotros al
<strong class="text-primary">{{ contactPhone }}</strong>
para solicitarlo. Esto es necesario porque no podemos procesar tus pedidos todos los días del mes.
Comunicate con nosotros para contarte todos los detalles y que puedas apoyar este proceso.
</p>
<div class="d-flex justify-center ga-2 mb-3">
<v-btn
icon
size="small"
color="primary"
variant="tonal"
:href="'tel:' + contactPhone"
>
<v-icon size="18">mdi-phone</v-icon>
</v-btn>
<v-btn
icon
size="small"
color="success"
variant="tonal"
:href="'https://wa.me/' + contactPhone"
target="_blank"
>
<v-icon size="18">mdi-whatsapp</v-icon>
</v-btn>
<v-btn
icon
size="small"
color="info"
variant="tonal"
:href="'https://t.me/+57' + contactPhone"
target="_blank"
>
<v-icon size="18">mdi-send</v-icon>
</v-btn>
</div>
<v-btn
color="primary"
variant="tonal"
prepend-icon="mdi-login"
@click="openLoginDialog"
>
Iniciar Sesión
</v-btn>
</v-card>
</div> </div>
</v-col> </v-col>
</v-row> </v-row>
@@ -141,7 +187,7 @@
<!-- Modal 1: Confirmación de productos --> <!-- Modal 1: Confirmación de productos -->
<v-dialog v-model="checkoutDialog" max-width="600" persistent> <v-dialog v-model="checkoutDialog" max-width="600" persistent>
<v-card> <v-card>
<v-card-title class="headline">Confirmar Compra</v-card-title> <v-card-title class="headline">Confirmar Pedido</v-card-title>
<v-card-text> <v-card-text>
<v-list v-if="cartItems.length > 0" class="product-list-scroll"> <v-list v-if="cartItems.length > 0" class="product-list-scroll">
<v-list-item v-for="item in cartItems" :key="item.id"> <v-list-item v-for="item in cartItems" :key="item.id">
@@ -176,7 +222,7 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- Modal 2: Datos personales --> <!-- Modal 2: Datos personales + aviso de coordinación -->
<v-dialog v-model="personalDataDialog" max-width="500" persistent> <v-dialog v-model="personalDataDialog" max-width="500" persistent>
<v-card> <v-card>
<v-card-title class="headline">Datos de Contacto</v-card-title> <v-card-title class="headline">Datos de Contacto</v-card-title>
@@ -213,6 +259,48 @@
variant="outlined" variant="outlined"
></v-select> ></v-select>
</v-form> </v-form>
<v-divider class="my-3"></v-divider>
<v-alert type="info" variant="tonal" class="mb-0">
<div class="text-body-2 mb-2">
Para coordinar la entrega de tu pedido escríbenos:
</div>
<div class="d-flex ga-2">
<v-btn
icon
size="x-small"
color="primary"
variant="tonal"
:href="'tel:' + contactPhone"
>
<v-icon size="16">mdi-phone</v-icon>
</v-btn>
<v-btn
icon
size="x-small"
color="success"
variant="tonal"
:href="'https://wa.me/57' + contactPhone"
target="_blank"
>
<v-icon size="16">mdi-whatsapp</v-icon>
</v-btn>
<v-btn
icon
size="x-small"
color="info"
variant="tonal"
:href="'https://t.me/+57' + contactPhone"
target="_blank"
>
<v-icon size="16">mdi-send</v-icon>
</v-btn>
<span class="text-body-2 font-weight-bold ml-1 d-flex align-center">
{{ contactPhone }}
</span>
</div>
</v-alert>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@@ -224,11 +312,17 @@
:loading="isSubmitting" :loading="isSubmitting"
:disabled="isSubmitting" :disabled="isSubmitting"
> >
Finalizar Compra Finalizar Pedido
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- Login Dialog -->
<LoginDialog
ref="loginDialogRef"
@login-success="onLoginSuccess"
/>
</v-container> </v-container>
</template> </template>
@@ -236,7 +330,9 @@
import Card from "@/components/catalog/Card.vue"; import Card from "@/components/catalog/Card.vue";
import Cart from "@/components/catalog/Cart.vue"; import Cart from "@/components/catalog/Cart.vue";
import PaginationControls from "@/components/catalog/PaginationControls.vue"; import PaginationControls from "@/components/catalog/PaginationControls.vue";
import LoginDialog from "@/components/LoginDialog.vue";
import { useCartStore } from "@/stores/cart"; import { useCartStore } from "@/stores/cart";
import { useAuthStore } from "@/stores/auth";
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"; import not_image_product from "@/assets/not_image_for_product.jpeg";
@@ -245,10 +341,13 @@ export default {
Card, Card,
Cart, Cart,
PaginationControls, PaginationControls,
LoginDialog,
}, },
setup() { setup() {
const cartStore = useCartStore(); const cartStore = useCartStore();
const cartCollapsed = ref(false); // Cambiado a false para que inicie expandido en desktop const authStore = useAuthStore();
const cartCollapsed = ref(false);
const showLoginPrompt = ref(false);
const windowWidth = ref(window.innerWidth); const windowWidth = ref(window.innerWidth);
const isMobile = computed(() => windowWidth.value < 960); // Cambiado de 680 a 960 const isMobile = computed(() => windowWidth.value < 960); // Cambiado de 680 a 960
@@ -267,7 +366,9 @@ export default {
return { return {
cartStore, cartStore,
authStore,
cartCollapsed, cartCollapsed,
showLoginPrompt,
isMobile, isMobile,
windowWidth, windowWidth,
}; };
@@ -309,12 +410,19 @@ export default {
cartCount() { cartCount() {
return this.cartStore.cartCount; return this.cartStore.cartCount;
}, },
isAuthenticated() {
return this.authStore.isAuthenticated;
},
contactPhone() {
return import.meta.env.VITE_CONTACT_PHONE || '';
},
// Búsqueda // Búsqueda
filteredItems() { filteredItems() {
if (!this.searchQuery) return this.items; if (!this.searchQuery) return this.items;
const query = this.searchQuery.toLowerCase(); const normalize = (s) => s.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
const query = normalize(this.searchQuery);
return this.items.filter((item) => return this.items.filter((item) =>
item.name.toLowerCase().includes(query), normalize(item.name).includes(query),
); );
}, },
// Paginación // Paginación
@@ -440,7 +548,6 @@ export default {
}, },
async onSubmitPurchase() { async onSubmitPurchase() {
const form = this.$refs.personalForm; const form = this.$refs.personalForm;
if (form) { if (form) {
const { valid } = await form.validate(); const { valid } = await form.validate();
if (!valid) return; if (!valid) return;
@@ -462,7 +569,6 @@ export default {
customer_phone: this.customerPhone, customer_phone: this.customerPhone,
pickup_method: this.pickupMethod, pickup_method: this.pickupMethod,
}; };
this.api this.api
.createCatalogPurchase(payload) .createCatalogPurchase(payload)
.then((data) => { .then((data) => {
@@ -511,14 +617,27 @@ export default {
} }
}, },
scrollToTop() { scrollToTop() {
this.$nextTick(() => { const grid = this.$el?.querySelector(".product-grid");
const catalogHeader = this.$el?.querySelector(".page-header"); if (grid) {
if (catalogHeader) { const top = grid.getBoundingClientRect().top + window.scrollY - 16;
catalogHeader.scrollIntoView({ window.scrollTo({ top, behavior: "smooth" });
behavior: "smooth", } else {
block: "start", window.scrollTo({ top: 0, behavior: "smooth" });
}); }
} },
openLoginDialog() {
this.$refs.loginDialogRef.open();
},
showLogin() {
this.showLoginPrompt = true;
if (this.isMobile) {
this.cartCollapsed = false;
}
},
onLoginSuccess() {
this.showLoginPrompt = false;
this.api.getCurrentUser().then((user) => {
this.authStore.setUser(user);
}); });
}, },
currency(val) { currency(val) {
@@ -726,6 +845,23 @@ export default {
} }
} }
/* ============================================
LOGIN PROMPT CARD
============================================ */
.login-prompt-card {
border: 2px dashed rgba(0, 0, 0, 0.12);
background: #fafafa !important;
position: relative;
overflow-wrap: break-word;
word-break: break-word;
}
.login-prompt-close {
position: absolute;
top: 8px;
right: 8px;
}
/* ============================================ /* ============================================
MODALES MODALES
============================================ */ ============================================ */

View File

@@ -11,6 +11,8 @@ import { setupLayouts } from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes' import { routes } from 'vue-router/auto-routes'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
const PUBLICO_RESTRICTED = ['/comprar']
const ADMIN_ROUTES = [ const ADMIN_ROUTES = [
'/sincronizar_clientes_tryton', '/sincronizar_clientes_tryton',
'/sincronizar_ventas_tryton', '/sincronizar_ventas_tryton',
@@ -40,6 +42,8 @@ router.beforeEach((to, from, next) => {
next({ path: '/autenticarse', replace: true }) next({ path: '/autenticarse', replace: true })
} else if (requiresAdmin && !authStore.isAdmin && authStore.user) { } else if (requiresAdmin && !authStore.isAdmin && authStore.user) {
next({ path: '/', replace: true }) next({ path: '/', replace: true })
} else if (authStore.user?.role === 'publico' && PUBLICO_RESTRICTED.includes(to.path)) {
next({ path: '/catalog', replace: true })
} else { } else {
next() next()
} }

View File

@@ -6,6 +6,7 @@ export const useAuthStore = defineStore('auth', {
}), }),
getters: { getters: {
isAdmin: (state) => state.user?.role === 'administrator', isAdmin: (state) => state.user?.role === 'administrator',
isPublico: (state) => state.user?.role === 'publico',
isAuthenticated: (state) => !!state.user isAuthenticated: (state) => !!state.user
}, },
actions: { actions: {