Compare commits
15 Commits
78dfea8714
...
feat/clean
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
523d0b9d08 | ||
|
|
0806731a1f | ||
|
|
beb5e153f3 | ||
|
|
3e19b3d272 | ||
|
|
b21d3a0ca6 | ||
|
|
3029e6d4b9 | ||
| 92483a110a | |||
|
|
a89af0f0bb | ||
|
|
b2a47c44dc | ||
|
|
2c2082266e | ||
| 4b006aaa01 | |||
| 762fde2cd0 | |||
| bb26f55829 | |||
|
|
5397ab2255 | ||
|
|
a2ab4fceb7 |
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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;"
|
|
||||||
>
|
|
||||||
© 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;"
|
||||||
|
>
|
||||||
|
© 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>
|
||||||
|
|||||||
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,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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -106,8 +106,9 @@
|
|||||||
{ 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: 'Gestión de Productos', route: '/admin/products', icon: 'mdi-package-variant'},
|
{ title: 'Gestión de Productos', route: '/admin/products', icon: 'mdi-package-variant'},
|
||||||
{ title: 'Ver Ventas por Catálogo', route: '/admin/catalog-sales', icon: 'mdi-cart-arrow-down'},
|
{ title: 'Imágenes de Catálogo', route: '/admin/catalogue-images', icon: 'mdi-image-multiple'},
|
||||||
|
{ title: 'Ver Ventas por Catálogo', route: '/admin/catalog-sales', icon: 'mdi-cart-arrow-down'},
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
{ header: 'Sincronización Tryton' },
|
{ header: 'Sincronización Tryton' },
|
||||||
{ title: 'Importar Productos', route: '/sincronizar_productos_tryton', icon: 'mdi-download'},
|
{ title: 'Importar Productos', route: '/sincronizar_productos_tryton', icon: 'mdi-download'},
|
||||||
@@ -119,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();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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>
|
||||||
@@ -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
|
||||||
@@ -379,7 +487,7 @@ export default {
|
|||||||
this.items = data.map((product) => ({
|
this.items = data.map((product) => ({
|
||||||
...product,
|
...product,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
img: product.img || not_image_product,
|
img: (product.catalogue_images?.length > 0) ? product.catalogue_images[0] : (product.img || not_image_product),
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -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) => {
|
||||||
@@ -470,7 +576,7 @@ export default {
|
|||||||
this.personalDataDialog = false;
|
this.personalDataDialog = false;
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: "/summary_purchase",
|
path: "/summary_purchase",
|
||||||
query: {
|
query: {
|
||||||
id: parseInt(data.id),
|
id: parseInt(data.id),
|
||||||
type: 'catalog'
|
type: 'catalog'
|
||||||
},
|
},
|
||||||
@@ -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
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -21,6 +23,7 @@ const ADMIN_ROUTES = [
|
|||||||
'/cuadrar_tarro',
|
'/cuadrar_tarro',
|
||||||
'/admin/products',
|
'/admin/products',
|
||||||
'/admin/catalog-sales',
|
'/admin/catalog-sales',
|
||||||
|
'/admin/catalogue-images',
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -39,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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,22 @@ class Api {
|
|||||||
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;
|
||||||
|
|||||||
@@ -130,6 +130,30 @@ class DjangoApi {
|
|||||||
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;
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user