7 Commits

Author SHA1 Message Date
7beac90c05 feat: mejora selector de productos con autocomplete y estética del modal en gestión de imágenes de catálogo 2026-07-11 13:52:35 -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
6 changed files with 101 additions and 181 deletions

View File

@@ -80,55 +80,67 @@
</v-col>
</v-row>
<v-dialog v-model="dialog.show" max-width="500" persistent>
<v-card>
<v-card-title>
<v-dialog v-model="dialog.show" max-width="520" persistent>
<v-card elevation="2" rounded="lg">
<v-card-title class="text-h6 font-weight-bold pa-6 pb-0">
<v-icon :icon="dialog.isEdit ? 'mdi-pencil' : 'mdi-plus-circle-outline'" class="mr-2" color="primary" />
{{ dialog.isEdit ? 'Editar' : 'Agregar' }} Imagen de Catálogo
</v-card-title>
<v-card-text>
<v-divider class="mt-3" />
<v-card-text class="pa-6">
<v-form ref="formRef">
<v-select
<v-label class="font-weight-medium mb-1 d-block">Producto</v-label>
<v-autocomplete
v-model="form.product"
:items="productOptions"
label="Producto"
placeholder="Buscar producto…"
:rules="[(v) => !!v || 'Seleccione un producto']"
item-title="name"
item-value="id"
variant="outlined"
:disabled="dialog.isEdit"
class="mb-3"
class="mb-5"
clearable
prepend-inner-icon="mdi-package-variant-closed"
density="comfortable"
/>
<v-label class="font-weight-medium mb-1 d-block">Imagen</v-label>
<v-file-input
ref="fileInputRef"
label="Imagen"
accept="image/*"
:rules="[(v) => !!v || 'Seleccione una imagen']"
variant="outlined"
prepend-icon="mdi-camera"
class="mb-3"
prepend-inner-icon="mdi-image-outline"
class="mb-5"
@update:model-value="onFileSelected"
density="comfortable"
hide-details="auto"
/>
<v-img
v-if="form.preview"
:src="form.preview"
max-height="200"
max-height="220"
contain
class="mb-3 rounded"
class="rounded-lg border mt-2"
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer />
<v-btn variant="text" @click="closeDialog">Cancelar</v-btn>
<v-btn variant="tonal" @click="closeDialog">Cancelar</v-btn>
<v-btn
color="primary"
variant="elevated"
:loading="submitting"
:disabled="submitting"
@click="submitForm"
class="ml-2"
>
<v-icon start :icon="dialog.isEdit ? 'mdi-content-save' : 'mdi-upload'" />
{{ dialog.isEdit ? 'Actualizar' : 'Agregar' }}
</v-btn>
</v-card-actions>

View File

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

View File

@@ -71,7 +71,7 @@
color="error"
class="qty-btn"
@click="decrease(product)"
:disabled="product.quantity === 0"
:disabled="disabled || product.quantity === 0"
>
<v-icon size="20">mdi-minus</v-icon>
</v-btn>
@@ -87,6 +87,7 @@
single-line
flat
aria-label="Cantidad"
:disabled="disabled"
@input="handleQuantityChange"
/>
@@ -97,6 +98,7 @@
color="success"
class="qty-btn"
@click="handleIncrease"
:disabled="disabled"
>
<v-icon size="20">mdi-plus</v-icon>
</v-btn>
@@ -128,6 +130,10 @@ export default {
type: Function,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
},
methods: {
handleIncrease() {
@@ -170,7 +176,6 @@ export default {
.product-image-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: linear-gradient(135deg, #fafafa 0%, #ffffff 100%);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
@@ -330,143 +335,6 @@ export default {
/* Móvil estándar (375px - 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 {
font-size: 0.85rem;
}
@@ -478,10 +346,6 @@ export default {
/* Tablet (560px - 959px) */
@media (min-width: 560px) and (max-width: 959px) {
.product-image-container {
height: 150px;
}
.product-content {
padding: 10px;
}
@@ -512,10 +376,6 @@ export default {
/* Desktop (≥ 960px) */
@media (min-width: 960px) {
.product-image-container {
height: 160px;
}
.product-content {
padding: 12px 10px;
}
@@ -557,10 +417,6 @@ export default {
/* Desktop Large (≥ 1280px) */
@media (min-width: 1280px) {
.product-image-container {
height: 170px;
}
.product-content {
padding: 14px 12px;
}
@@ -582,10 +438,6 @@ export default {
/* Desktop Extra Large (≥ 1920px) */
@media (min-width: 1920px) {
.product-image-container {
height: 180px;
}
.product-name {
font-size: 1.05rem;
min-height: 2.5rem;

View File

@@ -2,7 +2,7 @@
<v-container fluid>
<!-- Backdrop para mobile cuando el carrito está expandido -->
<div
v-if="isMobile && !cartCollapsed"
v-if="isMobile && !cartCollapsed && isAuthenticated"
class="cart-backdrop"
@click="cartCollapsed = true"
></div>
@@ -80,6 +80,7 @@
:decrease="decrease"
:currency="currency"
:updateQuantity="updateQuantity"
:disabled="!isAuthenticated"
@add-to-cart="addToCart"
/>
</v-col>
@@ -124,6 +125,7 @@
:class="{ 'cart-is-collapsed': cartCollapsed && isMobile }"
>
<Cart
v-if="isAuthenticated"
:cart-items="cartItems"
:currency="currency"
:is-collapsed="cartCollapsed"
@@ -134,6 +136,20 @@
@update-quantity="updateCartQuantity"
@toggle-collapse="toggleCart"
/>
<v-card v-else class="login-prompt-card pa-4 text-center">
<v-icon size="48" color="primary" class="mb-2">mdi-cart-lock</v-icon>
<p class="text-body-2 text-medium-emphasis mb-3">
Inicia sesión para agregar productos al carrito y finalizar tu compra
</p>
<v-btn
color="primary"
variant="tonal"
prepend-icon="mdi-login"
@click="openLoginDialog"
>
Iniciar Sesión
</v-btn>
</v-card>
</div>
</v-col>
</v-row>
@@ -229,6 +245,12 @@
</v-card-actions>
</v-card>
</v-dialog>
<!-- Login Dialog -->
<LoginDialog
ref="loginDialogRef"
@login-success="onLoginSuccess"
/>
</v-container>
</template>
@@ -236,7 +258,9 @@
import Card from "@/components/catalog/Card.vue";
import Cart from "@/components/catalog/Cart.vue";
import PaginationControls from "@/components/catalog/PaginationControls.vue";
import LoginDialog from "@/components/LoginDialog.vue";
import { useCartStore } from "@/stores/cart";
import { useAuthStore } from "@/stores/auth";
import { inject, ref, computed, onMounted, onUnmounted } from "vue";
import not_image_product from "@/assets/not_image_for_product.jpeg";
@@ -245,9 +269,11 @@ export default {
Card,
Cart,
PaginationControls,
LoginDialog,
},
setup() {
const cartStore = useCartStore();
const authStore = useAuthStore();
const cartCollapsed = ref(false); // Cambiado a false para que inicie expandido en desktop
const windowWidth = ref(window.innerWidth);
@@ -267,6 +293,7 @@ export default {
return {
cartStore,
authStore,
cartCollapsed,
isMobile,
windowWidth,
@@ -309,12 +336,16 @@ export default {
cartCount() {
return this.cartStore.cartCount;
},
isAuthenticated() {
return this.authStore.isAuthenticated;
},
// Búsqueda
filteredItems() {
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) =>
item.name.toLowerCase().includes(query),
normalize(item.name).includes(query),
);
},
// Paginación
@@ -511,14 +542,20 @@ export default {
}
},
scrollToTop() {
this.$nextTick(() => {
const catalogHeader = this.$el?.querySelector(".page-header");
if (catalogHeader) {
catalogHeader.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
const grid = this.$el?.querySelector(".product-grid");
if (grid) {
const top = grid.getBoundingClientRect().top + window.scrollY - 16;
window.scrollTo({ top, behavior: "smooth" });
} else {
window.scrollTo({ top: 0, behavior: "smooth" });
}
},
openLoginDialog() {
this.$refs.loginDialogRef.open();
},
onLoginSuccess() {
this.api.getCurrentUser().then((user) => {
this.authStore.setUser(user);
});
},
currency(val) {
@@ -726,6 +763,14 @@ export default {
}
}
/* ============================================
LOGIN PROMPT CARD
============================================ */
.login-prompt-card {
border: 2px dashed rgba(0, 0, 0, 0.12);
background: #fafafa !important;
}
/* ============================================
MODALES
============================================ */

View File

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

View File

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