6 Commits

11 changed files with 286 additions and 121 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

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

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

@@ -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,52 +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="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"
:disabled="disabled" @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"
:disabled="disabled" >
> <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,
@@ -312,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 {

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

@@ -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
@@ -82,6 +68,7 @@
:updateQuantity="updateQuantity" :updateQuantity="updateQuantity"
:disabled="!isAuthenticated" :disabled="!isAuthenticated"
@add-to-cart="addToCart" @add-to-cart="addToCart"
@request-login="showLogin"
/> />
</v-col> </v-col>
</v-row> </v-row>
@@ -136,11 +123,54 @@
@update-quantity="updateCartQuantity" @update-quantity="updateCartQuantity"
@toggle-collapse="toggleCart" @toggle-collapse="toggleCart"
/> />
<v-card v-else class="login-prompt-card pa-4 text-center"> <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> <v-icon size="48" color="primary" class="mb-2">mdi-cart-lock</v-icon>
<p class="text-body-2 text-medium-emphasis mb-3"> <p class="text-body-2 text-medium-emphasis mb-3">
Inicia sesión para agregar productos al carrito y finalizar tu compra 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> </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 <v-btn
color="primary" color="primary"
variant="tonal" variant="tonal"
@@ -157,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">
@@ -192,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>
@@ -229,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>
@@ -240,7 +312,7 @@
: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>
@@ -274,7 +346,8 @@ export default {
setup() { setup() {
const cartStore = useCartStore(); const cartStore = useCartStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const cartCollapsed = ref(false); // Cambiado a false para que inicie expandido en desktop 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
@@ -295,6 +368,7 @@ export default {
cartStore, cartStore,
authStore, authStore,
cartCollapsed, cartCollapsed,
showLoginPrompt,
isMobile, isMobile,
windowWidth, windowWidth,
}; };
@@ -339,6 +413,9 @@ export default {
isAuthenticated() { isAuthenticated() {
return this.authStore.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;
@@ -471,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;
@@ -493,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) => {
@@ -553,7 +628,14 @@ export default {
openLoginDialog() { openLoginDialog() {
this.$refs.loginDialogRef.open(); this.$refs.loginDialogRef.open();
}, },
showLogin() {
this.showLoginPrompt = true;
if (this.isMobile) {
this.cartCollapsed = false;
}
},
onLoginSuccess() { onLoginSuccess() {
this.showLoginPrompt = false;
this.api.getCurrentUser().then((user) => { this.api.getCurrentUser().then((user) => {
this.authStore.setUser(user); this.authStore.setUser(user);
}); });
@@ -769,6 +851,15 @@ export default {
.login-prompt-card { .login-prompt-card {
border: 2px dashed rgba(0, 0, 0, 0.12); border: 2px dashed rgba(0, 0, 0, 0.12);
background: #fafafa !important; background: #fafafa !important;
position: relative;
overflow-wrap: break-word;
word-break: break-word;
}
.login-prompt-close {
position: absolute;
top: 8px;
right: 8px;
} }
/* ============================================ /* ============================================