Merge pull request #1 from enredacoop/migration/latienda-v2-to-v3
migration/latienda-v2-to-v3
This commit is contained in:
441
components/CompanyForm.vue
Normal file
441
components/CompanyForm.vue
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="activeModal"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
:ok-variant="modalColor"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
<form class="form" @submit.prevent="submitCompany">
|
||||||
|
<FormHeader title="general" />
|
||||||
|
<p class="help-text">
|
||||||
|
Estos son los datos básicos de la cooperativa. Procura completar el mayor
|
||||||
|
número de campos posible.
|
||||||
|
</p>
|
||||||
|
<fieldset class="fieldset fieldset-general">
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.cif"
|
||||||
|
:value="form.cif ? form.cif : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="C.I.F."
|
||||||
|
@input="form.cif = $event"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.company_name"
|
||||||
|
:value="form.company_name ? form.company_name : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Nombre de la cooperativa"
|
||||||
|
@input="form.company_name = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.short_name"
|
||||||
|
:value="form.short_name ? form.short_name : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Nombre corto"
|
||||||
|
@input="form.short_name = $event"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.web_link"
|
||||||
|
:value="form.web_link ? form.web_link : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Sitio web"
|
||||||
|
@input="form.web_link = $event"
|
||||||
|
/>
|
||||||
|
<small v-if="!isValid(form.shop_link)" class="error"
|
||||||
|
>La url no es válida</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.email"
|
||||||
|
:value="form.email ? form.email : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Email"
|
||||||
|
@input="form.email = $event"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.phone"
|
||||||
|
:value="form.phone ? form.phone : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Teléfono"
|
||||||
|
@input="form.phone = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.mobile"
|
||||||
|
:value="form.mobile ? form.mobile : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Móvil"
|
||||||
|
@input="form.mobile = $event"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.other_phone"
|
||||||
|
:value="form.other_phone ? form.other_phone : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Otro teléfono"
|
||||||
|
@input="form.other_phone = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="imagen">Logo</label>
|
||||||
|
<ClientOnly>
|
||||||
|
<FormInputImage :image-url="form.logo" @change="handleImage($event)" />
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="tags-basic">Palabras clave</label>
|
||||||
|
<BFormTags
|
||||||
|
v-model="form.tags"
|
||||||
|
placeholder="Añade palabras clave"
|
||||||
|
input-id="tags-basic"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="">Descripción</label>
|
||||||
|
<textarea v-model="form.description" class="textarea" type="text" />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<FormHeader title="tienda online" />
|
||||||
|
<p class="help-text">
|
||||||
|
Indica si la cooperativa dispone de tienda online. En caso afirmativo,
|
||||||
|
indica su url y la plataforma utilizada en el seleccionable. Si utilizas
|
||||||
|
WooCommerce, puedes indicar su API para sincronizar automáticamente todos
|
||||||
|
los productos.
|
||||||
|
<a href="#">Sigue estos pasos para obtener la API.</a>
|
||||||
|
</p>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<div class="cont">
|
||||||
|
<div>
|
||||||
|
<BFormCheckbox
|
||||||
|
v-model="form.shop"
|
||||||
|
label="¿Tiene tienda online?"
|
||||||
|
class="label"
|
||||||
|
:value="form.shop"
|
||||||
|
>Tienda online</BFormCheckbox>
|
||||||
|
</div>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.shop_link"
|
||||||
|
:value="form.shop_link ? form.shop_link : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Enlace a tienda online"
|
||||||
|
@input="form.shop_link = $event"
|
||||||
|
/>
|
||||||
|
<small v-if="!isValid(form.shop_link)" class="error"
|
||||||
|
>La url no es válida</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="select-shop">Plataforma</label>
|
||||||
|
<BFormSelect id="select-shop" v-model="form.platform" name="" >
|
||||||
|
<option selected disabled value="">Plataforma</option>
|
||||||
|
<option
|
||||||
|
v-for="(category, key) in categories"
|
||||||
|
:key="key"
|
||||||
|
:value="category.value"
|
||||||
|
>
|
||||||
|
{{ category.text }}
|
||||||
|
</option>
|
||||||
|
</BFormSelect>
|
||||||
|
</div>
|
||||||
|
<div v-if="form.platform === 'WOO_COMMERCE'" class="cont-col">
|
||||||
|
<h3>Configuración de WooCommerce</h3>
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.credentials.secret"
|
||||||
|
:value="form.credentials.secret ? form.credentials.secret : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="API WooCoomerce clave secreta"
|
||||||
|
@input="form.credentials.secret = $event"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.credentials.key"
|
||||||
|
:value="form.credentials.key ? form.credentials.key : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="API WooCoomerce clave pública"
|
||||||
|
@input="form.credentials.key = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<FormHeader title="localización" />
|
||||||
|
<p class="help-text">
|
||||||
|
La geolocalización nos ayudará a mostrar los productos a aquellos usuarios
|
||||||
|
que estén cerca de tu posición.
|
||||||
|
</p>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<!-- TODO: Arreglar este componente: -->
|
||||||
|
<!-- <GoogleAddress :value="form.address" @added-data="getPlace" /> -->
|
||||||
|
<br />
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.shop_rss_feed"
|
||||||
|
:value="form.shop_rss_feed ? form.shop_rss_feed : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Enlace RSS"
|
||||||
|
@input="form.shop_rss_feed = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<FormHeader title="términos y condiciones" />
|
||||||
|
<p class="help-text help-text-terms">
|
||||||
|
En estos textos podrás indicar las condiciones de venta o condiciones de
|
||||||
|
envío de la cooperativa. Si no tienes claro qué texto añadir, puedes hacer
|
||||||
|
<a @click="loadText">click aquí para cargar un texto predefinido</a>
|
||||||
|
que podrás editar. Ten en cuenta que al clicar se borrará el contenido de
|
||||||
|
ambos campos.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="">Condiciones de venta</label>
|
||||||
|
<textarea v-model="form.sale_terms" class="textarea" type="text" />
|
||||||
|
<label for="">Condiciones de envío</label>
|
||||||
|
<textarea v-model="form.shipping_terms" class="textarea" type="text" />
|
||||||
|
<FormInput
|
||||||
|
v-model="form.shipping_cost"
|
||||||
|
:value="form.shipping_cost ? form.shipping_cost : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Gastos de envío"
|
||||||
|
@input="form.shipping_cost = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div align="center">
|
||||||
|
<SubmitButton text="guardar" image-url="" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
return {
|
||||||
|
auth,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
categories: [
|
||||||
|
{ text: 'WooCommerce', value: 'WOO_COMMERCE' },
|
||||||
|
{ text: 'Prestashop', value: 'PRESTASHOP' },
|
||||||
|
{ text: 'Shopify', value: 'SHOPIFY' },
|
||||||
|
],
|
||||||
|
form: {
|
||||||
|
id: null,
|
||||||
|
cif: '',
|
||||||
|
company_name: '',
|
||||||
|
short_name: '',
|
||||||
|
web_link: '',
|
||||||
|
shop: false,
|
||||||
|
shop_link: '',
|
||||||
|
platform: '',
|
||||||
|
email: '',
|
||||||
|
logo: null,
|
||||||
|
city: null,
|
||||||
|
address: '',
|
||||||
|
tags: [],
|
||||||
|
geo: null,
|
||||||
|
phone: '',
|
||||||
|
credentials: { secret: '', key: '' },
|
||||||
|
mobile: '',
|
||||||
|
other_phone: '',
|
||||||
|
description: '',
|
||||||
|
shop_rss_feed: '',
|
||||||
|
sale_terms: '',
|
||||||
|
shipping_cost: '',
|
||||||
|
sync: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
default_text: {
|
||||||
|
sale_terms: `Devoluciones - derecho de desistimiento
|
||||||
|
|
||||||
|
Puedes devolver un pedido comprado a través del vendedor en el plazo de 14 días desde que se efectuó la entrega ejerciendo el derecho de desistimiento. Para cualquier otra duda puede consultar a la cooperativa que le ha vendido el producto.
|
||||||
|
|
||||||
|
Garantía Legal
|
||||||
|
|
||||||
|
Si el producto recibido no concuerda con la descripción o es defectuoso dentro de los 24 meses siguientes a su recepción, tiene derecho de manera gratuita a la reparación, reemplazo o reembolso del producto por parte del vendedor.`,
|
||||||
|
|
||||||
|
shipping_terms: `Salvo que se indique lo contrario los pedidos gestionados por esta cooperativa se envían en un plazo de dos a cinco días desde la recepción del pedido. Se le avisará si su pedido sufre algún retraso o se cancela.`,
|
||||||
|
},
|
||||||
|
|
||||||
|
activeModal: false,
|
||||||
|
modalText: '',
|
||||||
|
modalColor: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.getCompany()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
isValid(url) {
|
||||||
|
if (url) return /^(ftp|http|https):\/\/[^ "]+\.+[^ "]+$/.test(url)
|
||||||
|
else return true
|
||||||
|
},
|
||||||
|
loadText() {
|
||||||
|
this.form.sale_terms = this.default_text.sale_terms
|
||||||
|
this.form.shipping_terms = this.default_text.shipping_terms
|
||||||
|
},
|
||||||
|
getPlace(value) {
|
||||||
|
if (value) {
|
||||||
|
this.form.address = value.address
|
||||||
|
this.form.geo = value.geo
|
||||||
|
this.form.city = value.city
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getCompany() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const data = await $fetch('my_company/', {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (data) {
|
||||||
|
this.form = data.company
|
||||||
|
if (this.form.credentials === null)
|
||||||
|
this.form.credentials = { secret: '', key: '' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleImage(e) {
|
||||||
|
this.form.logo = e
|
||||||
|
},
|
||||||
|
|
||||||
|
parseForm() {
|
||||||
|
const formData = new FormData()
|
||||||
|
Object.keys(this.form).forEach((key) => {
|
||||||
|
if ((key === 'geo') | (key === 'tags') | (key === 'credentials'))
|
||||||
|
formData.append(key, JSON.stringify(this.form[key]))
|
||||||
|
else if (key === 'city') formData.append(key, this.form[key])
|
||||||
|
else if (this.form[key] || this.form[key] === '')
|
||||||
|
formData.append(key, this.form[key])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (typeof formData.get('logo') === 'string') {
|
||||||
|
formData.delete('logo')
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitCompany() {
|
||||||
|
const formData = this.parseForm()
|
||||||
|
console.log('Submitting company data:', formData)
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
await $fetch(`/companies/${this.form.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'PATCH',
|
||||||
|
//TODO: review with Diego. I changed formaData with this.form
|
||||||
|
body: this.form,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.modalText = 'Actualizado correctamente'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
} catch (error) {
|
||||||
|
this.modalText = 'Ha habido un error'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
this.activeModal = true
|
||||||
|
console.error('Error al actualizar la cooperativa:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.form {
|
||||||
|
@include desktop {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
@include tablet {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
@include mobile {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imagenInput {
|
||||||
|
font-size: $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
label, .label {
|
||||||
|
text-align: left;
|
||||||
|
color: $color-navy;
|
||||||
|
font-weight: $bold;
|
||||||
|
font-size: $xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $color-grey-inputs;
|
||||||
|
border: 1px solid $color-grey-inputs-border;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 5px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fieldset {
|
||||||
|
margin-bottom: 70px;
|
||||||
|
align-items: left;
|
||||||
|
}
|
||||||
|
.cont {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cont-col {
|
||||||
|
margin: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
margin: 20px 0 15px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.help-text {
|
||||||
|
text-align: left;
|
||||||
|
font-size: $xs;
|
||||||
|
text-align: justify;
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-greylayout;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
background-color: $color-light;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: -10px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: crimson;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
237
components/CoopCard.vue
Normal file
237
components/CoopCard.vue
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<div class="productcard_container">
|
||||||
|
<div class="row productcard_container-basic">
|
||||||
|
<div class="image_container col-md-2">
|
||||||
|
<img
|
||||||
|
v-if="coop.logo"
|
||||||
|
class="image"
|
||||||
|
:src="coop.logo"
|
||||||
|
:alt="coop.company_name"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="image"
|
||||||
|
src="@/assets/img/latienda-product-default.svg"
|
||||||
|
:alt="coop.company_name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="info_container col-md-6" align="left">
|
||||||
|
<NuxtLink :to="`/c/${this.coop.id}`">
|
||||||
|
<h2>{{ coop.company_name }}</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
<p class="description">{{ coop.description }}</p>
|
||||||
|
<div class="tags_container">
|
||||||
|
<NuxtLink
|
||||||
|
:to="tagRoute(n)"
|
||||||
|
class="tag_container"
|
||||||
|
v-for="n in coop.tags"
|
||||||
|
:key="n"
|
||||||
|
>
|
||||||
|
<img class="tag_img" src="@/assets/img/latienda-tag.svg" />
|
||||||
|
<span>{{ n }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 button_container" align="center">
|
||||||
|
<a v-if="coop.shop_link" :href="coop.shop_link" class="button_buy">
|
||||||
|
<img
|
||||||
|
alt="tienda"
|
||||||
|
class="button_cart_img"
|
||||||
|
src="@/assets/img/latienda-tienda.svg"
|
||||||
|
/>
|
||||||
|
<span>Tienda online</span>
|
||||||
|
</a>
|
||||||
|
<a v-if="coop.web_link" :href="coop.web_link" class="button_buy">
|
||||||
|
<img
|
||||||
|
alt="web"
|
||||||
|
class="button_cart_img"
|
||||||
|
src="@/assets/img/latienda-web.svg"
|
||||||
|
/>
|
||||||
|
<span>Página web</span>
|
||||||
|
</a>
|
||||||
|
<div class="smlogos_container">
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<a @click="shareFacebook">
|
||||||
|
<img
|
||||||
|
class="smlogo_img"
|
||||||
|
alt="facebook logo"
|
||||||
|
src="@/assets/img/latienda-smlogo-facebook.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a @click="shareTwitter">
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<img
|
||||||
|
class="smlogo_img"
|
||||||
|
alt="twitter logo"
|
||||||
|
src="@/assets/img/latienda-smlogo-twitter.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="shareWhatsApp()"
|
||||||
|
data-action="share/whatsapp/share"
|
||||||
|
target="_blank"
|
||||||
|
title="latiendacoop"
|
||||||
|
>
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<img
|
||||||
|
alt="whatsapp logo"
|
||||||
|
class="smlogo_img"
|
||||||
|
src="@/assets/img/latienda-smlogo-whatsapp.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import socialShare from '~/utils/socialShare'
|
||||||
|
export default {
|
||||||
|
name: 'CoopCard',
|
||||||
|
props: ['coop'],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
coopUrl() {
|
||||||
|
return `${window.location.origin}/c/${this.coop.id}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
tagRoute(tag) {
|
||||||
|
return `/busqueda?tags=${tag}`
|
||||||
|
},
|
||||||
|
|
||||||
|
shareFacebook() {
|
||||||
|
const url = socialShare.facebook(this.coopUrl)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
},
|
||||||
|
shareTwitter() {
|
||||||
|
const url = socialShare.twitter(this.coopUrl)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
},
|
||||||
|
shareWhatsApp() {
|
||||||
|
return socialShare.whatsApp(this.coopUrl)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.productcard_container {
|
||||||
|
border: 3px #e9e9e9 solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
.productcard_container-basic {
|
||||||
|
padding: 25px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info_container {
|
||||||
|
h2 {
|
||||||
|
outline: none;
|
||||||
|
font-weight: $medium;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
font-weight: $bold;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 5;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: $s;
|
||||||
|
|
||||||
|
.button_buy {
|
||||||
|
width: 100%;
|
||||||
|
color: $color-orange;
|
||||||
|
font-weight: $bold;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border: 3px solid $color-orange;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: $color-light;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 99, 99, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag_container {
|
||||||
|
margin: 25px 6px 0 0;
|
||||||
|
border: 2px solid $color-greylayout;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
|
||||||
|
.tag_img {
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.smlogo_container {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
.smlogo_img {
|
||||||
|
width: 35px;
|
||||||
|
fill: $color-greytext;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
img:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_cart_img {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content > h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
components/FacebookSignIn.vue
Normal file
60
components/FacebookSignIn.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button class="button f-signin-btn" @click="logInWithFacebook">
|
||||||
|
Login with Facebook
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
script: [
|
||||||
|
{
|
||||||
|
src: 'https://connect.facebook.net/es_ES/sdk.js',
|
||||||
|
defer: true,
|
||||||
|
async: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
window.fbAsyncInit = async function () {
|
||||||
|
await window.FB.init({
|
||||||
|
appId: process.env.facebookId,
|
||||||
|
cookie: true,
|
||||||
|
version: 'v13.0',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async logInWithFacebook() {
|
||||||
|
await window.FB.login(function (response) {
|
||||||
|
if (response.authResponse) {
|
||||||
|
console.log(response)
|
||||||
|
alert('You are logged in & cookie set!')
|
||||||
|
// Now you can redirect the user or do an AJAX request to
|
||||||
|
// a PHP script that grabs the signed request from the cookie.
|
||||||
|
} else {
|
||||||
|
alert('User cancelled login or did not fully authorize.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scooped>
|
||||||
|
.f-signin-btn {
|
||||||
|
align-self: center;
|
||||||
|
background-color: $color-facebook;
|
||||||
|
color: $color-light;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
>{{ labelText + (required ? '*' : '') }}
|
>{{ labelText + (required ? '*' : '') }}
|
||||||
<input
|
<input
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
|
:value="value ? value : inputValue"
|
||||||
:required="required"
|
:required="required"
|
||||||
:type="type"
|
:type="type"
|
||||||
:step="step"
|
:step="step"
|
||||||
|
|||||||
115
components/FormInputImage.vue
Normal file
115
components/FormInputImage.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-container">
|
||||||
|
<input class="input" type="file" @change="loadImage" />
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div v-show="edit">
|
||||||
|
<cropper
|
||||||
|
ref="cropperRef"
|
||||||
|
class="cropper"
|
||||||
|
:src="croppieImage"
|
||||||
|
:stencil-props="{ aspectRatio: 1 }"
|
||||||
|
/>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="filter-button" @click.prevent="crop">Aplicar</button>
|
||||||
|
<button @click.prevent="cancel">Cancelar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result -->
|
||||||
|
<div v-show="cropped && !edit">
|
||||||
|
<img :src="cropped" @click.prevent="editImage" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Cropper } from "vue-advanced-cropper";
|
||||||
|
import "vue-advanced-cropper/dist/style.css";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Cropper },
|
||||||
|
props: {
|
||||||
|
imageUrl: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["change"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
croppieImage: "",
|
||||||
|
cropped: null,
|
||||||
|
edit: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (typeof this.imageUrl === "string") {
|
||||||
|
this.croppieImage = this.imageUrl;
|
||||||
|
this.cropped = this.imageUrl;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editImage() {
|
||||||
|
this.edit = true;
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.edit = false;
|
||||||
|
},
|
||||||
|
loadImage(e) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
this.croppieImage = evt.target.result;
|
||||||
|
this.edit = true;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
},
|
||||||
|
crop() {
|
||||||
|
const canvas = this.$refs.cropperRef.getResult().canvas;
|
||||||
|
if (canvas) {
|
||||||
|
const base64 = canvas.toDataURL("image/png");
|
||||||
|
this.cropped = base64;
|
||||||
|
|
||||||
|
// Emitir como File
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
const fileName =
|
||||||
|
Math.random().toString(36).substring(2, 15) + ".png";
|
||||||
|
const file = new File([blob], fileName, { type: "image/png" });
|
||||||
|
this.$emit("change", file);
|
||||||
|
}
|
||||||
|
}, "image/png");
|
||||||
|
}
|
||||||
|
this.edit = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.cropper {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
.filter-button {
|
||||||
|
background-color: $color-navy;
|
||||||
|
color: $color-light;
|
||||||
|
font-size: $xs;
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
components/GoogleSignIn.vue
Normal file
92
components/GoogleSignIn.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<button class="g-signin-btn" id="googleSignIn" type="button">
|
||||||
|
Login with Google
|
||||||
|
</button>
|
||||||
|
<!-- id="google-signin-button"
|
||||||
|
class="g-signin2"
|
||||||
|
data-onsuccess="onGoogleSignIn" -->
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
/* eslint-disable */
|
||||||
|
// function onGoogleSignIn(googleUser) {
|
||||||
|
// // Useful data for your client-side scripts:
|
||||||
|
// var profile = googleUser.getBasicProfile()
|
||||||
|
// console.log('ID: ' + profile.getId()) // Don't send this directly to your server!
|
||||||
|
// console.log('Full Name: ' + profile.getName())
|
||||||
|
// console.log('Given Name: ' + profile.getGivenName())
|
||||||
|
// console.log('Family Name: ' + profile.getFamilyName())
|
||||||
|
// console.log('Image URL: ' + profile.getImageUrl())
|
||||||
|
// console.log('Email: ' + profile.getEmail())
|
||||||
|
|
||||||
|
// // The ID token you need to pass to your backend:
|
||||||
|
// var id_token = googleUser.getAuthResponse().id_token
|
||||||
|
// console.log('ID Token: ' + id_token)
|
||||||
|
// }
|
||||||
|
|
||||||
|
function onLoadGoogleCallback() {
|
||||||
|
gapi.load('auth2', function () {
|
||||||
|
const auth2 = gapi.auth2.init({
|
||||||
|
client_id: process.env.googleId,
|
||||||
|
cookiepolicy: 'single_host_origin',
|
||||||
|
scope: 'profile',
|
||||||
|
})
|
||||||
|
|
||||||
|
const element = document.getElementById('googleSignIn')
|
||||||
|
auth2.attachClickHandler(
|
||||||
|
element,
|
||||||
|
{},
|
||||||
|
function (googleUser) {
|
||||||
|
console.log('Signed in: ' + googleUser.getBasicProfile().getName())
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
console.log('Sign-in error', error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
name: 'google-signin-scope',
|
||||||
|
content: 'profile email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'google-signin-client_id',
|
||||||
|
content: process.env.googleId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
script: [
|
||||||
|
{
|
||||||
|
src: 'https://apis.google.com/js/platform.js',
|
||||||
|
defer: true,
|
||||||
|
async: true,
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// src: 'https://apis.google.com/js/api:client.js',
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.onload = onLoadGoogleCallback
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.g-signin-btn {
|
||||||
|
align-self: center;
|
||||||
|
background-color: $color-google;
|
||||||
|
color: $color-light;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
components/LineChart.vue
Normal file
117
components/LineChart.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <section> -->
|
||||||
|
<div :id="chartId" class="chart-container ">
|
||||||
|
<div v-show="dialog && dialog.dataIndex !== -1" class="chart-dialog">
|
||||||
|
<p>this is a test</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import { LineChart } from 'echarts/charts';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
// Import the tooltip, title, rectangular coordinate system, dataset and transform components
|
||||||
|
import {
|
||||||
|
DatasetComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
GridComponent,
|
||||||
|
LegendComponent,
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
TransformComponent
|
||||||
|
} from 'echarts/components';
|
||||||
|
|
||||||
|
// Features like Universal Transition and Label Layout
|
||||||
|
import { LabelLayout, UniversalTransition } from 'echarts/features';
|
||||||
|
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
|
||||||
|
|
||||||
|
// Register the required components
|
||||||
|
echarts.use([
|
||||||
|
DatasetComponent,
|
||||||
|
DataZoomComponent,
|
||||||
|
GridComponent,
|
||||||
|
LabelLayout,
|
||||||
|
LegendComponent,
|
||||||
|
LineChart,
|
||||||
|
SVGRenderer,
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
TransformComponent,
|
||||||
|
UniversalTransition,
|
||||||
|
CanvasRenderer
|
||||||
|
]);
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
chartId: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
lineChartData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => { }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chart: null,
|
||||||
|
showDataDialog: false,
|
||||||
|
dialog: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
dataIndex: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
chartContainer() {
|
||||||
|
return document.getElementById(this.chartId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
lineChartData: {
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal && newVal.xAxis && newVal.series) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const el = document.getElementById(this.chartId);
|
||||||
|
if (el && el.clientWidth > 0 && el.clientHeight > 0) {
|
||||||
|
this.initChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initChart() {
|
||||||
|
this.chart = echarts.init(this.chartContainer, {
|
||||||
|
renderer: 'svg',
|
||||||
|
useDirtyRect: false,
|
||||||
|
decalPattern: true,
|
||||||
|
});
|
||||||
|
this.chart.setOption(this.lineChartData)
|
||||||
|
this.chart.resize();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 24rem;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-dialog {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="container wrapper">
|
<div class="container wrapper">
|
||||||
<div class="navmenu-container">
|
<div class="navmenu-container">
|
||||||
<NavMenu @logout="logout" />
|
<NavMenu @handle-logout="handleLogout" />
|
||||||
</div>
|
</div>
|
||||||
<!-- isAdmin: {{ isAdmin }} <br>
|
<!-- isAdmin: {{ isAdmin }} <br>
|
||||||
isAuthenticated: {{ isAuthenticated }} <br> -->
|
isAuthenticated: {{ isAuthenticated }} <br> -->
|
||||||
@@ -71,7 +71,6 @@
|
|||||||
import { mapActions } from 'pinia'
|
import { mapActions } from 'pinia'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
return {
|
return {
|
||||||
@@ -89,10 +88,9 @@ export default {
|
|||||||
return this.auth.getName
|
return this.auth.getName
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('auth', ['logout']),
|
...mapActions(useAuthStore, ['logout']),
|
||||||
async logout() {
|
async handleLogout() {
|
||||||
try {
|
try {
|
||||||
await this.logout()
|
await this.logout()
|
||||||
this.$router.push('/login')
|
this.$router.push('/login')
|
||||||
|
|||||||
@@ -13,12 +13,13 @@
|
|||||||
to="/editar/productos/importar"
|
to="/editar/productos/importar"
|
||||||
>Importar</NuxtLink
|
>Importar</NuxtLink
|
||||||
>
|
>
|
||||||
<NuxtLink to="/" @click="logout" >Cerrar sesión</NuxtLink>
|
<NuxtLink to="/" @click="handleLogout" >Cerrar sesión</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { mapActions } from 'pinia'
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
@@ -35,16 +36,26 @@ export default {
|
|||||||
await this.checkIfCoopValidated()
|
await this.checkIfCoopValidated()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async logout() {
|
...mapActions(useAuthStore, ['logout']),
|
||||||
await this.auth.logout()
|
async handleLogout() {
|
||||||
|
try {
|
||||||
|
await this.logout()
|
||||||
|
this.$router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error logging out:', error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
//TODO: check if cooperative is validated is working
|
|
||||||
async checkIfCoopValidated() {
|
async checkIfCoopValidated() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const accessToken = this.auth.access
|
||||||
const result = await $fetch('my_company/', {
|
const result = await $fetch('my_company/', {
|
||||||
baseURL: config.public.baseURL,
|
baseURL: config.public.baseURL,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.coopIsValidated = result.data.company.is_validated
|
this.coopIsValidated = result.company.is_validated
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="navsearch_container container-fluid">
|
<div class="navsearch_container container-fluid">
|
||||||
<NuxtLink to="/editar/perfil">Mi perfil</NuxtLink>
|
<NuxtLink to="/editar/perfil">Mi perfil</NuxtLink>
|
||||||
<NuxtLink to="/" @click="logout" >Cerrar sesión</NuxtLink>
|
<NuxtLink to="/" @click="handleLogout" >Cerrar sesión</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { mapActions } from 'pinia'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
@@ -15,8 +17,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async logout() {
|
...mapActions(useAuthStore, ['logout']),
|
||||||
await this.auth.logout()
|
async handleLogout() {
|
||||||
|
try {
|
||||||
|
await this.logout()
|
||||||
|
this.$router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error logging out:', error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="navsearch_container container-fluid">
|
<div class="navsearch_container container-fluid">
|
||||||
<NuxtLink to="/busqueda"> Todos</NuxtLink>
|
<NuxtLink to="/busqueda"> Todos</NuxtLink>
|
||||||
<NuxtLink :to="{ name: 'busqueda', query: { order: 'newest' } }">
|
<button @click="searchLastestProducts"> Últimos productos</button>
|
||||||
Últimos productos</NuxtLink
|
|
||||||
>
|
|
||||||
<NuxtLink to="/busqueda"> Más buscados</NuxtLink>
|
<NuxtLink to="/busqueda"> Más buscados</NuxtLink>
|
||||||
<NuxtLink to="/c"> Cooperativas</NuxtLink>
|
<NuxtLink to="/c"> Cooperativas</NuxtLink>
|
||||||
<NuxtLink to="/registro"> Regístrate</NuxtLink>
|
<NuxtLink to="/registro"> Regístrate</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script></script>
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
searchLastestProducts() {
|
||||||
|
return navigateTo({
|
||||||
|
name: 'busqueda',
|
||||||
|
query: { order: 'newest' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.navsearch_container {
|
.navsearch_container {
|
||||||
@@ -22,7 +31,7 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a, button {
|
||||||
font-size: $m;
|
font-size: $m;
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
color: $color-navy;
|
color: $color-navy;
|
||||||
@@ -31,7 +40,11 @@
|
|||||||
a:nth-child(1):after,
|
a:nth-child(1):after,
|
||||||
a:nth-child(2):after,
|
a:nth-child(2):after,
|
||||||
a:nth-child(3):after,
|
a:nth-child(3):after,
|
||||||
a:nth-child(4):after {
|
a:nth-child(4):after,
|
||||||
|
button:nth-child(1):after,
|
||||||
|
button:nth-child(2):after,
|
||||||
|
button:nth-child(3):after,
|
||||||
|
button:nth-child(4):after {
|
||||||
color: $color-navy;
|
color: $color-navy;
|
||||||
content: '\22EE';
|
content: '\22EE';
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="nav-menu-container">
|
|
||||||
<img
|
|
||||||
class="burger"
|
|
||||||
src="@/assets/img/latienda-burger-nav.svg"
|
|
||||||
alt=""
|
|
||||||
@click="isMenuOpen = !isMenuOpen"
|
|
||||||
/>
|
|
||||||
<div :class="isMenuOpen ? `shadow` : ''">
|
|
||||||
<transition name="slider" mode="out-in">
|
|
||||||
<div v-if="isMenuOpen" class="nav-menu">
|
|
||||||
<img
|
|
||||||
class="close-icon"
|
|
||||||
src="@/assets/img/latienda-close-nav.svg"
|
|
||||||
alt=""
|
|
||||||
@click="isMenuOpen = !isMenuOpen"
|
|
||||||
/>
|
|
||||||
<nav class="nav">
|
|
||||||
<ul class="section-list">
|
|
||||||
<NuxtLink to="/">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<img
|
|
||||||
class="section-img"
|
|
||||||
src="@/assets/img/latienda-ubicacion.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span class="section-text">Inicio</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/c">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<img
|
|
||||||
class="section-img"
|
|
||||||
src="@/assets/img/latienda-tienda-nav.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span class="section-text">Cooperativas</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/page/info">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<img
|
|
||||||
class="section-img"
|
|
||||||
src="@/assets/img/latienda-bag.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span class="section-text">Sobre nosotros</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<a href="mailto:info@latienda.coop">
|
|
||||||
<img
|
|
||||||
class="section-img"
|
|
||||||
src="@/assets/img/envelope-simple.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span class="section-text">Contacto</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<ul class="login-list">
|
|
||||||
<NuxtLink v-if="!isAuthenticated" to="/login">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<img
|
|
||||||
class="section-img"
|
|
||||||
src="@/assets/img/latienda-lock.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span class="section-text">Acceder</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink v-if="isManager" to="/editar/perfil">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<img
|
|
||||||
class="section-img"
|
|
||||||
src="@/assets/img/latienda-user.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span class="section-text">Perfil</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink v-if="isManager" to="/editar/cooperativa">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<span class="section-text login">Cooperativa</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink v-if="isManager" to="/editar/productos">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<span class="section-text login">Productos</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink v-if="isManager" to="/editar/productos/importar">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<span class="section-text login">Importar</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink v-if="isAuthenticated" @click.native="logout" to="/">
|
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<img
|
|
||||||
class="section-img"
|
|
||||||
src="@/assets/img/latienda-sign-out.svg"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span class="section-text">Cerrar sesión</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
</ul>
|
|
||||||
<ul class="link-list">
|
|
||||||
<li class="link">
|
|
||||||
<a href="https://coceta.coop/" target="_blank">
|
|
||||||
<span class="link-text">Coceta</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<NuxtLink to="/page/terminos">
|
|
||||||
<li class="link" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<span class="link-text">Términos y condiciones</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/page/legal">
|
|
||||||
<li class="link" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<span class="link-text">Política de privacidad</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/page/cookies">
|
|
||||||
<li class="link" @click="isMenuOpen = !isMenuOpen">
|
|
||||||
<span class="link-text">Cookies</span>
|
|
||||||
</li>
|
|
||||||
</NuxtLink>
|
|
||||||
</ul>
|
|
||||||
<div class="credits">
|
|
||||||
<span>2021 La Tienda.Coop</span>
|
|
||||||
<a href="http://enreda.coop/" target="_blank"
|
|
||||||
>Sitio desarrollado por Enreda</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isMenuOpen: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isAuthenticated() {
|
|
||||||
return this.$store.getters['auth/isAuthenticated']
|
|
||||||
},
|
|
||||||
isManager() {
|
|
||||||
return this.$store.getters['auth/isManager']
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async logout() {
|
|
||||||
this.isMenuOpen = false
|
|
||||||
await this.$store.dispatch('auth/logout')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.slider-enter-active,
|
|
||||||
.slider-leave-active {
|
|
||||||
transition: all 0.5s ease;
|
|
||||||
}
|
|
||||||
.slider-enter,
|
|
||||||
.slider-leave-to {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-container {
|
|
||||||
@include tablet {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu {
|
|
||||||
font-family: $font-primary;
|
|
||||||
padding: 30px 0 0 20px;
|
|
||||||
background-color: $color-green;
|
|
||||||
height: 100vh;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 9999999999;
|
|
||||||
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);
|
|
||||||
overflow: scroll;
|
|
||||||
@include mobile {
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
@include tablet {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 999999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.burger,
|
|
||||||
.close-icon {
|
|
||||||
width: 1.8rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-icon {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: 0.4em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-list {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid $color-navy;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-text {
|
|
||||||
font-weight: $medium;
|
|
||||||
font-size: $s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-img {
|
|
||||||
width: 1.2rem;
|
|
||||||
margin-right: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $color-navy;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-list,
|
|
||||||
.credits {
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
padding: 0.1em;
|
|
||||||
}
|
|
||||||
.link {
|
|
||||||
padding: 0.3em 0;
|
|
||||||
font-size: $xs;
|
|
||||||
}
|
|
||||||
.credits {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
span,
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
color: $color-navy;
|
|
||||||
font-size: $xs;
|
|
||||||
padding: 0.3em 0;
|
|
||||||
}
|
|
||||||
margin-bottom: 4rem;
|
|
||||||
}
|
|
||||||
.login {
|
|
||||||
padding-left: 2rem;
|
|
||||||
font-weight: $regular;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
<span class="section-text login">Importar</span>
|
<span class="section-text login">Importar</span>
|
||||||
</li>
|
</li>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink v-if="isAuthenticated" @click="logout" to="/">
|
<NuxtLink v-if="isAuthenticated" @click="handleLogout" to="/">
|
||||||
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
<li class="section" @click="isMenuOpen = !isMenuOpen">
|
||||||
<img
|
<img
|
||||||
class="section-img"
|
class="section-img"
|
||||||
@@ -145,6 +145,7 @@ import { mapActions } from 'pinia'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
emits: ['handleLogout'],
|
||||||
setup() {
|
setup() {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
return {
|
return {
|
||||||
@@ -165,11 +166,11 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('auth', ['logout']),
|
...mapActions(useAuthStore, ['logout']),
|
||||||
async logout() {
|
async handleLogout() {
|
||||||
this.isMenuOpen = false
|
this.isMenuOpen = false
|
||||||
this.$emit('logout')
|
this.$emit('handleLogout')
|
||||||
await this.logout()
|
//await this.logout()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
511
components/ProductCard.vue
Normal file
511
components/ProductCard.vue
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
<template>
|
||||||
|
<div class="productcard_container">
|
||||||
|
<div class="row productcard_container-basic">
|
||||||
|
<div class="image_container" :class="expanded ? 'col-md-5' : 'col-md-2'">
|
||||||
|
<img
|
||||||
|
v-if="product.image"
|
||||||
|
class="image"
|
||||||
|
:src="product.image"
|
||||||
|
:alt="product.name"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="image"
|
||||||
|
src="@/assets/img/latienda-product-default.svg"
|
||||||
|
:alt="product.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="info_container" :class="expanded ? 'col-md-5' : 'col-md-6'">
|
||||||
|
<h2 v-b-toggle="'collapse-' + product.id" variant="primary">
|
||||||
|
{{ product.name }}
|
||||||
|
</h2>
|
||||||
|
<span v-if="product.price" class="price">{{
|
||||||
|
`${product.price}€`
|
||||||
|
}}</span>
|
||||||
|
<span v-else class="price">Precio a consultar</span>
|
||||||
|
<span v-if="Number(product.shipping_cost)"
|
||||||
|
>| {{ `Gastos de envío ${product.shipping_cost}€` }}</span
|
||||||
|
>
|
||||||
|
<span v-else>| Sin gastos de envío</span>
|
||||||
|
|
||||||
|
<span v-if="product.stock">| {{ `Stock ${product.stock}` }} </span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="description"
|
||||||
|
:class="{ 'not-expanded-description': !expanded }"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
v-if="product.description"
|
||||||
|
v-html="sanitizedDescription"
|
||||||
|
></p>
|
||||||
|
<span v-if="product.shipping_terms">{{
|
||||||
|
product.shipping_terms
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<b-collapse :id="'collapse-' + product.id" accordion="my-accordion" @show="onOpen" @hide="onClose">
|
||||||
|
<div class="tags_container">
|
||||||
|
<NuxtLink
|
||||||
|
:to="tagRoute(n)"
|
||||||
|
class="tag_container"
|
||||||
|
v-for="n in product.tags"
|
||||||
|
:key="n"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="tag image"
|
||||||
|
class="tag_img"
|
||||||
|
src="@/assets/img/latienda-tag.svg"
|
||||||
|
/>
|
||||||
|
<span>{{ n }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="smlogos_container">
|
||||||
|
<p class="share-text">Comparte:</p>
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<a class="smlogo_link" @click="shareFacebook">
|
||||||
|
<img
|
||||||
|
class="smlogo_img"
|
||||||
|
alt="facebook logo"
|
||||||
|
src="@/assets/img/latienda-smlogo-facebook.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<!-- <a @click="shareTwitter"> -->
|
||||||
|
<a :href="shareTwitter()">
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<img
|
||||||
|
class="smlogo_img"
|
||||||
|
alt="twitter logo"
|
||||||
|
src="@/assets/img/latienda-smlogo-twitter.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="shareWhatsApp()"
|
||||||
|
data-action="share/whatsapp/share"
|
||||||
|
target="_blank"
|
||||||
|
title="latiendacoop"
|
||||||
|
>
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<img
|
||||||
|
alt="whatsapp logo"
|
||||||
|
class="smlogo_img"
|
||||||
|
src="@/assets/img/latienda-smlogo-whatsapp.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="product.company" class="coop_info">
|
||||||
|
<NuxtLink :to="`c/${product.company.id}`">
|
||||||
|
<h2>{{ product.company.company_name }}</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
<p>{{ product.company.description }}</p>
|
||||||
|
<a :href="product.company.web_link">{{
|
||||||
|
product.company.web_link
|
||||||
|
}}</a>
|
||||||
|
</div>
|
||||||
|
</b-collapse>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
expanded
|
||||||
|
? 'col-md-2 button_container-detail'
|
||||||
|
: 'col-md-4 button_container'
|
||||||
|
"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="buyIntent"
|
||||||
|
:class="expanded ? 'button_buy-simple' : 'button_buy'"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="button_cart_img"
|
||||||
|
alt="cart"
|
||||||
|
src="@/assets/img/latienda-carrito.svg"
|
||||||
|
/>
|
||||||
|
<span v-show="!expanded">Comprar</span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="product.discount && product.discount > 0"
|
||||||
|
class="discount-tag"
|
||||||
|
>
|
||||||
|
{{ `Descuento ${product.discount}%` }}
|
||||||
|
</div>
|
||||||
|
<span v-if="product.company" v-show="!expanded">{{
|
||||||
|
product.company.company_name
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="expanded && relatedProducts" class="related_products">
|
||||||
|
<h2>Productos relacionados</h2>
|
||||||
|
<ProductsRelated :related-products="relatedProducts" />
|
||||||
|
</div>
|
||||||
|
<ProductModal v-if="modal" :product="product" @close-modal="closeModal" />
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="active"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
:ok-variant="modalColor"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
import { mapState } from 'pinia'
|
||||||
|
import socialShare from '~/utils/socialShare'
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
product: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expanded: false,
|
||||||
|
modal: false,
|
||||||
|
productUrl: null,
|
||||||
|
relatedProducts: null,
|
||||||
|
active: false,
|
||||||
|
modalText: '',
|
||||||
|
modalColor: 'info',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAuthStore, ['access']),
|
||||||
|
sanitizedDescription() {
|
||||||
|
return DOMPurify.sanitize(this.product.description, {
|
||||||
|
ALLOWED_TAGS: ['p'],
|
||||||
|
ALLOWED_ATTR: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.productUrl = window.location.origin + `/productos/${this.product.id}`
|
||||||
|
this.getRelatedProducts()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async onOpen() {
|
||||||
|
this.expanded = true
|
||||||
|
await this.getRelatedProducts()
|
||||||
|
await this.sendLog('view')
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
this.expanded = false
|
||||||
|
},
|
||||||
|
tagRoute(tag) {
|
||||||
|
return `/busqueda?tags=${tag}`
|
||||||
|
},
|
||||||
|
async getRelatedProducts() {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const data = await $fetch(`/products/${this.product.id}/related/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: '/',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.relatedProducts = data
|
||||||
|
} catch {
|
||||||
|
this.relatedProducts = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openUrl(url) {
|
||||||
|
window.open(url)
|
||||||
|
},
|
||||||
|
|
||||||
|
buyIntent() {
|
||||||
|
this.sendLog('shop')
|
||||||
|
return this.product.url
|
||||||
|
? this.openUrl(this.product.url)
|
||||||
|
: (this.modal = true)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPosition() {
|
||||||
|
const geoLocation = () =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(posData) => {
|
||||||
|
resolve(posData)
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const position = await geoLocation()
|
||||||
|
const geo = {
|
||||||
|
latitude: position.coords.latitude,
|
||||||
|
longitude: position.coords.longitude,
|
||||||
|
}
|
||||||
|
return geo
|
||||||
|
} catch {
|
||||||
|
const geo = null
|
||||||
|
return geo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendLog(action) {
|
||||||
|
const geo = await this.getPosition()
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const accessToken = this.access
|
||||||
|
const data = await $fetch('https://api.ipify.org?format=json', {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
const ip = data.ip
|
||||||
|
const object = {
|
||||||
|
action: action,
|
||||||
|
action_object: {
|
||||||
|
model: 'product',
|
||||||
|
id: this.product.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (ip) object.ip = ip
|
||||||
|
if (geo) object.geo = geo
|
||||||
|
//TODO: review problems with 406 error backend
|
||||||
|
await $fetch(`/stats/me/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: object,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending log:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal(value) {
|
||||||
|
console.log(value)
|
||||||
|
this.modal = false
|
||||||
|
this.active = true
|
||||||
|
if (value === 200 || value === 201) {
|
||||||
|
this.modalText = 'Actualizado correctamente'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
} else if (value) {
|
||||||
|
this.modalText = 'Se ha producido un error en el envío'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
shareFacebook() {
|
||||||
|
const url = socialShare.facebook(this.productUrl)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
},
|
||||||
|
shareTwitter() {
|
||||||
|
return socialShare.twitter(this.productUrl)
|
||||||
|
},
|
||||||
|
shareWhatsApp() {
|
||||||
|
return socialShare.whatsApp(this.productUrl)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.productcard_container {
|
||||||
|
border: 3px $color-grey-nav solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.productcard_container-basic {
|
||||||
|
padding: 25px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info_container {
|
||||||
|
h2 {
|
||||||
|
outline: none;
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
font-weight: $bold;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-expanded-description {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 5;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
@include mobile {
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: $m;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: medium;
|
||||||
|
font-size: $m;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_buy {
|
||||||
|
border: 3px solid $color-orange;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: $color-light;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 99, 99, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $color-orange;
|
||||||
|
font-weight: $bold;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_buy-simple {
|
||||||
|
border: 3px solid $color-orange;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $color-light;
|
||||||
|
padding: 10px 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 99, 99, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_cart_img {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag_container {
|
||||||
|
margin: 25px 6px 0 0;
|
||||||
|
border: 2px solid $color-greylayout;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
|
||||||
|
.tag_img {
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-tag {
|
||||||
|
margin: 5px;
|
||||||
|
border: none;
|
||||||
|
background-color: $color-green;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-text {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smlogo_container {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
.smlogo_img {
|
||||||
|
width: 40px;
|
||||||
|
fill: $color-greytext;
|
||||||
|
}
|
||||||
|
|
||||||
|
img:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_cart_img {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop_info {
|
||||||
|
margin-top: 25px;
|
||||||
|
p,
|
||||||
|
a {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_products {
|
||||||
|
background-color: $color-lighter-green;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 35px auto;
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content > h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
467
components/ProductCardDetails.vue
Normal file
467
components/ProductCardDetails.vue
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
<template>
|
||||||
|
<div class="productcard_container">
|
||||||
|
<div class="row productcard_container-basic">
|
||||||
|
<div class="image_container col-md-5">
|
||||||
|
<img :src="getProductImg(product)" class="image" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="info_container col-md-5">
|
||||||
|
<h2 variant="primary">
|
||||||
|
{{ product?.name }}
|
||||||
|
</h2>
|
||||||
|
<span v-if="product?.price" class="price">{{
|
||||||
|
`${product?.price}€`
|
||||||
|
}}</span>
|
||||||
|
<span v-else class="price">Precio a consultar</span>
|
||||||
|
<span v-if="Number(product?.shipping_cost)"
|
||||||
|
>| {{ `Gastos de envío ${product?.shipping_cost}€` }}</span
|
||||||
|
>
|
||||||
|
<span v-else>| Sin gastos de envío</span>
|
||||||
|
|
||||||
|
<span v-if="product?.stock">| {{ `Stock ${product?.stock}€` }} </span>
|
||||||
|
<p
|
||||||
|
v-if="product?.description"
|
||||||
|
class="description"
|
||||||
|
v-html="sanitize(product?.description)"
|
||||||
|
></p>
|
||||||
|
<span v-if="product?.shipping_terms">{{ product?.shipping_terms }}</span>
|
||||||
|
<BCollapse visible accordion="my-accordion">
|
||||||
|
<div class="tags_container">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="n in product?.tags"
|
||||||
|
:key="n"
|
||||||
|
:to="tagRoute(n)"
|
||||||
|
class="tag_container"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="tag_img"
|
||||||
|
alt="tag image"
|
||||||
|
src="@/assets/img/latienda-tag.svg"
|
||||||
|
/>
|
||||||
|
<span>{{ n }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="smlogos_container">
|
||||||
|
<p class="share-text">Comparte:</p>
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<a @click="shareFacebook">
|
||||||
|
<img
|
||||||
|
alt="facebook logo"
|
||||||
|
class="smlogo_img"
|
||||||
|
src="@/assets/img/latienda-smlogo-facebook.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a :href="shareTwitter()">
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<img
|
||||||
|
alt="twitter logo"
|
||||||
|
class="smlogo_img"
|
||||||
|
src="@/assets/img/latienda-smlogo-twitter.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="shareWhatsApp()"
|
||||||
|
data-action="share/whatsapp/share"
|
||||||
|
target="_blank"
|
||||||
|
title="latiendacoop"
|
||||||
|
>
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<img
|
||||||
|
alt="whatsapp logo"
|
||||||
|
class="smlogo_img"
|
||||||
|
src="@/assets/img/latienda-smlogo-whatsapp.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="coop_info">
|
||||||
|
<NuxtLink :to="`/c/${company?.id}`">
|
||||||
|
<h2>{{ company?.company_name }}</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
<p class="description">{{ company?.description }}</p>
|
||||||
|
<a href="#">{{ company?.web_link }}</a>
|
||||||
|
</div>
|
||||||
|
</BCollapse>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 button_container-detail" align="center">
|
||||||
|
<button class="button_buy-simple" @click="buyIntent">
|
||||||
|
<img
|
||||||
|
class="button_cart_img"
|
||||||
|
alt="cart"
|
||||||
|
src="@/assets/img/latienda-carrito.svg"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="product?.discount && product?.discount > 0"
|
||||||
|
class="discount-tag"
|
||||||
|
>
|
||||||
|
{{ `Descuento ${product?.discount}%` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="related_products">
|
||||||
|
<h2 v-if="related">Productos relacionados</h2>
|
||||||
|
<h2 v-else>Otros productos</h2>
|
||||||
|
<ProductsRelated :related-products="relatedProducts" />
|
||||||
|
</div>
|
||||||
|
<ProductModal v-if="modal" :product="product" @close-modal="closeModal" />
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="active"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
:ok-variant="modalColor"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'pinia'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
import socialShare from '~/utils/socialShare'
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
product: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
company: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
relatedProducts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
related: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modal: true,
|
||||||
|
productUrl: null,
|
||||||
|
geolocation: null,
|
||||||
|
active: false,
|
||||||
|
modalText: '',
|
||||||
|
modalColor: 'info',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAuthStore, ['id', 'access']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.productUrl = window.location.href
|
||||||
|
this.sendLog('view')
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getProductImg(product) {
|
||||||
|
if (product && product.image)
|
||||||
|
return product.image
|
||||||
|
return `@/assets/img/latienda-product-default.svg`
|
||||||
|
},
|
||||||
|
tagRoute(tag) {
|
||||||
|
return `/busqueda?tags=${tag}`
|
||||||
|
},
|
||||||
|
openUrl(url) {
|
||||||
|
window.open(url)
|
||||||
|
},
|
||||||
|
closeModal(value) {
|
||||||
|
this.modal = false
|
||||||
|
if (value === 200 || value === 201) {
|
||||||
|
this.modalText = 'Actualizado correctamente'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
} else if (value) {
|
||||||
|
this.modalText = 'Se ha producido un error en el envío'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO: implement buyIntent (review functionality, because sendLog is not working)
|
||||||
|
buyIntent() {
|
||||||
|
this.sendLog('shop')
|
||||||
|
return this.product.url
|
||||||
|
? this.openUrl(this.product.url)
|
||||||
|
: (this.modal = true)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPosition() {
|
||||||
|
const geoLocation = () =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(posData) => {
|
||||||
|
resolve(posData)
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const position = await geoLocation()
|
||||||
|
const geo = {
|
||||||
|
latitude: position.coords.latitude,
|
||||||
|
longitude: position.coords.longitude,
|
||||||
|
}
|
||||||
|
return geo
|
||||||
|
} catch {
|
||||||
|
const geo = null
|
||||||
|
return geo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendLog(action) {
|
||||||
|
const geo = await this.getPosition()
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const accessToken = this.access
|
||||||
|
const response = await $fetch('https://api.ipify.org?format=json', {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
const ip = response.ip
|
||||||
|
const object = {
|
||||||
|
action: action,
|
||||||
|
action_object: {
|
||||||
|
model: 'product',
|
||||||
|
id: this.product.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
console.log('Sending log OBJECT:', object)
|
||||||
|
if (ip) object.ip = ip
|
||||||
|
if (geo) object.geo = geo
|
||||||
|
//TODO: review problems with 406 error backend
|
||||||
|
await $fetch(`/stats/me/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: object,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending log:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
shareFacebook() {
|
||||||
|
const url = socialShare.facebook(this.productUrl)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
},
|
||||||
|
shareTwitter() {
|
||||||
|
return socialShare.twitter(this.productUrl)
|
||||||
|
},
|
||||||
|
shareWhatsApp() {
|
||||||
|
return socialShare.whatsApp(this.productUrl)
|
||||||
|
},
|
||||||
|
|
||||||
|
sanitize(dirtyHtml) {
|
||||||
|
return DOMPurify.sanitize(dirtyHtml, {
|
||||||
|
ALLOWED_TAGS: [
|
||||||
|
'b',
|
||||||
|
'i',
|
||||||
|
'em',
|
||||||
|
'strong',
|
||||||
|
'a',
|
||||||
|
'p',
|
||||||
|
'br',
|
||||||
|
'ul',
|
||||||
|
'li',
|
||||||
|
'ol',
|
||||||
|
'span',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
],
|
||||||
|
ALLOWED_ATTR: ['href', 'target', 'rel', 'class'],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.productcard_container {
|
||||||
|
border: 3px solid $color-grey-nav;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.productcard_container-basic {
|
||||||
|
padding: 25px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info_container {
|
||||||
|
h2 {
|
||||||
|
outline: none;
|
||||||
|
font-weight: medium;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: $m;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: medium;
|
||||||
|
font-size: $m;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_buy {
|
||||||
|
border: 3px solid $color-orange;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: $color-light;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 99, 99, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $color-orange;
|
||||||
|
font-weight: $bold;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_buy-simple {
|
||||||
|
border: 3px solid $color-orange;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $color-light;
|
||||||
|
padding: 10px 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 99, 99, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_cart_img {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag_container {
|
||||||
|
margin: 25px 6px 0 0;
|
||||||
|
border: 2px solid $color-greylayout;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
|
||||||
|
.tag_img {
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-text {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smlogo_container {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
.smlogo_img {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 40px;
|
||||||
|
fill: $color-greylayout;
|
||||||
|
}
|
||||||
|
img:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_cart_img {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop_info {
|
||||||
|
margin-top: 25px;
|
||||||
|
p,
|
||||||
|
a {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_products {
|
||||||
|
background-color: $color-lighter-green;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 35px auto;
|
||||||
|
font-weight: medium;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-tag {
|
||||||
|
margin: 5px;
|
||||||
|
border: none;
|
||||||
|
background-color: $color-green;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content > h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
338
components/ProductFilter.vue
Normal file
338
components/ProductFilter.vue
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div v-b-toggle.collapse-all class="filters-header">
|
||||||
|
<img
|
||||||
|
class="image"
|
||||||
|
src="@/assets/img/product-filter-filtrar.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<h2 class="text">FILTRAR POR</h2>
|
||||||
|
</div>
|
||||||
|
<BCollapse id="collapse-all" visible>
|
||||||
|
<!-- Location -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="location">
|
||||||
|
<div class="googleaddress-container">
|
||||||
|
<!-- TODO: Revisar componente -->
|
||||||
|
<!-- <GoogleAddress
|
||||||
|
@addedData="getPlace"
|
||||||
|
:geo="geo"
|
||||||
|
:label="'Cercanía'"
|
||||||
|
/> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapsible collapsible-range">
|
||||||
|
<hr />
|
||||||
|
<div v-b-toggle.collapse-price class="m-1 filter-header">
|
||||||
|
<h2 class="title">Precio</h2>
|
||||||
|
<img
|
||||||
|
src="../assets/img/latienda-arrow-down.svg"
|
||||||
|
alt=""
|
||||||
|
class="arrow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BCollapse id="collapse-price" visible>
|
||||||
|
<!-- Price -->
|
||||||
|
<div class="filter-range-container">
|
||||||
|
<ClientOnly>
|
||||||
|
<v-range-slider
|
||||||
|
v-model="priceRange"
|
||||||
|
:min="0"
|
||||||
|
:max="500"
|
||||||
|
step="10"
|
||||||
|
thumb-label="always"
|
||||||
|
color="#8cead8"
|
||||||
|
track-color="#d6d5d5"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<span>{{ priceRange[0] }} €</span>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<span>{{ priceRange[1] }} €</span>
|
||||||
|
</template>
|
||||||
|
</v-range-slider>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
<hr class="dotted-hr" />
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="checkbox-shipped"
|
||||||
|
v-model="filterForm.shipping_cost"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkbox-shipped">
|
||||||
|
Sin gastos de envío
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<hr class="dotted-hr" />
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
id="checkbox-discount"
|
||||||
|
v-model="filterForm.discount"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="checkbox-discount">
|
||||||
|
Descuentos
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</BCollapse>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible collapsible-checkboxes">
|
||||||
|
<hr />
|
||||||
|
<div v-b-toggle.collapse-categories class="m-1 filter-header">
|
||||||
|
<h2 class="title">Categorías</h2>
|
||||||
|
<img
|
||||||
|
src="../assets/img/latienda-arrow-down.svg"
|
||||||
|
alt=""
|
||||||
|
class="arrow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BCollapse id="collapse-categories" visible>
|
||||||
|
<div class="checkboxes">
|
||||||
|
<div
|
||||||
|
v-for="(n, index) in categories"
|
||||||
|
:key="index"
|
||||||
|
class="form-check"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="'checkbox-' + index"
|
||||||
|
v-model="checkedCategories"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
:value="n"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" :for="'checkbox-' + index">
|
||||||
|
{{ n }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="filter-button" @click="applyFilters">
|
||||||
|
Aplicar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</BCollapse>
|
||||||
|
</div>
|
||||||
|
</BCollapse>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
filters: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
prices: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
currentFilters: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
geo: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['applyFilters'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
filterForm: {
|
||||||
|
shipping_cost: false,
|
||||||
|
discount: false,
|
||||||
|
},
|
||||||
|
visible: true,
|
||||||
|
checkedCategories: [],
|
||||||
|
categories: [
|
||||||
|
'Alimentación, bebida y tabaco',
|
||||||
|
'Arte y ocio',
|
||||||
|
'Bebés y niños pequeños',
|
||||||
|
'Bricolaje',
|
||||||
|
'Cámaras y ópticas',
|
||||||
|
'Casa y jardín',
|
||||||
|
'Economía e industria',
|
||||||
|
'Electrónica',
|
||||||
|
'Elementos religiosos y ceremoniales',
|
||||||
|
'Equipamiento deportivo',
|
||||||
|
'Juegos y juguetes',
|
||||||
|
'Maletas y bolsos de viaje',
|
||||||
|
'Material de oficina',
|
||||||
|
'Mobiliario',
|
||||||
|
'Multimedia',
|
||||||
|
'Productos para adultos',
|
||||||
|
'Productos para mascotas y animales',
|
||||||
|
'Ropa y accesorios',
|
||||||
|
'Salud y belleza',
|
||||||
|
'Software',
|
||||||
|
'Vehículos y recambios',
|
||||||
|
],
|
||||||
|
priceRange: [0, 500],
|
||||||
|
priceRangeFilter: {},
|
||||||
|
place: null,
|
||||||
|
coordinates: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
currentFilters(newCurrentFilters) {
|
||||||
|
if (newCurrentFilters) {
|
||||||
|
if (newCurrentFilters['category']) {
|
||||||
|
this.checkedCategories = newCurrentFilters['category']
|
||||||
|
} else {
|
||||||
|
this.checkedCategories = []
|
||||||
|
}
|
||||||
|
if (Object.keys(newCurrentFilters).includes('shipping_cost')) {
|
||||||
|
this.filterForm.shipping_cost = true
|
||||||
|
} else {
|
||||||
|
this.filterForm.shipping_cost = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.filterForm.discount = this.$route.query.hasOwnProperty('discount')
|
||||||
|
},
|
||||||
|
prices(newPrices) {
|
||||||
|
if (newPrices.min && newPrices.max) {
|
||||||
|
this.priceRange = [newPrices.min, newPrices.max]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
if (this.prices.min && this.prices.max) {
|
||||||
|
this.priceRange = [this.prices.min, this.prices.max]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getPlace(value) {
|
||||||
|
this.place = value
|
||||||
|
},
|
||||||
|
|
||||||
|
applyFilters() {
|
||||||
|
const filters = {}
|
||||||
|
filters.price_min = this.priceRange[0]
|
||||||
|
filters.price_max = this.priceRange[1]
|
||||||
|
if (this.filterForm.shipping_cost) filters.shipping_cost = false
|
||||||
|
if (this.filterForm.discount) filters.discount = true
|
||||||
|
filters.category = this.checkedCategories
|
||||||
|
if (this.place) {
|
||||||
|
filters.latitude = this.place.geo.latitude
|
||||||
|
filters.longitude = this.place.geo.longitude
|
||||||
|
}
|
||||||
|
this.$emit('applyFilters', filters)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter-button {
|
||||||
|
margin-top: 30px;
|
||||||
|
background-color: $color-navy;
|
||||||
|
color: $color-light;
|
||||||
|
font-size: $xs;
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.filter-range-container {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
.filters-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
outline: none;
|
||||||
|
@include mobile {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
font-size: $s;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: $color-navy;
|
||||||
|
font-weight: $bold;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.location {
|
||||||
|
label {
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-navy;
|
||||||
|
font-weight: $bold;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 20px;
|
||||||
|
background: url('../assets/img/product-filter-ubicacion.svg') center /
|
||||||
|
contain no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-input {
|
||||||
|
background-color: $color-grey-nav;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
.filter-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-navy;
|
||||||
|
font-weight: $bold;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: Noto Sans Regular, sans-serif;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkboxes {
|
||||||
|
height: 6rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotted-hr {
|
||||||
|
border-top: 2px dotted $color-greylighter;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
376
components/ProductForm.vue
Normal file
376
components/ProductForm.vue
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
<template>
|
||||||
|
<form class="form" @submit.prevent="sendProduct">
|
||||||
|
<div class="cont-col">
|
||||||
|
<BFormCheckbox
|
||||||
|
id="customSwitch1"
|
||||||
|
v-model="form.active"
|
||||||
|
class="label"
|
||||||
|
>Activo
|
||||||
|
<span class="help-text"
|
||||||
|
>Desactiva el producto si quieres paralizar temporalmente su
|
||||||
|
venta</span
|
||||||
|
>
|
||||||
|
</BFormCheckbox>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<FormHeader title="General" />
|
||||||
|
<p class="help-text">
|
||||||
|
Estos son los datos básicos de un producto. Procura completar el mayor
|
||||||
|
número de campos posible. Los campos señalados con asterisco (*) son
|
||||||
|
obligatorios, los demás son opcionales. Si el producto o servicio no tiene
|
||||||
|
precio asignado, aparecerá "Consultar precio".
|
||||||
|
</p>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.name"
|
||||||
|
:value="form.name ? form.name : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Nombre"
|
||||||
|
required
|
||||||
|
@input="form.name = $event"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.price"
|
||||||
|
:value="form.price ? form.price : ''"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
label-text="Precio"
|
||||||
|
placeholder="precio + iva"
|
||||||
|
@input="form.price = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.url"
|
||||||
|
:value="form.url ? form.url : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="Url"
|
||||||
|
placeholder="enlace directo al producto en tu tienda o web"
|
||||||
|
@input="form.url = $event"
|
||||||
|
/>
|
||||||
|
<small v-if="form.url && !isValidUrl(form.url)" class="error">
|
||||||
|
La url no es válida
|
||||||
|
</small>
|
||||||
|
<br />
|
||||||
|
<FormInput
|
||||||
|
v-model="form.sku"
|
||||||
|
:value="form.sku ? form.sku : ''"
|
||||||
|
type="text"
|
||||||
|
label-text="SKU o identificador"
|
||||||
|
@input="form.sku = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="imagen">Imagen</label>
|
||||||
|
<ClientOnly>
|
||||||
|
<FormInputImage :image-url="form.image" @change="handleImage" />
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="cont-col">
|
||||||
|
<input
|
||||||
|
id="imagen"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="imagenInput"
|
||||||
|
placeholder="Elige una imagen"
|
||||||
|
@change="handleImage"
|
||||||
|
/>
|
||||||
|
</div> -->
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="">Descripción del producto</label>
|
||||||
|
<textarea v-model="form.description" class="textarea" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="cont">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.stock"
|
||||||
|
:value="form.stock ? form.stock : ''"
|
||||||
|
type="number"
|
||||||
|
label-text="Stock"
|
||||||
|
@input="form.stock = $event"
|
||||||
|
/>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.discount"
|
||||||
|
:value="form.discount ? form.discount : ''"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
label-text="Descuento"
|
||||||
|
@input="form.discount = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.identifiers"
|
||||||
|
:value="form.identifiers ? form.identifiers : ''"
|
||||||
|
label-text="Identificador único"
|
||||||
|
@input="form.identifiers = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<FormHeader title="Categorías" />
|
||||||
|
<p class="help-text">
|
||||||
|
Estos datos ayudan a que tu producto sea encontrado por los clientes.
|
||||||
|
Procura completar el mayor número de campos posibles. Los campos
|
||||||
|
señalados con asterisco (*) son obligatorios, los demás son opcionales.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="category">Categoría*</label>
|
||||||
|
<BFormInput
|
||||||
|
id="category"
|
||||||
|
v-model="form.category"
|
||||||
|
autocomplete="off"
|
||||||
|
list="my-list-id"
|
||||||
|
/>
|
||||||
|
<datalist id="my-list-id">
|
||||||
|
<option v-for="(choice, index) in categories" :key="`category-${index}`">
|
||||||
|
{{ choice }}
|
||||||
|
</option>
|
||||||
|
</datalist>
|
||||||
|
<!-- <b-form-select required v-model="form.category" name="" id="category">
|
||||||
|
<option disabled value="">Categoría</option>
|
||||||
|
<option
|
||||||
|
v-for="(category, key) in categories"
|
||||||
|
:key="key"
|
||||||
|
:value="category"
|
||||||
|
>
|
||||||
|
{{ category }}
|
||||||
|
</option>
|
||||||
|
</b-form-select> -->
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="tags-basic">Palabras clave</label>
|
||||||
|
<BFormTags
|
||||||
|
v-model="form.tags"
|
||||||
|
placeholder="Añade palabras clave (moda, complementos...)"
|
||||||
|
input-id="tags-basic"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="tags-basic">Atributos</label>
|
||||||
|
<BFormTags
|
||||||
|
v-model="form.attributes"
|
||||||
|
placeholder="Añade características del producto (talla, color...)"
|
||||||
|
input-id="tags-basic"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<FormHeader title="Envío" />
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="">Condiciones de envío</label>
|
||||||
|
<p class="help-text">
|
||||||
|
Aquí podrás indicar las condiciones de envío específicas para este
|
||||||
|
producto. Si no lo rellenas se mostrarán las opciones por defecto que
|
||||||
|
puedes editar en tu formulario de edición de la Cooperativa en el
|
||||||
|
apartado condiciones de envío.
|
||||||
|
</p>
|
||||||
|
<textarea v-model="form.shipping_terms" class="textarea" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="cont-col">
|
||||||
|
<FormInput
|
||||||
|
v-model="form.shipping_cost"
|
||||||
|
:value="form.shipping_cost ? form.shipping_cost : ''"
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
label-text="Gastos de envío"
|
||||||
|
placeholder="ej. 4,50"
|
||||||
|
@input="form.shipping_cost = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="submit-btn" align="center">
|
||||||
|
<SubmitButton text="guardar" image-url="" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FormInputImage from './FormInputImage.vue'
|
||||||
|
import dataProcessing from '~/utils/dataProcessing'
|
||||||
|
export default {
|
||||||
|
components: { FormInputImage },
|
||||||
|
props: {
|
||||||
|
productForm: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['send'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
sku: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
image: null,
|
||||||
|
url: '',
|
||||||
|
price: '',
|
||||||
|
shipping_cost: '',
|
||||||
|
shipping_terms: '',
|
||||||
|
source: 'MANUAL',
|
||||||
|
active: true,
|
||||||
|
discount: '',
|
||||||
|
stock: null,
|
||||||
|
category: '',
|
||||||
|
tags: [],
|
||||||
|
attributes: [],
|
||||||
|
identifiers: '',
|
||||||
|
},
|
||||||
|
// tagsArray: ['tag1', 'tag2', 'tag3', 'tag33'],
|
||||||
|
categories: [
|
||||||
|
'Alimentación, bebida y tabaco',
|
||||||
|
'Arte y ocio',
|
||||||
|
'Bebés y niños pequeños',
|
||||||
|
'Bricolaje',
|
||||||
|
'Cámaras y ópticas',
|
||||||
|
'Casa y jardín',
|
||||||
|
'Economía e industria',
|
||||||
|
'Electrónica',
|
||||||
|
'Elementos religiosos y ceremoniales',
|
||||||
|
'Equipamiento deportivo',
|
||||||
|
'Juegos y juguetes',
|
||||||
|
'Maletas y bolsos de viaje',
|
||||||
|
'Material de oficina',
|
||||||
|
'Mobiliario',
|
||||||
|
'Multimedia',
|
||||||
|
'Productos para adultos',
|
||||||
|
'Productos para mascotas y animales',
|
||||||
|
'Ropa y accesorios',
|
||||||
|
'Salud y belleza',
|
||||||
|
'Software',
|
||||||
|
'Vehículos y recambios',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.getAllCategories()
|
||||||
|
if (this.productForm && Object.keys(this.productForm).length > 0) {
|
||||||
|
Object.keys(this.form).forEach((key) => {
|
||||||
|
this.form[key] = this.productForm[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
isValidUrl: dataProcessing.isValidUrl,
|
||||||
|
async getAllCategories() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const data = await $fetch(`/products/all_categories/`,{
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
|
||||||
|
})
|
||||||
|
this.categories = data
|
||||||
|
},
|
||||||
|
parseForm() {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
Object.keys(this.form).forEach((key) => {
|
||||||
|
if ((key === 'tags') | (key === 'attributes')) {
|
||||||
|
formData.append(key, JSON.stringify(this.form[key]))
|
||||||
|
} else if (this.form[key] || this.form[key] === '')
|
||||||
|
formData.append(key, this.form[key])
|
||||||
|
})
|
||||||
|
if (typeof formData.get('image') === 'string') {
|
||||||
|
formData.delete('image')
|
||||||
|
}
|
||||||
|
//console.log('FormData:', formData)
|
||||||
|
return formData
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleImage(e) {
|
||||||
|
this.form.image = e
|
||||||
|
},
|
||||||
|
|
||||||
|
sendProduct() {
|
||||||
|
const formData = this.parseForm()
|
||||||
|
//TODO: review with Diego. I changed formaData with this.form
|
||||||
|
this.$emit('send', formData)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.form {
|
||||||
|
@include desktop {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
@include tablet {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
@include mobile {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imagenInput {
|
||||||
|
font-size: $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
label, .label {
|
||||||
|
text-align: left;
|
||||||
|
color: $color-navy;
|
||||||
|
font-weight: $bold;
|
||||||
|
font-size: $xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $color-grey-inputs;
|
||||||
|
border: 1px solid $color-grey-inputs-border;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 5px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fieldset {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.cont {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
@include mobile {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cont-col {
|
||||||
|
margin: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
margin: 10px 0 15px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.submit-btn {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.help-text {
|
||||||
|
text-align: left;
|
||||||
|
font-size: $xs;
|
||||||
|
text-align: justify;
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-greylayout;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
background-color: $color-light;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: 6px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: $color-error;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
310
components/ProductModal.vue
Normal file
310
components/ProductModal.vue
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="modal">
|
||||||
|
<div v-if="product" class="mask">
|
||||||
|
<div class="wrapper" @click.self="handleEmit">
|
||||||
|
<div class="container">
|
||||||
|
<div class="element header">
|
||||||
|
<h2>Interesado en comprar el producto</h2>
|
||||||
|
<img src="@/assets/img/latienda-lineapuntos-2.svg" alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<form @submit.prevent="sendForm">
|
||||||
|
<div>
|
||||||
|
<BFormGroup class="element">
|
||||||
|
<BFormInput
|
||||||
|
v-model="form.email"
|
||||||
|
required
|
||||||
|
class="input"
|
||||||
|
size="lg"
|
||||||
|
placeholder="Email"
|
||||||
|
/>
|
||||||
|
</BFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<BFormGroup class="element">
|
||||||
|
<BFormInput
|
||||||
|
v-model="form.telephone"
|
||||||
|
required
|
||||||
|
class="input"
|
||||||
|
size="lg"
|
||||||
|
placeholder="Teléfono"
|
||||||
|
/>
|
||||||
|
</BFormGroup>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isAuthenticated" class="element">
|
||||||
|
<BButton
|
||||||
|
class="input"
|
||||||
|
size="lg"
|
||||||
|
variant="outline-primary w-100"
|
||||||
|
@click="redirectToLogin"
|
||||||
|
>Login</BButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="element coop">
|
||||||
|
<!-- <BCard
|
||||||
|
img-src="https://placekitten.com/1000/300"
|
||||||
|
img-alt="Card image"
|
||||||
|
img-left
|
||||||
|
>
|
||||||
|
<BCardText>
|
||||||
|
<h3>Cooperativa</h3>
|
||||||
|
<p>Dirección</p>
|
||||||
|
</BCardText>
|
||||||
|
</BCard> -->
|
||||||
|
<div class="content">
|
||||||
|
<img :src="getImgUrl(product.company.logo)" alt="" />
|
||||||
|
<div class="text">
|
||||||
|
<h3>{{ product?.company?.company_name }}</h3>
|
||||||
|
<p>{{ product?.company?.address }}</p>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="element coop">
|
||||||
|
<!-- <BCard
|
||||||
|
img-src="https://placekitten.com/1000/300"
|
||||||
|
img-alt="Card image"
|
||||||
|
img-left
|
||||||
|
>
|
||||||
|
<BCardText>
|
||||||
|
<h3>Cooperativa</h3>
|
||||||
|
<p>Dirección</p>
|
||||||
|
</BCardText>
|
||||||
|
</BCard> -->
|
||||||
|
<div class="content">
|
||||||
|
<img :src="getImgUrl(product.image)" alt="" />
|
||||||
|
<div class="text">
|
||||||
|
<h3>{{ product?.name }}</h3>
|
||||||
|
<p>{{ product?.price }} €</p>
|
||||||
|
<p class="text-muted">
|
||||||
|
{{ product?.shipping_cost || 'Sin gastos de envío' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="element">
|
||||||
|
<BFormTextarea
|
||||||
|
id="textarea-no-resize"
|
||||||
|
v-model="form.comment"
|
||||||
|
class="input"
|
||||||
|
placeholder="Comentarios"
|
||||||
|
rows="3"
|
||||||
|
no-resize
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<BButton type="submit" class="enviar-button">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="loading"
|
||||||
|
:size="15"
|
||||||
|
:width="2"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
<span v-else>Enviar</span>
|
||||||
|
</BButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
product: { type: Object, default: () => ({}) },
|
||||||
|
},
|
||||||
|
emits: ['closeModal'],
|
||||||
|
setup() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
return {
|
||||||
|
authStore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isAuthenticated: true,
|
||||||
|
form: {
|
||||||
|
email: undefined,
|
||||||
|
telephone: undefined,
|
||||||
|
company: undefined,
|
||||||
|
product: undefined,
|
||||||
|
comment: '',
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.isAuthenticated = this.authStore.isAuthenticated
|
||||||
|
if (this.isAuthenticated) {
|
||||||
|
const email = this.authStore.email
|
||||||
|
this.form.email = email
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async sendForm() {
|
||||||
|
this.form.company = this.product.company.id
|
||||||
|
this.form.product = this.product.id
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
let response
|
||||||
|
let status
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
try {
|
||||||
|
response = await $fetch(`/purchase_email/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: { ...this.form }
|
||||||
|
})
|
||||||
|
status = response.status
|
||||||
|
} catch {
|
||||||
|
status = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
this.$emit('closeModal', status)
|
||||||
|
},
|
||||||
|
getImgUrl(image) {
|
||||||
|
if (image) return image
|
||||||
|
return `@/assets/img/latienda-product-default.svg`
|
||||||
|
},
|
||||||
|
redirectToLogin() {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'login',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleEmit() {
|
||||||
|
this.$emit('closeModal', null)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.mask {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9998;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: table;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 500px;
|
||||||
|
margin: 0px auto;
|
||||||
|
padding: 20px 40px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 7px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header > h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374493;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > img {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
width: 48px;
|
||||||
|
border-right: none;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop {
|
||||||
|
border: 1px lightgray solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop > .content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin: 5px 10px;
|
||||||
|
}
|
||||||
|
.coop > .content > img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.coop > .content > .text > h3,
|
||||||
|
p {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enviar-button {
|
||||||
|
width: 100%;
|
||||||
|
background: #fd6871;
|
||||||
|
border-color: #fd6871;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following styles are auto-applied to elements with
|
||||||
|
* transition="modal" when their visibility is toggled
|
||||||
|
* by Vue.js.
|
||||||
|
*
|
||||||
|
* You can easily play with the modal transition by editing
|
||||||
|
* these styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.modal-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter .modal-container,
|
||||||
|
.modal-leave-active .modal-container {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
86
components/ProductsRelated.vue
Normal file
86
components/ProductsRelated.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row related_products-cards">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="(product, key) in relatedProducts"
|
||||||
|
:key="`related-${key}`"
|
||||||
|
:to="`/productos/${product.id}`"
|
||||||
|
class="col mx-2 related_product-card"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="product.image"
|
||||||
|
:src="product.image"
|
||||||
|
alt=""
|
||||||
|
class="related_product-image"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="image-default"
|
||||||
|
src="@/assets/img/latienda-product-default.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<h3>{{ product.name }}</h3>
|
||||||
|
<span v-if="product.price" class="price">{{ `${product.price}€` }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
relatedProducts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.related_products {
|
||||||
|
background-color: $color-lighter-green;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-default {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
width: 12px;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_product-card {
|
||||||
|
border: 3px solid $color-grey-nav;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
|
||||||
|
.related_product-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
display: block;
|
||||||
|
font-weight: bolder;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container wrapper">
|
<div class="container wrapper">
|
||||||
<form @submit.prevent="search" class="search-container">
|
<form class="search-container" @submit.prevent="search" >
|
||||||
<div class="categorias-wrapper">
|
<div class="categorias-wrapper">
|
||||||
<select v-model="selectedCategory" class="categorias">
|
<select v-model="selectedCategory" class="categorias">
|
||||||
<option selected value="">Todas las categorías</option>
|
<option selected value="">Todas las categorías</option>
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
id="searchbox"
|
id="searchbox"
|
||||||
@focus="focused"
|
|
||||||
@blur="focusedOut"
|
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
class="search-text"
|
class="search-text"
|
||||||
type="text"
|
type="text"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
|
@focus="focused"
|
||||||
|
@blur="focusedOut"
|
||||||
/>
|
/>
|
||||||
<div class="search-link">
|
<div class="search-link">
|
||||||
<img
|
<img
|
||||||
@@ -76,7 +76,7 @@ export default {
|
|||||||
this.startTyping()
|
this.startTyping()
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
this.stopTyping()
|
this.stopTyping()
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -96,8 +96,8 @@ export default {
|
|||||||
let i = 0
|
let i = 0
|
||||||
let word = 0
|
let word = 0
|
||||||
let step = 0
|
let step = 0
|
||||||
let input = document.querySelector('#searchbox')
|
const input = document.querySelector('#searchbox')
|
||||||
let placeholderTexts = [
|
const placeholderTexts = [
|
||||||
'Jabón sólido',
|
'Jabón sólido',
|
||||||
'Huertos de libertad',
|
'Huertos de libertad',
|
||||||
'Hierbabuena',
|
'Hierbabuena',
|
||||||
|
|||||||
181
components/TrendStats.vue
Normal file
181
components/TrendStats.vue
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="values" :style="cssProps" class="trendChart">
|
||||||
|
<div class="data">
|
||||||
|
<div class="data-info">
|
||||||
|
<header>
|
||||||
|
<strong class="title">{{ name }}</strong>
|
||||||
|
</header>
|
||||||
|
<div v-if="info" class="period">
|
||||||
|
<span class="period-text">{{ info?.month }}</span>
|
||||||
|
<strong class="value">{{ info?.value }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart">
|
||||||
|
<ClientOnly>
|
||||||
|
<LineChart :chart-id="chartId" :line-chart-data="dataset" />
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
chartId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'inherit',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentInfo: null,
|
||||||
|
months: [
|
||||||
|
'Enero',
|
||||||
|
'Febrero',
|
||||||
|
'Marzo',
|
||||||
|
'Abril',
|
||||||
|
'Mayo',
|
||||||
|
'Junio',
|
||||||
|
'Julio',
|
||||||
|
'Agosto',
|
||||||
|
'Septiembre',
|
||||||
|
'Octubre',
|
||||||
|
'Noviembre',
|
||||||
|
'Diciembre',
|
||||||
|
],
|
||||||
|
abrevMonths: [
|
||||||
|
'Ene',
|
||||||
|
'Feb',
|
||||||
|
'Mar',
|
||||||
|
'Abr',
|
||||||
|
'May',
|
||||||
|
'Jun',
|
||||||
|
'Jul',
|
||||||
|
'Ago',
|
||||||
|
'Sep',
|
||||||
|
'Oct',
|
||||||
|
'Nov',
|
||||||
|
'Dic',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
dataset() {
|
||||||
|
return {
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: this.abrevMonths
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: this.yAxisValues,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
color: this.color
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cssProps() {
|
||||||
|
return {
|
||||||
|
'--color': this.color,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
info() {
|
||||||
|
const actualMonth = new Date().getMonth();
|
||||||
|
const currentMonthInfo = this.values.sort((a, b) => a.month - b.month)[actualMonth];
|
||||||
|
const res = {
|
||||||
|
month: currentMonthInfo?.month ? this.months[actualMonth] : 'Este mes',
|
||||||
|
value: currentMonthInfo?.value ? currentMonthInfo?.value : 0,
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
yAxisValues(){
|
||||||
|
const ArrValues = [...this.values]
|
||||||
|
const res = [];
|
||||||
|
ArrValues.sort((a, b) => a.month - b.month)
|
||||||
|
ArrValues.forEach(item => {
|
||||||
|
res.push(item.value)
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// onMouseMove(params) {
|
||||||
|
// if (params) {
|
||||||
|
// this.currentInfo = {
|
||||||
|
// month: this.months[params.data[0].month - 1],
|
||||||
|
// value: params.data[0].value,
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// this.currentInfo = null
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.trendChart {
|
||||||
|
border-bottom: 2px solid rgba(var(--color), 0.2);
|
||||||
|
.title {
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
.data {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
.period {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.chart {
|
||||||
|
width: 40%;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stroke {
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke: var(--color);
|
||||||
|
}
|
||||||
|
.fill {
|
||||||
|
opacity: 0.2;
|
||||||
|
fill: var(--color);
|
||||||
|
}
|
||||||
|
.active-line {
|
||||||
|
stroke: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.point {
|
||||||
|
display: none;
|
||||||
|
fill: var(--color);
|
||||||
|
stroke: var(--color);
|
||||||
|
}
|
||||||
|
.point.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
example copy.env
Normal file
8
example copy.env
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
API = 'http://0.0.0.0:8000/'
|
||||||
|
API_V1 = '/api/v1/'
|
||||||
|
BASE_URL= $API$API_V1
|
||||||
|
GOOGLE_CLIENT_ID='1076928279923-s6hdbpcpbeaeqnact5t5kmktrkaipndq.apps.googleusercontent.com'
|
||||||
|
FACEBOOK_ID='751687992155393'
|
||||||
|
|
||||||
|
GOOGLE_MAPS_API_KEY = 'AIzaSyDX_8JveVk4S5cnDcMHijmOO9HGj5OJHmQ'
|
||||||
|
GOOGLE_ANALYTICS_ID = 'G-QT1WE1L619'
|
||||||
@@ -5,20 +5,32 @@
|
|||||||
<NuxtLink to="/admin/cooperativas"> Administrar cooperativas </NuxtLink>
|
<NuxtLink to="/admin/cooperativas"> Administrar cooperativas </NuxtLink>
|
||||||
<NuxtLink to="/admin/importar"> Alta de cooperativas por .csv </NuxtLink>
|
<NuxtLink to="/admin/importar"> Alta de cooperativas por .csv </NuxtLink>
|
||||||
<NuxtLink to="/admin/estadisticas"> Estadísticas </NuxtLink>
|
<NuxtLink to="/admin/estadisticas"> Estadísticas </NuxtLink>
|
||||||
<NuxtLink to="/" @click="logout">Cerrar sesión</NuxtLink>
|
<button @click="handleLogout" class="logout-link">Cerrar sesión</button>
|
||||||
|
<!-- <NuxtLink to="/" @click="handleLogout">Cerrar sesión</NuxtLink> -->
|
||||||
</div>
|
</div>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { mapActions } from 'pinia'
|
import { mapActions } from 'pinia'
|
||||||
export default {
|
export default {
|
||||||
|
setup() {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
return {
|
||||||
|
auth,
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('auth', ['logout']),
|
...mapActions(useAuthStore, ['logout']),
|
||||||
async logout() {
|
async handleLogout() {
|
||||||
|
try {
|
||||||
await this.logout()
|
await this.logout()
|
||||||
|
this.$router.push('/')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error logging out:', error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -30,7 +42,7 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px 20px;
|
padding: 20px 20px;
|
||||||
|
|
||||||
a {
|
a, button {
|
||||||
font-size: $m;
|
font-size: $m;
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
color: $color-navy;
|
color: $color-navy;
|
||||||
@@ -38,7 +50,11 @@ export default {
|
|||||||
}
|
}
|
||||||
a:nth-child(1):after,
|
a:nth-child(1):after,
|
||||||
a:nth-child(2):after,
|
a:nth-child(2):after,
|
||||||
a:nth-child(3):after {
|
a:nth-child(3):after,
|
||||||
|
button:nth-child(1):after,
|
||||||
|
button:nth-child(2):after,
|
||||||
|
button:nth-child(3):after,
|
||||||
|
button:nth-child(4):after {
|
||||||
color: $color-navy;
|
color: $color-navy;
|
||||||
content: '\22EE';
|
content: '\22EE';
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
export default defineNuxtRouteMiddleware((to, from) => {
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
//TODO: remove logs
|
// console.log('🔍 Middleware ejecutado')
|
||||||
console.log('🔍 Middleware ejecutado')
|
// console.log('📦 to.meta:', to.meta)
|
||||||
console.log('📦 to.meta:', to.meta)
|
|
||||||
const AUTH_ROLES = {
|
const AUTH_ROLES = {
|
||||||
ANON: 0,
|
ANON: 0,
|
||||||
SHOP_USER: 1,
|
SHOP_USER: 1,
|
||||||
@@ -11,12 +10,12 @@ export default defineNuxtRouteMiddleware((to, from) => {
|
|||||||
}
|
}
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const userRole = authStore.role
|
const userRole = authStore.role
|
||||||
console.log('👤 Rol actual:', userRole)
|
//console.log('👤 Rol actual:', userRole)
|
||||||
|
|
||||||
const authority = to.meta?.auth?.authority as keyof typeof AUTH_ROLES
|
const authority = to.meta?.auth?.authority as keyof typeof AUTH_ROLES
|
||||||
const requiredLevel = AUTH_ROLES[authority]
|
const requiredLevel = AUTH_ROLES[authority]
|
||||||
//const required = to.meta.auth?.authority
|
//const required = to.meta.auth?.authority
|
||||||
console.log('⚠️ Autoridad requerida:', authority, requiredLevel)
|
//console.log('⚠️ Autoridad requerida:', authority, requiredLevel)
|
||||||
|
|
||||||
|
|
||||||
// Check if user is connected first
|
// Check if user is connected first
|
||||||
@@ -25,18 +24,18 @@ export default defineNuxtRouteMiddleware((to, from) => {
|
|||||||
// Get authorizations for matched routes (with children routes too)
|
// Get authorizations for matched routes (with children routes too)
|
||||||
|
|
||||||
const userLevel = AUTH_ROLES[userRole as keyof typeof AUTH_ROLES]
|
const userLevel = AUTH_ROLES[userRole as keyof typeof AUTH_ROLES]
|
||||||
console.log('🧮 userLevel:', userLevel, 'requiredLevel:', requiredLevel)
|
//console.log('🧮 userLevel:', userLevel, 'requiredLevel:', requiredLevel)
|
||||||
|
|
||||||
console.log('[Auth Middleware]', {
|
// console.log('[Auth Middleware]', {
|
||||||
to: to.path,
|
// to: to.path,
|
||||||
meta: to.meta,
|
// meta: to.meta,
|
||||||
userRole: authStore.role,
|
// userRole: authStore.role,
|
||||||
requiredLevel,
|
// requiredLevel,
|
||||||
userLevel,
|
// userLevel,
|
||||||
})
|
// })
|
||||||
|
|
||||||
if (userLevel < requiredLevel) {
|
if (userLevel < requiredLevel) {
|
||||||
console.log('🚫 Bloqueando acceso - redirigiendo a /login')
|
// console.log('🚫 Bloqueando acceso - redirigiendo a /login')
|
||||||
return navigateTo('/login')
|
return navigateTo('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { $fetch } from 'ofetch'
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
components: true,
|
components: true,
|
||||||
|
ssr: true,
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
@@ -38,6 +39,10 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
'~/plugins/google-analytics.client.ts',
|
||||||
|
],
|
||||||
|
|
||||||
sitemap: {
|
sitemap: {
|
||||||
exclude: ['/admin', '/admin/**', '/editar', '/editar/**'],
|
exclude: ['/admin', '/admin/**', '/editar', '/editar/**'],
|
||||||
urls: async () => {
|
urls: async () => {
|
||||||
|
|||||||
120
package-lock.json
generated
120
package-lock.json
generated
@@ -12,11 +12,16 @@
|
|||||||
"@nuxt/eslint": "^1.8.0",
|
"@nuxt/eslint": "^1.8.0",
|
||||||
"@nuxtjs/sitemap": "^7.4.3",
|
"@nuxtjs/sitemap": "^7.4.3",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.32.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"nuxt": "^3.17.7",
|
"nuxt": "^3.17.7",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
|
"vue-advanced-cropper": "^2.8.9",
|
||||||
|
"vue-gtag": "^3.5.2",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue3-carousel": "^0.16.0"
|
"vue3-carousel": "^0.16.0"
|
||||||
},
|
},
|
||||||
@@ -4321,6 +4326,13 @@
|
|||||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/yauzl": {
|
"node_modules/@types/yauzl": {
|
||||||
"version": "2.10.3",
|
"version": "2.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
@@ -6035,6 +6047,12 @@
|
|||||||
"consola": "^3.2.3"
|
"consola": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classnames": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/clean-regexp": {
|
"node_modules/clean-regexp": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
|
||||||
@@ -6713,6 +6731,12 @@
|
|||||||
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/debounce": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -7027,6 +7051,15 @@
|
|||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/domutils": {
|
"node_modules/domutils": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
@@ -7094,6 +7127,28 @@
|
|||||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/easy-bem": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/easy-bem/-/easy-bem-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/echarts/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -9247,6 +9302,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -11033,9 +11097,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pinia-plugin-persistedstate": {
|
"node_modules/pinia-plugin-persistedstate": {
|
||||||
"version": "4.4.1",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.5.0.tgz",
|
||||||
"integrity": "sha512-lmuMPpXla2zJKjxEq34e1E9P9jxkWEhcVwwioCCE0izG45kkTOvQfCzvwhW3i38cvnaWC7T1eRdkd15Re59ldw==",
|
"integrity": "sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-pick-omit": "^1.2.1",
|
"deep-pick-omit": "^1.2.1",
|
||||||
@@ -14014,6 +14078,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-advanced-cropper": {
|
||||||
|
"version": "2.8.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-advanced-cropper/-/vue-advanced-cropper-2.8.9.tgz",
|
||||||
|
"integrity": "sha512-1jc5gO674kVGpJKekoaol6ZlwaF5VYDLSBwBOUpViW0IOrrRsyLw6XNszjEqgbavvqinlKNS6Kqlom3B5M72Tw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"debounce": "^1.2.0",
|
||||||
|
"easy-bem": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-bundle-renderer": {
|
"node_modules/vue-bundle-renderer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.2.tgz",
|
||||||
@@ -14052,6 +14134,21 @@
|
|||||||
"eslint": "^8.57.0 || ^9.0.0"
|
"eslint": "^8.57.0 || ^9.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-gtag": {
|
||||||
|
"version": "3.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-gtag/-/vue-gtag-3.5.2.tgz",
|
||||||
|
"integrity": "sha512-efTY4yrkNraFSu6CZqhFZLX5LggqCr44d6kcPnPPtzYhvu5ywrTFUnuvM2Vm238QC+YT43HkVEXV/L1OYnHvNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vue-router": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.5.1",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||||
@@ -14576,6 +14673,21 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,16 @@
|
|||||||
"@nuxt/eslint": "^1.8.0",
|
"@nuxt/eslint": "^1.8.0",
|
||||||
"@nuxtjs/sitemap": "^7.4.3",
|
"@nuxtjs/sitemap": "^7.4.3",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
|
"echarts": "^6.0.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.32.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"nuxt": "^3.17.7",
|
"nuxt": "^3.17.7",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
|
"vue-advanced-cropper": "^2.8.9",
|
||||||
|
"vue-gtag": "^3.5.2",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue3-carousel": "^0.16.0"
|
"vue3-carousel": "^0.16.0"
|
||||||
},
|
},
|
||||||
|
|||||||
180
pages/admin/cooperativas.vue
Normal file
180
pages/admin/cooperativas.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!loading" class="container" >
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="activeModal"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
:ok-variant="modalColor"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-10">
|
||||||
|
<h1 class="title">Administrar cooperativas</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="companies">
|
||||||
|
<v-data-table
|
||||||
|
v-model="selectedItemsIndexes"
|
||||||
|
show-select
|
||||||
|
:single-select="false"
|
||||||
|
:headers="headers"
|
||||||
|
:search="search"
|
||||||
|
:items="companies"
|
||||||
|
:loading="loading"
|
||||||
|
loading-text="Cargando cooperativas..."
|
||||||
|
>
|
||||||
|
<template #top>
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<!-- Search -->
|
||||||
|
<v-text-field
|
||||||
|
v-model="search"
|
||||||
|
append-icon="mdi-magnify"
|
||||||
|
label="Buscar cooperativa"
|
||||||
|
single-line
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<!-- . Search -->
|
||||||
|
</v-toolbar>
|
||||||
|
</template>
|
||||||
|
<template #[`item.company_name`]="item">
|
||||||
|
<a :href="`/c/${item.item.id}`" target="_blank" class="mr-2">
|
||||||
|
{{ item.item.company_name }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template #[`item.is_validated`]="item">
|
||||||
|
<v-icon v-if="item.item.is_validated" small class="mr-2 validated">
|
||||||
|
mdi-check
|
||||||
|
</v-icon>
|
||||||
|
<v-icon v-else small class="mr-2 not-validated"> mdi-close </v-icon>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<button class="submit-btn" @click="validateCompanies">Validar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'admin',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'SITE_ADMIN' },
|
||||||
|
})
|
||||||
|
const auth = useAuthStore();
|
||||||
|
return {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search: '',
|
||||||
|
companies: [],
|
||||||
|
selectedItemsIndexes: [],
|
||||||
|
loading: true,
|
||||||
|
headers: [
|
||||||
|
{ text: 'Nombre', value: 'company_name' },
|
||||||
|
{ text: 'C.I.F', value: 'cif' },
|
||||||
|
{ text: 'Ciudad', value: 'city' },
|
||||||
|
{ text: 'Email', value: 'email' },
|
||||||
|
{ text: 'Validada', value: 'is_validated' },
|
||||||
|
],
|
||||||
|
modalText: '',
|
||||||
|
modalColor: '',
|
||||||
|
activeModal: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedItems() {
|
||||||
|
const itemsArr = []
|
||||||
|
this.selectedItemsIndexes.forEach(index => {
|
||||||
|
this.companies.forEach(item => {
|
||||||
|
if (item.id === index) {
|
||||||
|
itemsArr.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return itemsArr
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try{
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const data = await $fetch('admin_companies/', {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.companies = data
|
||||||
|
this.loading = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching companies:', error)
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async validateCompanies() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
try {
|
||||||
|
await this.selectedItems.forEach(async (item) => {
|
||||||
|
await $fetch(`admin_companies/${item.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
is_validated: true,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const index = this.companies.indexOf(item)
|
||||||
|
this.companies[index].is_validated = true
|
||||||
|
this.selectedItemsIndexes = []
|
||||||
|
this.modalText = 'Los productos han sido eliminados correctamente.'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error validating companies:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $xxl;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background-color: $color-orange;
|
||||||
|
color: $color-light;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validated {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
.not-validated {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
110
pages/admin/estadisticas.vue
Normal file
110
pages/admin/estadisticas.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row general">
|
||||||
|
<div class="text-center col-6">
|
||||||
|
<h2>Cooperativas</h2>
|
||||||
|
<p class="general-value">
|
||||||
|
<strong>{{ companiesCount }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center col-6">
|
||||||
|
<h2>Productos</h2>
|
||||||
|
<p class="general-value">
|
||||||
|
<strong>{{ productsCount }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<trend-stats
|
||||||
|
:chart-id="`1`"
|
||||||
|
:color="`#39af77`"
|
||||||
|
:name="`Nuevas cooperativas`"
|
||||||
|
:values="companiesTimeline"
|
||||||
|
/>
|
||||||
|
<trend-stats
|
||||||
|
:chart-id="`2`"
|
||||||
|
:color="`red`"
|
||||||
|
:name="`Nuevos productos`"
|
||||||
|
:values="productsTimeline"
|
||||||
|
/>
|
||||||
|
<trend-stats
|
||||||
|
:chart-id="`3`"
|
||||||
|
:color="`purple`"
|
||||||
|
:name="`Nuevos usuarios`"
|
||||||
|
:values="usersTimeline"
|
||||||
|
/>
|
||||||
|
<trend-stats
|
||||||
|
:chart-id="`4`"
|
||||||
|
:color="`blue`"
|
||||||
|
:name="`Contactados`"
|
||||||
|
:values="contactTimeline"
|
||||||
|
/>
|
||||||
|
<trend-stats
|
||||||
|
:chart-id="`5`"
|
||||||
|
:color="`orange`"
|
||||||
|
:name="`Redirecciones a producto`"
|
||||||
|
:values="shoppingTimeline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'admin',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'SITE_ADMIN' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
return { auth }
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
companiesCount: 0,
|
||||||
|
productsCount: 0,
|
||||||
|
companiesTimeline: [],
|
||||||
|
productsTimeline: [],
|
||||||
|
usersTimeline: [],
|
||||||
|
contactTimeline: [],
|
||||||
|
shoppingTimeline: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
try {
|
||||||
|
const stats = await $fetch('/admin_stats/', {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (stats) {
|
||||||
|
this.companiesCount = stats.company_count
|
||||||
|
this.productsCount = stats.product_count
|
||||||
|
this.companiesTimeline = stats.companies_timeline
|
||||||
|
this.productsTimeline = stats.products_timeline
|
||||||
|
this.usersTimeline = stats.users_timeline
|
||||||
|
this.contactTimeline = stats.contact_timeline
|
||||||
|
this.shoppingTimeline = stats.shopping_timeline
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching admin stats:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.general-value {
|
||||||
|
font-size: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
114
pages/admin/importar.vue
Normal file
114
pages/admin/importar.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="activeModal"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
:ok-variant="modalColor"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-10"></div>
|
||||||
|
<h1 class="title">Importar productos desde CSV</h1>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="submitFile">
|
||||||
|
<div class="cont-col">
|
||||||
|
<label for="file"> Archivo .csv</label>
|
||||||
|
<input
|
||||||
|
id="file"
|
||||||
|
type="file"
|
||||||
|
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||||
|
placeholder="Elige un archivo"
|
||||||
|
required
|
||||||
|
@change="handleFile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
<SubmitButton text="Importar" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'admin',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'SITE_ADMIN' },
|
||||||
|
})
|
||||||
|
const auth = useAuthStore();
|
||||||
|
return {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
file: null,
|
||||||
|
error: null,
|
||||||
|
modalText: '',
|
||||||
|
modalColor: '',
|
||||||
|
activeModal: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async handleFile(e) {
|
||||||
|
const selectedFile = await e.target.files[0]
|
||||||
|
this.file = selectedFile
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitFile() {
|
||||||
|
this.error = null
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('csv_file', this.file)
|
||||||
|
//TODO: Review functionality
|
||||||
|
try {
|
||||||
|
await $fetch('/load_coops/',{
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.modalText = 'Productos importados correctamente'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
} catch (error) {
|
||||||
|
this.error = 'Ha habido un error'
|
||||||
|
console.error('Error importing products:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $xxl;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
.cont-col {
|
||||||
|
margin: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
text-align: left;
|
||||||
|
color: $color-navy;
|
||||||
|
font-weight: $bold;
|
||||||
|
font-size: $xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $color-error;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
29
pages/admin/index.vue
Normal file
29
pages/admin/index.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<BContainer class="admin-dashboard">
|
||||||
|
<h1>Panel de Administración</h1>
|
||||||
|
<p>Bienvenido al panel de administración. Aquí puedes gestionar la configuración del sitio y los permisos de los usuarios.</p>
|
||||||
|
</BContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'admin',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'SITE_ADMIN' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.admin-dashboard {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-dashboard h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,18 +1,430 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="container mt-5">
|
||||||
pagina busqueda
|
<div class="row">
|
||||||
<h1>Busqueda</h1>
|
<div class="col-md-3">
|
||||||
<p>Esta es la página de búsqueda.</p>
|
<ProductFilter
|
||||||
<p>Utiliza el componente NavBarSearch para navegar.</p>
|
:filters="filters"
|
||||||
|
:current-filters="currentFilters"
|
||||||
|
:prices="prices"
|
||||||
|
:geo="coordinates"
|
||||||
|
@apply-filters="updateData"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="loadingProducts" class="col-md-9 loading-spinner">
|
||||||
|
<BSpinner />
|
||||||
|
<span>Cargando productos...</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loadingProducts" class="col-md-9">
|
||||||
|
<div class="carousel">
|
||||||
|
<h2 class="title">Últimos productos</h2>
|
||||||
|
<ItemsRow class="items" :type="`product`" :items="carouselProducts.results" />
|
||||||
|
</div>
|
||||||
|
<div v-if="hasFilterTags" class="applied-filters">
|
||||||
|
<h2 class="title">FILTROS APLICADOS</h2>
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<button
|
||||||
|
v-if="appliedFilters.hasOwnProperty('shipping_cost')"
|
||||||
|
type="button"
|
||||||
|
class="btn-tag"
|
||||||
|
@click="removeFilter('shipping_cost')"
|
||||||
|
>
|
||||||
|
<span>Sin gastos de envío</span>
|
||||||
|
<img src="@/assets/img/latienda-close.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="appliedFilters.hasOwnProperty('discount')"
|
||||||
|
type="button"
|
||||||
|
class="btn-tag"
|
||||||
|
@click="removeFilter('discount')"
|
||||||
|
>
|
||||||
|
<span>Descuentos</span>
|
||||||
|
<img src="@/assets/img/latienda-close.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
<div v-if="appliedFilters.hasOwnProperty('category')">
|
||||||
|
<button
|
||||||
|
v-for="(cat, key) in appliedFilters.category"
|
||||||
|
:key="key"
|
||||||
|
type="button"
|
||||||
|
class="btn-delete-all"
|
||||||
|
@click="removeCategory(cat)"
|
||||||
|
>
|
||||||
|
<span>{{ cat }}</span>
|
||||||
|
<img src="@/assets/img/latienda-close.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="results">
|
||||||
|
<h2 class="title"></h2>
|
||||||
|
<p class="count">Hay {{ count }} productos</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="products.length !== 0">
|
||||||
|
<div v-for="product in products" :key="product.id">
|
||||||
|
<ProductCard :key="product.key" :product="product" />
|
||||||
|
</div>
|
||||||
|
<BPagination
|
||||||
|
v-model="currentPage"
|
||||||
|
class="pagination"
|
||||||
|
:total-rows="count"
|
||||||
|
:per-page="perPage"
|
||||||
|
@change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-results">
|
||||||
|
<p>
|
||||||
|
No hemos encontrado resultados para su búsqueda... pero puede buscar
|
||||||
|
otro o consultar estos productos.
|
||||||
|
</p>
|
||||||
|
<div v-for="product in defaultProducts" :key="product.id">
|
||||||
|
<ProductCard :key="product.key" :product="product" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- appliedFilters: {{appliedFilters}} <br>
|
||||||
|
filters: {{filters}} <br>
|
||||||
|
prices: {{prices}} <br>
|
||||||
|
coordinates: {{coordinates}} <br>
|
||||||
|
products: {{products}} <br>
|
||||||
|
defaultProducts: {{defaultProducts}} <br>
|
||||||
|
carouselProducts: {{carouselProducts}} <br>
|
||||||
|
count: {{count}} -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import serverSearch from '~/utils/serverSearch'
|
||||||
|
import clientSearch from '~/utils/clientSearch'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup(){
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'mainbanner',
|
||||||
|
})
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
return { auth }
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchText: '',
|
||||||
|
currentPage: 1,
|
||||||
|
perPage: 10,
|
||||||
|
filterTags: {
|
||||||
|
shipping_cost: undefined,
|
||||||
|
},
|
||||||
|
previousParams: null,
|
||||||
|
currentFilters: null,
|
||||||
|
mountedProducts: [],
|
||||||
|
appliedFilters: {},
|
||||||
|
filters: {},
|
||||||
|
prices: { min: null, max: null },
|
||||||
|
coordinates: null,
|
||||||
|
products: [],
|
||||||
|
defaultProducts: [],
|
||||||
|
carouselProducts: [],
|
||||||
|
count: 0,
|
||||||
|
loadingProducts: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hasFilterTags() {
|
||||||
|
if (!this.appliedFilters) return false
|
||||||
|
return (
|
||||||
|
Object.keys(this.appliedFilters).includes('shipping_cost') ||
|
||||||
|
Object.keys(this.appliedFilters).includes('discount') ||
|
||||||
|
(Array.isArray(this.appliedFilters.category) &&
|
||||||
|
this.appliedFilters.category.length > 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'$route.query'(newValue) {
|
||||||
|
//console.log('New Value:', newValue)
|
||||||
|
//console.log('Route changed:', this.$route.fullPath)
|
||||||
|
//console.log('Current params:', this.$route.query)
|
||||||
|
this.updateData(newValue)
|
||||||
|
Object.assign(this.$data, this.$options.data())
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const params = import.meta.client ? clientSearch(this.$route.query) : serverSearch(this.$route.query)
|
||||||
|
const data = await $fetch(`/search_products/?`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
params: params,
|
||||||
|
headers: {
|
||||||
|
Authorization: '/',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
//console.log('data', data)
|
||||||
|
|
||||||
|
const products = data.products
|
||||||
|
//console.log('products', products)
|
||||||
|
let defaultProducts = []
|
||||||
|
if (products.length === 0) {
|
||||||
|
//console.log('no products, fetching default')
|
||||||
|
const data = await $fetch(`/search_products/?q=${params.q}`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
order: 'newest',
|
||||||
|
limit: 10,
|
||||||
|
offset: 0
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: '/',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defaultProducts = data.products
|
||||||
|
}
|
||||||
|
|
||||||
|
const carouselProducts = await $fetch(`/products/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
limit: 10,
|
||||||
|
offset: 0
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: '/',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
//console.log('carouselProducts', carouselProducts)
|
||||||
|
|
||||||
|
let coordinates
|
||||||
|
if (params.latitude && params.longitude) {
|
||||||
|
coordinates = {
|
||||||
|
lat: Number(params.latitude),
|
||||||
|
lng: Number(params.longitude),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prices
|
||||||
|
if (params.price_min || params.price_max) {
|
||||||
|
prices = { max: params.price_max, min: params.price_min }
|
||||||
|
} else if (data.prices.min || data.prices.max) {
|
||||||
|
prices = data.prices
|
||||||
|
} else {
|
||||||
|
prices = { max: null, min: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appliedFilters = params.q
|
||||||
|
this.filters = data.filters
|
||||||
|
this.prices = prices
|
||||||
|
this.coordinates = coordinates
|
||||||
|
this.products = products
|
||||||
|
this.defaultProducts = defaultProducts
|
||||||
|
this.carouselProducts = carouselProducts
|
||||||
|
this.count = data.count
|
||||||
|
this.loadingProducts = false
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.currentFilters = this.appliedFilters
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async handlePageChange(value) {
|
||||||
|
const offset = (value - 1) * this.perPage
|
||||||
|
this.products = await this.getMoreProducts(offset)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getMoreProducts(offset) {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const data = await $fetch(`/search_products/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
...this.appliedFilters,
|
||||||
|
limit: 10,
|
||||||
|
offset: offset
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: '/',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return data.products
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateData(value) {
|
||||||
|
//console.log('updateData called with:', value)
|
||||||
|
const filters = { q: this.appliedFilters.q }
|
||||||
|
const query = Object.keys(value).length === 0 ? { ...filters } : { ...value, ...filters }
|
||||||
|
|
||||||
|
//console.log('Navigating to busqueda with query:', query)
|
||||||
|
this.$router.push({ name: 'busqueda', query })
|
||||||
|
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const data = await $fetch('/search_products/', {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
params: query,
|
||||||
|
headers: { Authorization: '/' },
|
||||||
|
})
|
||||||
|
this.products = data.products
|
||||||
|
this.count = data.count
|
||||||
|
this.loadingProducts = false
|
||||||
|
},
|
||||||
|
|
||||||
|
removeCategory(cat) {
|
||||||
|
this.currentFilters = this.appliedFilters
|
||||||
|
const categoryArray = this.currentFilters.category
|
||||||
|
const newCats = []
|
||||||
|
categoryArray.forEach((element) => {
|
||||||
|
if (element !== cat) {
|
||||||
|
newCats.push(element)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.currentFilters.category = newCats
|
||||||
|
const noCategory = {}
|
||||||
|
Object.entries(this.currentFilters).forEach(([key, value]) => {
|
||||||
|
if (key !== 'category') {
|
||||||
|
noCategory[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (newCats.length === 0) {
|
||||||
|
return this.$router.push({
|
||||||
|
path: this.$route.path,
|
||||||
|
query: { ...noCategory },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return this.$router.push({
|
||||||
|
path: this.$route.path,
|
||||||
|
query: { category: newCats, ...noCategory },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFilter(filter) {
|
||||||
|
this.currentFilters = { ...this.appliedFilters }
|
||||||
|
this.currentFilters = Object.fromEntries(
|
||||||
|
Object.entries(this.currentFilters).filter(([key]) => key !== filter)
|
||||||
|
)
|
||||||
|
|
||||||
|
return this.$router.push({
|
||||||
|
name: 'busqueda',
|
||||||
|
query: { ...this.currentFilters },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
search() {
|
||||||
|
if (this.searchText) {
|
||||||
|
return this.$router.push({
|
||||||
|
name: 'busqueda',
|
||||||
|
query: { q: this.searchText },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.ad {
|
||||||
|
margin: 40px auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
background-color: $color-grey-nav;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.applied-filters {
|
||||||
|
@include mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: $xl;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: $color-light;
|
||||||
|
background-color: $color-dark-green;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 18px;
|
||||||
|
margin-left: 5px;
|
||||||
|
position: relative;
|
||||||
|
bottom: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-all {
|
||||||
|
background-color: $color-darker-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel {
|
||||||
|
@include mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: $xl;
|
||||||
|
color: $color-navy;
|
||||||
|
@include tablet {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.items {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.results {
|
||||||
|
.title {
|
||||||
|
font-size: $xl;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
.count {
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
margin-top: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
font-size: $xl;
|
||||||
|
color: $color-navy;
|
||||||
|
margin-top: 100px;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
415
pages/c/[id].vue
415
pages/c/[id].vue
@@ -1,15 +1,426 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row c-description">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="c-image-container">
|
||||||
|
<img v-if="coop?.logo" :src="coop?.logo" alt="" />
|
||||||
|
<img v-else src="@/assets/img/latienda-product-default.svg" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h1 class="coop-name">{{ coop?.company_name }}</h1>
|
||||||
|
<p class="coop-text">
|
||||||
|
{{ coop?.description }}
|
||||||
|
</p>
|
||||||
|
<div class="tags_container">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="n in coop?.tags"
|
||||||
|
:key="n"
|
||||||
|
:to="tagRoute(n)"
|
||||||
|
class="tag_container"
|
||||||
|
>
|
||||||
|
<img class="tag_img" src="@/assets/img/latienda-tag.svg" />
|
||||||
|
<span>{{ n }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row coop-links">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<BButton
|
||||||
|
v-if="coop?.shop_link"
|
||||||
|
class="div-link"
|
||||||
|
align="center"
|
||||||
|
:href="coop?.shop_link"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img class="div-link-img" src="@/assets/img/latienda-tienda.svg" />
|
||||||
|
<span>Tienda online</span>
|
||||||
|
</BButton>
|
||||||
|
<BButton
|
||||||
|
v-if="coop?.web_link"
|
||||||
|
class="div-link"
|
||||||
|
align="center"
|
||||||
|
:href="coop?.web_link"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<img class="div-link-img" src="@/assets/img/latienda-web.svg" />
|
||||||
|
<span>Página web</span>
|
||||||
|
</BButton>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div v-if="coop?.mobile || coop?.phone" class="div-action tel">
|
||||||
|
<img
|
||||||
|
class="div-action-img"
|
||||||
|
src="@/assets/img/latienda-telefono.svg"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
>{{ coop?.phone }} <br />
|
||||||
|
{{ coop?.mobile }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-if="coop?.email" class="div-action mail">
|
||||||
|
<img class="div-action-img" src="@/assets/img/latienda-email.svg" />
|
||||||
|
<span>{{ coop?.email }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div v-if="coop?.address" class="div-action address">
|
||||||
|
<img class="div-action-img" src="@/assets/img/latienda-casa.svg" />
|
||||||
|
<span>{{ coop?.address }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="coop?.city" class="div-action location">
|
||||||
|
<img
|
||||||
|
class="div-action-img"
|
||||||
|
src="@/assets/img/latienda-ubicacion.svg"
|
||||||
|
/>
|
||||||
|
<span>{{ coop?.city }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 c-tabs">
|
||||||
<div>
|
<div>
|
||||||
c/id page
|
<BCard no-body>
|
||||||
|
<BTabs card>
|
||||||
|
<BTab title="Devoluciones, garantías y reembolsos" active>
|
||||||
|
<BCardText>
|
||||||
|
{{
|
||||||
|
coop?.sale_terms ||
|
||||||
|
'Consultar con la cooperativa para más información'
|
||||||
|
}}
|
||||||
|
</BCardText>
|
||||||
|
</BTab>
|
||||||
|
<BTab title="Envío">
|
||||||
|
<BCardText>
|
||||||
|
{{
|
||||||
|
coop?.shipping_terms ||
|
||||||
|
'Consultar con la cooperativa para más información'
|
||||||
|
}}
|
||||||
|
</BCardText>
|
||||||
|
</BTab>
|
||||||
|
</BTabs>
|
||||||
|
</BCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="slicedProducts.length !== 0">
|
||||||
|
{{ slicedProducts }} productos encontrados
|
||||||
|
<div v-for="product in slicedProducts" :key="product.id">
|
||||||
|
<ProductCard :key="product.key" :product="product" />
|
||||||
|
</div>
|
||||||
|
<BPagination
|
||||||
|
v-model="currentPage"
|
||||||
|
:v-if="products"
|
||||||
|
class="pagination"
|
||||||
|
:total-rows="rows"
|
||||||
|
:per-page="perPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState } from 'pinia'
|
||||||
export default {
|
export default {
|
||||||
|
setup(){
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'main',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
//TODO: implement head() method
|
||||||
|
// head() {
|
||||||
|
// return {
|
||||||
|
// title: `latienda.coop | ${this.coop?.company_name}`,
|
||||||
|
// meta: [
|
||||||
|
// {
|
||||||
|
// hid: 'description',
|
||||||
|
// name: 'description',
|
||||||
|
// content: this.coop?.description,
|
||||||
|
// },
|
||||||
|
// { property: 'og:title', content: this.coop?.company_name },
|
||||||
|
// { property: 'og:description', content: this.coop?.description },
|
||||||
|
// { property: 'og:image', content: this.coop?.logo },
|
||||||
|
// { property: 'og:url', content: this.coop?.web_link },
|
||||||
|
// { name: 'twitter:card', content: 'summary_large_image' },
|
||||||
|
// ],
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
coop: null,
|
||||||
|
products: null,
|
||||||
|
currentPage: 1,
|
||||||
|
perPage: 10,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAuthStore, ['id', 'access']),
|
||||||
|
rows() {
|
||||||
|
return this.products?.length
|
||||||
|
},
|
||||||
|
slicedProducts() {
|
||||||
|
const initial = (this.currentPage - 1) * this.perPage
|
||||||
|
const final = this.currentPage * this.perPage
|
||||||
|
const items = this.products ? this.products?.slice(initial, final) : []
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const $route = useRoute()
|
||||||
|
this.coop = await $fetch(`/companies/${$route.params.id}/`,
|
||||||
|
{
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.products = await $fetch(
|
||||||
|
`/products?company=${route.params.id}`,
|
||||||
|
{
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.sendLog('view')
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
tagRoute(tag) {
|
||||||
|
return `/busqueda?tags=${tag}`
|
||||||
|
},
|
||||||
|
async getPosition() {
|
||||||
|
const geoLocation = () =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(posData) => {
|
||||||
|
resolve(posData)
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const position = await geoLocation()
|
||||||
|
const geo = {
|
||||||
|
latitude: position.coords.latitude,
|
||||||
|
longitude: position.coords.longitude,
|
||||||
|
}
|
||||||
|
return geo
|
||||||
|
} catch {
|
||||||
|
const geo = null
|
||||||
|
return geo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendLog(action) {
|
||||||
|
const geo = await this.getPosition()
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const accessToken = this.access
|
||||||
|
const data = await $fetch('https://api.ipify.org?format=json', {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
const ip = data.ip
|
||||||
|
const object = {
|
||||||
|
action: action,
|
||||||
|
action_object: {
|
||||||
|
model: 'company',
|
||||||
|
id: this.coop?.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
//console.log('Sending log OBJECT:', object)
|
||||||
|
if (ip) object.ip = ip
|
||||||
|
if (geo) object.geo = geo
|
||||||
|
try {
|
||||||
|
//TODO: review problems with 406 error backend
|
||||||
|
await $fetch(`/stats/me/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: object,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error /stats:', error)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending log:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
//global classes for Bootstrap styling
|
||||||
|
ul.nav > li.nav-item {
|
||||||
|
font-weight: bolder;
|
||||||
|
:hover {
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
border-top: 4px solid $color-navy;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 80px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
padding: 0.4em 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-description {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
@include mobile {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop-links {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
.div-action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.img-tel {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-link-img,
|
||||||
|
.div-action-img {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-action-img {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-image-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop-name {
|
||||||
|
font-size: $xl;
|
||||||
|
color: $color-navy;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.coop-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-link:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 99, 99, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-link {
|
||||||
|
width: 100%;
|
||||||
|
border: 3px solid $color-orange;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: $color-light;
|
||||||
|
font-weight: $bold;
|
||||||
|
padding: 15px 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $color-orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-action {
|
||||||
|
width: 100%;
|
||||||
|
background-color: $color-grey-nav;
|
||||||
|
border: none;
|
||||||
|
color: $color-greytext;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
padding: 20px 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag_container {
|
||||||
|
margin: 5px 6px 0 0;
|
||||||
|
border: 2px solid $color-greylayout;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
|
||||||
|
.tag_img {
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-tabs {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
.b-tabs {
|
||||||
|
font-weight: $bold;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 700;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $l;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: Noto Sans, sans-serif;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,15 +1,192 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="container mt-5">
|
||||||
pagina cooperativas c/index
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-10 coopcard-list">
|
||||||
|
<h1 class="title">Últimas cooperativas añadidas</h1>
|
||||||
|
<p class="help">
|
||||||
|
Si quieres que tu cooperativa forme parte de este gran proyecto
|
||||||
|
registrate en el siguiente
|
||||||
|
<NuxtLink to="/registro/cooperativa"><b>formulario</b></NuxtLink
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<div class="form-container">
|
||||||
|
<form class="search-container" @submit.prevent="search">
|
||||||
|
<input
|
||||||
|
v-model="searchText"
|
||||||
|
class="search-text"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="Buscar cooperativas"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
class="search-icon"
|
||||||
|
src="@/assets/img/latienda-search-blue.svg"
|
||||||
|
alt="latienda.coop-search"
|
||||||
|
@click="search"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-for="coop in companyList" :key="coop.id">
|
||||||
|
<CoopCard :coop="coop" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <BPagination
|
||||||
|
v-model="currentPage"
|
||||||
|
class="pagination"
|
||||||
|
:total-rows="rows"
|
||||||
|
:per-page="perPage"
|
||||||
|
/> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
setup(){
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'mainbanner',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentPage: 1,
|
||||||
|
perPage: 10,
|
||||||
|
searchText: '',
|
||||||
|
companyList: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// computed: {
|
||||||
|
// rows() {
|
||||||
|
// return this.companyList?.length
|
||||||
|
// },
|
||||||
|
// slicedCompanies() {
|
||||||
|
// const initial = (this.currentPage - 1) * this.perPage
|
||||||
|
// const final = this.currentPage * this.perPage
|
||||||
|
// const items = this.companyList ? this.companyList.slice(initial, final) : []
|
||||||
|
// return items
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
mounted() {
|
||||||
|
this.getCompanies()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getCompanies() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`/companies/sample/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
this.companyList = response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching companies:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async search() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
if (this.searchText) {
|
||||||
|
const response = await $fetch(`/search/companies/?search=${this.searchText}`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
this.companyList = response
|
||||||
|
} else {
|
||||||
|
const response = await $fetch(`/companies/sample/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
this.companyList = response
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 80px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-top: 60px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: $xxl;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coopcard-list {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
color: $color-navy;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
font-size: $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: $color-light-green;
|
||||||
|
width: 50%;
|
||||||
|
height: 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container img,
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
padding: 6px 10px;
|
||||||
|
margin-right: 16px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: $xl;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
float: right;
|
||||||
|
height: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-text {
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $s;
|
||||||
|
font-weight: $regular;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
--webkit-appearance: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
59
pages/editar/cooperativa/index.vue
Normal file
59
pages/editar/cooperativa/index.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<BContainer class="container">
|
||||||
|
<BRow align-h="center">
|
||||||
|
<BCol class="edit-coop">
|
||||||
|
<h1 class="title">Editar Cooperativa</h1>
|
||||||
|
<div class="form-container">
|
||||||
|
<CompanyForm />
|
||||||
|
</div>
|
||||||
|
</BCol>
|
||||||
|
</BRow>
|
||||||
|
</BContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
setup (){
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'editar',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'COOP_MANAGER' }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
.edit-coop {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $xxl;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
text-align: left;
|
||||||
|
@include desktop {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
@include tablet {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
@include mobile {
|
||||||
|
width: 90%;
|
||||||
|
margin-top: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,18 +9,21 @@
|
|||||||
type="text"
|
type="text"
|
||||||
label-text="Contraseña actual"
|
label-text="Contraseña actual"
|
||||||
required
|
required
|
||||||
|
@input="form.old_password = $event"
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
type="password"
|
type="password"
|
||||||
label-text="Nueva contraseña"
|
label-text="Nueva contraseña"
|
||||||
required
|
required
|
||||||
|
@input="form.password = $event"
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-model="form.password2"
|
v-model="form.password2"
|
||||||
type="password"
|
type="password"
|
||||||
label-text="Nueva contraseña"
|
label-text="Nueva contraseña"
|
||||||
required
|
required
|
||||||
|
@input="form.password2 = $event"
|
||||||
/>
|
/>
|
||||||
<small v-if="error" class="error">{{ error }}</small>
|
<small v-if="error" class="error">{{ error }}</small>
|
||||||
<small v-if="success" class="success">{{ success }}</small>
|
<small v-if="success" class="success">{{ success }}</small>
|
||||||
@@ -34,6 +37,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState } from 'pinia'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -53,18 +58,29 @@ export default {
|
|||||||
success: null,
|
success: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAuthStore, ['id', 'access']),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async submitUser() {
|
async submitUser() {
|
||||||
this.error = null
|
this.error = null
|
||||||
this.success = null
|
this.success = null
|
||||||
if (this.form.password === this.form.password2) {
|
if (this.form.password === this.form.password2) {
|
||||||
try {
|
try {
|
||||||
await this.$api.put(
|
const config = useRuntimeConfig()
|
||||||
`/user/change_password/${this.$store.state.auth.id}/`,
|
const userId = this.id
|
||||||
this.form
|
const access = this.access
|
||||||
)
|
await $fetch(`/user/change_password/${userId}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'PUT',
|
||||||
|
body: this.form,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${access}`
|
||||||
|
}
|
||||||
|
})
|
||||||
this.success = 'Contraseña cambiada correctamente'
|
this.success = 'Contraseña cambiada correctamente'
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error('Error cambiando la contraseña:', error)
|
||||||
this.error = 'La contraseña actual no es correcta'
|
this.error = 'La contraseña actual no es correcta'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,12 +6,14 @@
|
|||||||
<h1 class="title">Editar perfil</h1>
|
<h1 class="title">Editar perfil</h1>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-model="form.full_name"
|
v-model="form.full_name"
|
||||||
|
:value="form.full_name ? form.full_name : ''"
|
||||||
type="text"
|
type="text"
|
||||||
label-text="Nombre de usuario"
|
label-text="Nombre de usuario"
|
||||||
@input="form.full_name = $event"
|
@input="form.full_name = $event"
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
|
:value="form.email ? form.email : ''"
|
||||||
type="text"
|
type="text"
|
||||||
label-text="Email"
|
label-text="Email"
|
||||||
@input="form.email = $event"
|
@input="form.email = $event"
|
||||||
@@ -55,31 +57,32 @@ export default {
|
|||||||
success: false,
|
success: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetch() {
|
computed: {
|
||||||
|
...mapState(useAuthStore, ['id', 'access']),
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
try {
|
try {
|
||||||
const response = await $fetch(`/my_user/`, {
|
const response = await $fetch(`/my_user/`, {
|
||||||
baseURL: config.public.baseURL,
|
baseURL: config.public.baseURL,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
body: JSON.stringify(this.form),
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.access}`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
console.log('User data fetched successfully:', response)
|
//console.log('User data fetched successfully:', response)
|
||||||
|
this.form.full_name = response.full_name
|
||||||
|
this.form.email = response.email
|
||||||
|
this.form.notify = response.notify
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching user data:', err)
|
console.error('Error fetching user data:', err)
|
||||||
this.error = true
|
this.error = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.form.full_name = response.full_name
|
|
||||||
this.form.email = response.email
|
|
||||||
this.form.notify = response.notify
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(useAuthStore, ['id', 'access']),
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions( useAuthStore, ['setUser']),
|
...mapActions( useAuthStore, ['setUser']),
|
||||||
async submitUser() {
|
async submitUser() {
|
||||||
// TODO: configurar primero el estar logeado (estados, getters) para que peuda coger esa info y actualizarla
|
|
||||||
this.error = false
|
this.error = false
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
try {
|
try {
|
||||||
|
|||||||
169
pages/editar/productos/[id].vue
Normal file
169
pages/editar/productos/[id].vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<BContainer class="container">
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="activeModal"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
:ok-variant="modalColor"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
<BRow align-h="center">
|
||||||
|
<BCol class="edit-product">
|
||||||
|
<h1 class="title">Editar producto</h1>
|
||||||
|
<ProductForm :product-form="form" @send="submitProduct" />
|
||||||
|
<div v-if="form.source !== 'MANUAL'" class="data">
|
||||||
|
<dl class="creation-data">
|
||||||
|
<dt>Fecha de creación:</dt>
|
||||||
|
<dd>{{ formatDatetime(form.created) }}</dd>
|
||||||
|
|
||||||
|
<dt>Fecha de actualización:</dt>
|
||||||
|
<dd>{{ formatDatetime(form.updated) }}</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="import-data">
|
||||||
|
<dt>Fecha de importación:</dt>
|
||||||
|
<dd>{{ formatDatetime(form.sync_date) }}</dd>
|
||||||
|
|
||||||
|
<dt>Importado desde:</dt>
|
||||||
|
<dd>{{ form.source }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</BCol>
|
||||||
|
</BRow>
|
||||||
|
</BContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import dataProcessing from '~/utils/dataProcessing'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'editar',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'COOP_MANAGER' },
|
||||||
|
})
|
||||||
|
const auth = useAuthStore();
|
||||||
|
return {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {},
|
||||||
|
activeModal: false,
|
||||||
|
modalText: '',
|
||||||
|
modalColor: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const route = useRoute()
|
||||||
|
this.form = await $fetch(`my_products/${route.params.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (this.form.source !== 'MANUAL' && this.form.history) {
|
||||||
|
try {
|
||||||
|
//TODO: Review Fetching the sync date from the history endpoint
|
||||||
|
const result = await $fetch(`history/${this.form.history}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
form.sync_date = result.data.sync_date
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
form.sync_date = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
formatDatetime: dataProcessing.formatDatetime,
|
||||||
|
|
||||||
|
async submitProduct(value) {
|
||||||
|
console.log('Submitting product:', value)
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const route = useRoute()
|
||||||
|
//TODO: review PUT method, its sending 200 status but not updating the product
|
||||||
|
try {
|
||||||
|
await $fetch(`/my_products/${route.params.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'PUT',
|
||||||
|
body: value,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.modalText = 'Producto actualizado correctamente'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
} catch (error) {
|
||||||
|
this.modalText = 'Ha habido un error'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
this.activeModal = true
|
||||||
|
console.error('Error updating product:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
.edit-product {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $xxl;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
.help-text {
|
||||||
|
border: 1px solid $color-greylayout;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: justify;
|
||||||
|
hyphens: auto;
|
||||||
|
font-size: $xs;
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-greylayout;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
background-color: $color-light;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data {
|
||||||
|
margin: 0 100px;
|
||||||
|
margin-top: 80px;
|
||||||
|
font-size: $xs;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
.creation-data,
|
||||||
|
.import-data {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
pages/editar/productos/crear.vue
Normal file
118
pages/editar/productos/crear.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<BContainer class="container">
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="activeModal"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
ok-variant="danger"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
<BRow align-h="center">
|
||||||
|
<BCol class="add-product">
|
||||||
|
<h1 class="title">Añadir producto</h1>
|
||||||
|
<ProductForm @send="submitProduct" />
|
||||||
|
</BCol>
|
||||||
|
</BRow>
|
||||||
|
</BContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'editar',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'COOP_MANAGER' },
|
||||||
|
})
|
||||||
|
const auth = useAuthStore();
|
||||||
|
return {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeModal: false,
|
||||||
|
modalText: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async submitProduct(value) {
|
||||||
|
console.log('Creating product:', value)
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
try {
|
||||||
|
await $fetch(`/products/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: value,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.$router.push({
|
||||||
|
name: 'editar-productos',
|
||||||
|
params: { action: 'created' },
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.modalText = 'Ha habido un error'
|
||||||
|
this.activeModal = true
|
||||||
|
console.error('Error updating product:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
.add-product {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $xxl;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
text-align: left;
|
||||||
|
@include desktop {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
@include tablet {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
@include mobile {
|
||||||
|
width: 90%;
|
||||||
|
margin-top: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.help-text {
|
||||||
|
border: 1px solid $color-greylayout;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: justify;
|
||||||
|
hyphens: auto;
|
||||||
|
font-size: $xs;
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-greylayout;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
background-color: $color-light;
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data {
|
||||||
|
margin: 0 100px;
|
||||||
|
margin-top: 80px;
|
||||||
|
font-size: $xs;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
408
pages/editar/productos/importar.vue
Normal file
408
pages/editar/productos/importar.vue
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="activeModal"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
:ok-variant="modalColor"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
<BContainer class="container">
|
||||||
|
<BRow align-h="start">
|
||||||
|
<div class="import-products">
|
||||||
|
<div
|
||||||
|
class="header"
|
||||||
|
@click="importOpen = !importOpen"
|
||||||
|
>
|
||||||
|
<h4 class="title">
|
||||||
|
Importar productos desde CSV
|
||||||
|
<img
|
||||||
|
src="@/assets/img/latienda-arrow-down.svg"
|
||||||
|
alt=""
|
||||||
|
class="arrow"
|
||||||
|
:class="{
|
||||||
|
'close': !importOpen,
|
||||||
|
'open': importOpen }"
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<BCollapse id="collapse-import" v-model="importOpen">
|
||||||
|
<div class="description">
|
||||||
|
<p>
|
||||||
|
La función <strong>Importar CSV</strong> te permite subir tus
|
||||||
|
productos a Latienda.coop sin tener que hacerlo manualmente.
|
||||||
|
Puedes ponerles nombres a las columnas en tu hoja de cálculo con
|
||||||
|
las siguientes etiquetas para que los campos se asocien
|
||||||
|
automáticamente en tus productos:
|
||||||
|
</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<strong>SKU: </strong>usa esta columna para proporcionar un
|
||||||
|
identificador único a tu producto.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Nombre de producto: </strong>asigna un nombre. Es el
|
||||||
|
texto que se ve primero al listar el producto.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Descripción: </strong>proporciona los detalles del
|
||||||
|
producto.
|
||||||
|
</li>
|
||||||
|
<li><strong>Imagen: </strong>url de la imagen del producto.</li>
|
||||||
|
<li>
|
||||||
|
<strong>URL: </strong>enlace a la página del producto si
|
||||||
|
existe.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Precio: </strong>precio en euros con IVA incluido. Si
|
||||||
|
se deja en blanco aparecerá el texto "Consultar precio".
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Gastos de envío: </strong>importe adicional por los
|
||||||
|
gastos de envío. Si se deja en blanco, o nulo, aparecerá el
|
||||||
|
texto "Sin gastos de envío".
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Condiciones de envío: </strong>aquí se puede indicar
|
||||||
|
las condiciones de envío específicas para cada producto. Si se
|
||||||
|
deja en blanco se mostrarán las opciones por defecto de tu
|
||||||
|
cooperativa.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Descuento: </strong>porcentaje de descuento sobre el
|
||||||
|
precio del producto.
|
||||||
|
</li>
|
||||||
|
<li><strong>Stock: </strong>número de unidades disponibles.</li>
|
||||||
|
<li>
|
||||||
|
<strong>Tags: </strong>etiquetas libres que describen al
|
||||||
|
producto. Usa el carácter "/" para separar cada tag.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Categoría: </strong>clasificación principal del
|
||||||
|
producto. el Latienda.coop usamos la taxonomía de Google.
|
||||||
|
Puedes ver todas las categorías disponibles
|
||||||
|
<a
|
||||||
|
href="https://www.google.com/basepages/producttype/taxonomy-with-ids.es-ES.txt"
|
||||||
|
target="_blank"
|
||||||
|
>aquí</a
|
||||||
|
>. Elige la categoría o subcategoría de cualquier nivel que
|
||||||
|
mejor defina a tu producto.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Identificadores: </strong>identificador único
|
||||||
|
opcional.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
Descarga la
|
||||||
|
<a href="/plantilla-latienda.csv" download="plantilla.csv" type="text/csv">plantilla</a>
|
||||||
|
de referencia.
|
||||||
|
</p>
|
||||||
|
<form @submit.prevent="submitFile">
|
||||||
|
<div>
|
||||||
|
<label for="file"
|
||||||
|
>Archivo .csv
|
||||||
|
<input
|
||||||
|
id="file"
|
||||||
|
type="file"
|
||||||
|
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||||
|
placeholder="Elige un archivo"
|
||||||
|
required
|
||||||
|
@change="handleFile"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<SubmitButton text="Importar" image-url="" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</BCollapse>
|
||||||
|
</div>
|
||||||
|
</BRow>
|
||||||
|
</BContainer>
|
||||||
|
|
||||||
|
<BContainer class="container">
|
||||||
|
<BRow align-h="start">
|
||||||
|
<div class="import-products">
|
||||||
|
<div
|
||||||
|
class="header"
|
||||||
|
@click="syncOpen = !syncOpen"
|
||||||
|
>
|
||||||
|
<h4 class="title">
|
||||||
|
Sincronización con tu tienda online
|
||||||
|
<img
|
||||||
|
src="@/assets/img/latienda-arrow-down.svg"
|
||||||
|
alt=""
|
||||||
|
class="arrow"
|
||||||
|
:class="{ 'close': !syncOpen, 'open': syncOpen }"
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<BCollapse id="collapse-sync" v-model="syncOpen">
|
||||||
|
<div v-if="platform" class="description">
|
||||||
|
<div class="platform">
|
||||||
|
<BFormInput v-model="platform" type="text" disabled />
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="credentials">
|
||||||
|
<BFormTextarea
|
||||||
|
v-model="credentials"
|
||||||
|
type="text"
|
||||||
|
rows="4"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
Si los datos no son correctos puedes editarlos en tu
|
||||||
|
<NuxtLink to="/editar/cooperativa">cooperativa</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="description">
|
||||||
|
<p>
|
||||||
|
No tienes configurada tu tienda online. <br /><br />
|
||||||
|
<NuxtLink to="/editar/cooperativa">Edita</NuxtLink> la
|
||||||
|
información de tu cooperativa y añade la información necesaría
|
||||||
|
para la sincronización con Woocommerce
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="syncWebsite">
|
||||||
|
<SubmitButton
|
||||||
|
text="Lanzar sincronización"
|
||||||
|
image-url=""
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</BCollapse>
|
||||||
|
</div>
|
||||||
|
</BRow>
|
||||||
|
</BContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'editar',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'COOP_MANAGER' },
|
||||||
|
})
|
||||||
|
const auth = useAuthStore();
|
||||||
|
return {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
file: null,
|
||||||
|
importOpen: false,
|
||||||
|
syncOpen: false,
|
||||||
|
platform: null,
|
||||||
|
credentials: null,
|
||||||
|
syncForm: null,
|
||||||
|
activeModal: false,
|
||||||
|
modalText: '',
|
||||||
|
modalColor: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try{
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const data = await $fetch('my_company/', {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (data.company.platform === 'WOO_COMMERCE')
|
||||||
|
this.platform = 'Woocommerce'
|
||||||
|
this.credentials = JSON.stringify(data.company.credentials, undefined, 2)
|
||||||
|
this.syncForm = {
|
||||||
|
key: data.company.credentials['key'],
|
||||||
|
secret: data.company.credentials['secret'],
|
||||||
|
url: data.company.shop_link
|
||||||
|
}
|
||||||
|
// const historyResults = await $fetch(`/history/`, {
|
||||||
|
// baseURL: config.public.baseURL,
|
||||||
|
// method: 'GET',
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Bearer ${this.auth.access}`
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// console.log(historyResults.data)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async handleFile(e) {
|
||||||
|
const selectedFile = await e.target.files[0]
|
||||||
|
this.file = selectedFile
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitFile() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
//TODO: Review functionality
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('csv_file', this.file)
|
||||||
|
console.log('Form data to submit:', formData)
|
||||||
|
await $fetch('/load_products/',{
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: this.file,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.modalText = 'Productos importados correctamente'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
} catch {
|
||||||
|
this.modalText = 'Ha habido un error'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
this.activeModal = true
|
||||||
|
}
|
||||||
|
// Clear the file input
|
||||||
|
this.file = null
|
||||||
|
},
|
||||||
|
|
||||||
|
async syncWebsite() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
//TODO: Error 500, its seems to be backend issue
|
||||||
|
try {
|
||||||
|
console.log('Sync form data:', this.syncForm)
|
||||||
|
await $fetch('/sync_shop/',{
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
url: this.syncForm.url,
|
||||||
|
key: this.syncForm.key,
|
||||||
|
secret: this.syncForm.secret
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.modalText = 'Sincronización iniciada'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
} catch {
|
||||||
|
this.modalText = 'Ha habido un error'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
this.activeModal = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.arrow {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
.open {
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-products {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: $color-navy;
|
||||||
|
@include mobile {
|
||||||
|
margin-top: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $l;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style-type: none;
|
||||||
|
counter-reset: a;
|
||||||
|
}
|
||||||
|
ol > li {
|
||||||
|
counter-increment: a;
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 18px;
|
||||||
|
margin-left: 35px;
|
||||||
|
}
|
||||||
|
ol > li:before {
|
||||||
|
color: #fff;
|
||||||
|
background: $color-navy;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
top: 2px;
|
||||||
|
left: -35px;
|
||||||
|
position: absolute;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
content: counter(a);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 400;
|
||||||
|
-webkit-text-stroke: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $color-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
text-align: left;
|
||||||
|
color: $color-navy;
|
||||||
|
font-weight: $bold;
|
||||||
|
font-size: $xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cont-col {
|
||||||
|
margin: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
margin: 15px 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
font-size: $s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
277
pages/editar/productos/index.vue
Normal file
277
pages/editar/productos/index.vue
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<template>
|
||||||
|
<BContainer class="container">
|
||||||
|
<BRow>
|
||||||
|
<BCol>
|
||||||
|
<h1 class="title">
|
||||||
|
Productos
|
||||||
|
<button
|
||||||
|
class="ml-4 mb-1 btn btn-outline-primary btn-sm"
|
||||||
|
@click="redirectToNewProduct"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</h1>
|
||||||
|
</BCol>
|
||||||
|
</BRow>
|
||||||
|
<BRow>
|
||||||
|
<BCol class="d-flex flex-row-reverse">
|
||||||
|
<button class="btn btn-secondary" :disabled="selectedItemsIndexes.length === 0" @click="desactivateProducts">
|
||||||
|
Desactivar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary mr-3" :disabled="selectedItemsIndexes.length === 0" @click="activateProducts">
|
||||||
|
Activar
|
||||||
|
</button>
|
||||||
|
<div v-show="selectedItemsIndexes.length !== 0" class="selected-products mr-3">
|
||||||
|
{{ selectedItemsIndexes.length }} productos seleccionados
|
||||||
|
</div>
|
||||||
|
</BCol>
|
||||||
|
</BRow>
|
||||||
|
<BRow>
|
||||||
|
<template v-if="products">
|
||||||
|
<v-data-table
|
||||||
|
v-model="selectedItemsIndexes"
|
||||||
|
show-select
|
||||||
|
:single-select="singleSelect"
|
||||||
|
:headers="headers"
|
||||||
|
:items="products"
|
||||||
|
:search="search"
|
||||||
|
:loading="loading"
|
||||||
|
loading-text="Cargando productos..."
|
||||||
|
>
|
||||||
|
<template #top>
|
||||||
|
<v-toolbar flat color="white">
|
||||||
|
<!-- Search -->
|
||||||
|
<v-text-field
|
||||||
|
v-model="search"
|
||||||
|
append-icon="mdi-magnify"
|
||||||
|
label="Buscar producto"
|
||||||
|
single-line
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<!-- . Search -->
|
||||||
|
</v-toolbar>
|
||||||
|
</template>
|
||||||
|
<template #[`item.active`]="item">
|
||||||
|
<v-icon v-if="item.item.active" small class="mr-2">
|
||||||
|
mdi-check
|
||||||
|
</v-icon>
|
||||||
|
<v-icon v-else small class="mr-2"> mdi-close </v-icon>
|
||||||
|
</template>
|
||||||
|
<template #[`item.actions`]="item">
|
||||||
|
<NuxtLink :to="`/editar/productos/${item.item.id}`">
|
||||||
|
<v-icon small class="mr-2"> mdi-pencil </v-icon>
|
||||||
|
</NuxtLink>
|
||||||
|
<v-icon small class="mr-2" @click="deleteItem(item)">
|
||||||
|
mdi-delete
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</template>
|
||||||
|
</BRow>
|
||||||
|
<BModal
|
||||||
|
id="modal-center"
|
||||||
|
v-model="activeModal"
|
||||||
|
centered
|
||||||
|
title="latienda.coop"
|
||||||
|
:ok-variant="modalColor"> {{ modalText }}
|
||||||
|
</BModal>
|
||||||
|
</BContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'editar',
|
||||||
|
middleware: 'auth',
|
||||||
|
auth: { authority: 'COOP_MANAGER' },
|
||||||
|
})
|
||||||
|
const auth = useAuthStore();
|
||||||
|
return {
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
products: [],
|
||||||
|
totalProducts: 0,
|
||||||
|
loading: true,
|
||||||
|
singleSelect: false,
|
||||||
|
selectedItemsIndexes: [],
|
||||||
|
search: '',
|
||||||
|
options: {},
|
||||||
|
headers: [
|
||||||
|
{ title: 'ID', value: 'id' },
|
||||||
|
{ title: 'Nombre', value: 'name' },
|
||||||
|
{ title: 'Categoría', value: 'category' },
|
||||||
|
{ title: 'Activo', value: 'active' },
|
||||||
|
{ title: 'Precio', value: 'price' },
|
||||||
|
{ title: 'Fuente', value: 'source' },
|
||||||
|
{ title: 'Stock', value: 'stock' },
|
||||||
|
{ title: 'Acción', value: 'actions' },
|
||||||
|
],
|
||||||
|
activeModal: false,
|
||||||
|
modalText: '',
|
||||||
|
modalColor: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedItems() {
|
||||||
|
const itemsArr = []
|
||||||
|
this.selectedItemsIndexes.forEach(index => {
|
||||||
|
this.products.forEach(item => {
|
||||||
|
if (item.id === index) {
|
||||||
|
itemsArr.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return itemsArr
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
if (this.$route.params.action === 'created') {
|
||||||
|
this.modalText = 'Producto creado correctamente'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
}
|
||||||
|
await this.getDataFromApi()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async getDataFromApi() {
|
||||||
|
this.loading = true
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const data = await $fetch(`/my_products/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.products = data
|
||||||
|
this.loading = false
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteItem(item) {
|
||||||
|
if (confirm('Confirma que quieres eliminar este producto')) {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
try {
|
||||||
|
await $fetch(`/my_products/${item.item.id}`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const index = this.products.indexOf(item.item)
|
||||||
|
this.products.splice(index, 1)
|
||||||
|
this.selectedItemsIndexes = []
|
||||||
|
this.modalText = 'Los productos han sido eliminados correctamente.'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.modalText = 'Ha habido un error'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
this.activeModal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async activateProducts() {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
await this.selectedItems.forEach(async (item) => {
|
||||||
|
await $fetch(`/my_products/${item.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
active: true
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const index = this.products.indexOf(item)
|
||||||
|
this.products[index].active = true
|
||||||
|
})
|
||||||
|
this.selectedItemsIndexes = []
|
||||||
|
this.modalText = 'Los productos han sido activados correctamente.'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.modalText = 'Ha habido un error'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
this.activeModal = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async desactivateProducts() {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
await this.selectedItems.forEach(async (item) => {
|
||||||
|
await $fetch(`/my_products/${item.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.auth.access}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const index = this.products.indexOf(item)
|
||||||
|
this.products[index].active = false
|
||||||
|
})
|
||||||
|
this.selectedItemsIndexes = []
|
||||||
|
this.modalText = 'Los productos han sido desactivados correctamente.'
|
||||||
|
this.modalColor = 'success'
|
||||||
|
this.activeModal = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.modalText = 'Ha habido un error'
|
||||||
|
this.modalColor = 'danger'
|
||||||
|
this.activeModal = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
redirectToNewProduct() {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'editar-productos-crear',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $xxl;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
@include mobile {
|
||||||
|
margin-top: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v-toolbar {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.selected-products {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -79,8 +79,9 @@ export default {
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
const response = await $fetch('/initial', {
|
const response = await $fetch('/initial', {
|
||||||
baseURL: this.$config.public.baseURL,
|
baseURL: config.public.baseURL,
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
})
|
})
|
||||||
this.cards = response.cards
|
this.cards = response.cards
|
||||||
|
|||||||
@@ -1,15 +1,92 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="container">
|
||||||
Producto 1
|
<h1 class="title">Detalles del producto</h1>
|
||||||
|
<ProductCardDetails
|
||||||
|
:product="product"
|
||||||
|
:related="related"
|
||||||
|
:related-products="relatedProducts"
|
||||||
|
:company="company"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
export default {
|
export default {
|
||||||
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'mainbanner'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
product: null,
|
||||||
|
company: null,
|
||||||
|
relatedProducts: null,
|
||||||
|
related: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const $route = useRoute()
|
||||||
|
this.product = await $fetch(`/products/${$route.params.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.product.company) {
|
||||||
|
this.company = await $fetch(`/companies/${this.product.company.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.relatedProducts = await $fetch(`/products/${this.product.id}/related/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
this.related = this.relatedProducts.length > 0
|
||||||
|
|
||||||
|
//return { product, company, relatedProducts, related }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching product details:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//TODO: meta data
|
||||||
|
// mounted() {
|
||||||
|
// useHead({
|
||||||
|
// title: `latienda.coop | ${this.product.id}`,
|
||||||
|
// meta: [
|
||||||
|
// {
|
||||||
|
// hid: 'description',
|
||||||
|
// name: 'description',
|
||||||
|
// content: this.product.description,
|
||||||
|
// },
|
||||||
|
// { property: 'og:title', content: this.product.id },
|
||||||
|
// { property: 'og:description', content: this.product.description },
|
||||||
|
// { property: 'og:image', content: this.product.image },
|
||||||
|
// { property: 'og:url', content: this.product.url },
|
||||||
|
// { name: 'twitter:card', content: 'summary_large_image' },
|
||||||
|
// ],
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
@include mobile {
|
||||||
|
margin-top: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: $xl;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
layout: 'admin',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Esto debe estar fuera del export default
|
|
||||||
definePageMeta({
|
|
||||||
middleware: 'auth',
|
|
||||||
auth: {
|
|
||||||
authority: 'SITE_ADMIN',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1>Solo para admins</h1>
|
|
||||||
</template>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// plugins/api.ts
|
|
||||||
import { useAuthStore } from '~/stores/auth'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
const auth = useAuthStore()
|
|
||||||
|
|
||||||
// Función personalizada para hacer requests
|
|
||||||
const apiFetch = async (url: string, options: any = {}) => {
|
|
||||||
try {
|
|
||||||
const res = await $fetch(url, {
|
|
||||||
baseURL: useRuntimeConfig().public.apiBase,
|
|
||||||
credentials: 'include', // para enviar cookies si es necesario
|
|
||||||
headers: {
|
|
||||||
...(options.headers || {}),
|
|
||||||
...(auth.access ? { Authorization: `Bearer ${auth.access}` } : {}),
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
} catch (error: any) {
|
|
||||||
// Si no es 401, relanzamos el error
|
|
||||||
if (error?.status !== 401) throw error
|
|
||||||
|
|
||||||
// Si es el endpoint de refresh, cerramos sesión
|
|
||||||
if (url.includes('refresh')) {
|
|
||||||
auth.logout()
|
|
||||||
return navigateTo('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usuario inactivo
|
|
||||||
if (error?.data?.code === 'user_inactive') {
|
|
||||||
auth.logout()
|
|
||||||
return navigateTo('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intentar refresh
|
|
||||||
try {
|
|
||||||
await auth.refresh()
|
|
||||||
return await apiFetch(url, options) // reintentar la petición original
|
|
||||||
} catch {
|
|
||||||
return navigateTo({ name: 'index', query: { redirected: 'true' } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inyectar como $api
|
|
||||||
nuxtApp.provide('api', apiFetch)
|
|
||||||
})
|
|
||||||
19
plugins/google-analytics.client.ts
Normal file
19
plugins/google-analytics.client.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// TODO: Review if its OK. https://matteo-gabriele.gitbook.io/vue-gtag/migration-v2-to-v3
|
||||||
|
import { configure } from 'vue-gtag'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
if (config.public.googleAnalyticsId) {
|
||||||
|
configure({
|
||||||
|
tagId: config.public.googleAnalyticsId,
|
||||||
|
appName: 'latienda.coop',
|
||||||
|
pageTracker: {
|
||||||
|
router,
|
||||||
|
useScreenview: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
11
public/README.md
Normal file
11
public/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# STATIC
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your static files.
|
||||||
|
Each file inside this directory is mapped to `/`.
|
||||||
|
Thus you'd want to delete this README.md before deploying to production.
|
||||||
|
|
||||||
|
Example: `/static/robots.txt` is mapped as `/robots.txt`.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static).
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 15 KiB |
1
public/plantilla-latienda.csv
Normal file
1
public/plantilla-latienda.csv
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sku,nombre-producto,descripcion,imagen,url,precio,gastos-envio,cond-envio,descuento,stock,tags,categoria,identificadores
|
||||||
|
@@ -1,16 +1,36 @@
|
|||||||
// stores/auth.ts
|
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
access: null as string | null,
|
access: null as string | null,
|
||||||
refresh: null as string | null,
|
refreshTokens: null as string | null,
|
||||||
id: null as number | null,
|
id: null as number | null,
|
||||||
name: null as string | null,
|
name: null as string | null,
|
||||||
email: null as string | null,
|
email: null as string | null,
|
||||||
role: 'ANON' as string,
|
role: 'ANON' as string,
|
||||||
cookiesAreAccepted: false,
|
cookiesAreAccepted: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
persist: true, // TODO: Enable persistence. Cookies will be stored 'auth' 👉🏻 https://prazdevs.github.io/pinia-plugin-persistedstate/frameworks/nuxt
|
||||||
|
// persist: {
|
||||||
|
// key: 'authentication-cookie',
|
||||||
|
// storage: piniaPluginPersistedstate.cookies({
|
||||||
|
// expires: 14,
|
||||||
|
// sameSite: 'strict',
|
||||||
|
// secure: !import.meta.dev,
|
||||||
|
// }),
|
||||||
|
// paths: [
|
||||||
|
// 'id',
|
||||||
|
// 'name',
|
||||||
|
// 'email',
|
||||||
|
// 'role',
|
||||||
|
// 'access',
|
||||||
|
// 'refreshTokens',
|
||||||
|
// 'cookiesAreAccepted',
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
isAuthenticated: (state) => !!state.access,
|
isAuthenticated: (state) => !!state.access,
|
||||||
isUser: (state) => state.role === 'SHOP_USER',
|
isUser: (state) => state.role === 'SHOP_USER',
|
||||||
@@ -29,6 +49,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { email, password }
|
body: { email, password }
|
||||||
})
|
})
|
||||||
|
//console.log('Login payload:', payload)
|
||||||
this.setPayload(payload)
|
this.setPayload(payload)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -41,25 +62,29 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
Authorization: `Bearer ${this.access}`
|
Authorization: `Bearer ${this.access}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
try {
|
||||||
this.setUserData(data)
|
this.setUserData(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting user data:', error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async refresh() {
|
async refreshAccessToken() {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
if (!this.refresh) return
|
if (!this.refreshTokens) return
|
||||||
const data = await $fetch('/token/refresh/', {
|
const data = await $fetch('/token/refresh/', {
|
||||||
baseURL: config.public.baseURL,
|
baseURL: config.public.baseURL,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { refresh: this.refresh }
|
body: { refresh: this.refreshTokens }
|
||||||
})
|
})
|
||||||
this.setPayload(data)
|
this.setPayload(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutations migration
|
async logout() {
|
||||||
logout() {
|
this.$reset()
|
||||||
this.$reset() // Reset the store state
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Mutations migration
|
||||||
acceptCookies() {
|
acceptCookies() {
|
||||||
this.cookiesAreAccepted = true
|
this.cookiesAreAccepted = true
|
||||||
},
|
},
|
||||||
@@ -74,7 +99,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
setPayload(payload: any) {
|
setPayload(payload: any) {
|
||||||
this.access = payload.access
|
this.access = payload.access
|
||||||
if (payload.refresh) {
|
if (payload.refresh) {
|
||||||
this.refresh = payload.refresh
|
this.refreshTokens = payload.refresh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user