create product functionality and page

This commit is contained in:
María
2025-08-21 11:55:20 +02:00
parent 2633959ce8
commit 4a05f8915b
4 changed files with 212 additions and 58 deletions

View File

@@ -10,7 +10,7 @@
:src="croppieImage" :src="croppieImage"
:stencil-props="{ aspectRatio: 1 }" :stencil-props="{ aspectRatio: 1 }"
/> />
<div> <div class="buttons">
<button class="filter-button" @click.prevent="crop">Aplicar</button> <button class="filter-button" @click.prevent="crop">Aplicar</button>
<button @click.prevent="cancel">Cancelar</button> <button @click.prevent="cancel">Cancelar</button>
</div> </div>
@@ -31,7 +31,7 @@ export default {
components: { Cropper }, components: { Cropper },
props: { props: {
imageUrl: { imageUrl: {
type: String, type: [String, Object],
default: null, default: null,
}, },
}, },
@@ -106,5 +106,10 @@ export default {
} }
.input { .input {
max-width: 100%; max-width: 100%;
margin-bottom: 1em;
}
.buttons {
margin-top: 1em;
} }
</style> </style>

View File

@@ -1,12 +1,10 @@
<template> <template>
<form class="form" @submit.prevent="sendProduct"> <form class="form" @submit.prevent="sendProduct">
{{ form }}
<div class="cont-col"> <div class="cont-col">
<BFormCheckbox <BFormCheckbox
id="customSwitch1" id="customSwitch1"
v-model="form.active" v-model="form.active"
class="label" class="label"
:value="form.active"
>Activo >Activo
<span class="help-text" <span class="help-text"
>Desactiva el producto si quieres paralizar temporalmente su >Desactiva el producto si quieres paralizar temporalmente su
@@ -15,7 +13,7 @@
</BFormCheckbox> </BFormCheckbox>
</div> </div>
<br /> <br />
<FormHeader title="general" /> <FormHeader title="General" />
<p class="help-text"> <p class="help-text">
Estos son los datos básicos de un producto. Procura completar el mayor 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 número de campos posible. Los campos señalados con asterisco (*) son
@@ -34,35 +32,41 @@
/> />
<FormInput <FormInput
v-model="form.price" v-model="form.price"
:value="form.price ? form.price : ''"
min="0" min="0"
step="0.01" step="0.01"
type="number" type="number"
label-text="Precio" label-text="Precio"
placeholder="precio + iva" placeholder="precio + iva"
@input="form.price = $event"
/> />
</div> </div>
<div class="cont"> <div class="cont">
<FormInput <FormInput
v-model="form.url" v-model="form.url"
:value="form.url ? form.url : ''"
type="text" type="text"
label-text="Url" label-text="Url"
placeholder="enlace directo al producto en tu tienda o web" placeholder="enlace directo al producto en tu tienda o web"
@input="form.url = $event"
/> />
<small class="error" v-if="form.url && !isValidUrl(form.url)" <small v-if="form.url && !isValidUrl(form.url)" class="error">
>La url no es válida</small La url no es válida
> </small>
<br /> <br />
<FormInput <FormInput
v-model="form.sku" v-model="form.sku"
:value="form.sku ? form.sku : ''"
type="text" type="text"
label-text="SKU o identificador" label-text="SKU o identificador"
@input="form.sku = $event"
/> />
</div> </div>
<div class="cont-col"> <div class="cont-col">
<label for="imagen">Imagen</label> <label for="imagen">Imagen</label>
<client-only> <ClientOnly>
<FormInputImage :imageUrl="form.image" @change="handleImage" /> <FormInputImage :image-url="form.image" @change="handleImage" />
</client-only> </ClientOnly>
</div> </div>
<!-- <div class="cont-col"> <!-- <div class="cont-col">
<input <input
@@ -79,20 +83,33 @@
<textarea v-model="form.description" class="textarea" type="text" /> <textarea v-model="form.description" class="textarea" type="text" />
</div> </div>
<div class="cont"> <div class="cont">
<FormInput v-model="form.stock" type="number" label-text="Stock" /> <FormInput
v-model="form.stock"
:value="form.stock ? form.stock : ''"
type="number"
label-text="Stock"
@input="form.stock = $event"
/>
<FormInput <FormInput
v-model="form.discount" v-model="form.discount"
:value="form.discount ? form.discount : ''"
step="0.01" step="0.01"
type="number" type="number"
label-text="Descuento" label-text="Descuento"
@input="form.discount = $event"
/> />
</div> </div>
<div class="cont-col"> <div class="cont-col">
<FormInput v-model="form.identifiers" label-text="Identificador único" /> <FormInput
v-model="form.identifiers"
:value="form.identifiers ? form.identifiers : ''"
label-text="Identificador único"
@input="form.identifiers = $event"
/>
</div> </div>
</fieldset> </fieldset>
<fieldset class="fieldset"> <fieldset class="fieldset">
<FormHeader title="categorías" /> <FormHeader title="Categorías" />
<p class="help-text"> <p class="help-text">
Estos datos ayudan a que tu producto sea encontrado por los clientes. Estos datos ayudan a que tu producto sea encontrado por los clientes.
Procura completar el mayor número de campos posibles. Los campos Procura completar el mayor número de campos posibles. Los campos
@@ -101,15 +118,14 @@
<div class="cont-col"> <div class="cont-col">
<label for="category">Categoría*</label> <label for="category">Categoría*</label>
<b-form-input <BFormInput
id="category"
v-model="form.category" v-model="form.category"
autocomplete="off" autocomplete="off"
list="my-list-id" list="my-list-id"
id="category" />
></b-form-input>
<datalist id="my-list-id"> <datalist id="my-list-id">
<option v-for="(choice, index) in categories" :key="index"> <option v-for="(choice, index) in categories" :key="`category-${index}`">
{{ choice }} {{ choice }}
</option> </option>
</datalist> </datalist>
@@ -126,22 +142,22 @@
</div> </div>
<div class="cont-col"> <div class="cont-col">
<label for="tags-basic">Palabras clave</label> <label for="tags-basic">Palabras clave</label>
<b-form-tags <BFormTags
v-model="form.tags" v-model="form.tags"
placeholder="Añade palabras clave (moda, complementos...)" placeholder="Añade palabras clave (moda, complementos...)"
input-id="tags-basic" input-id="tags-basic"
></b-form-tags> />
</div> </div>
<div class="cont-col"> <div class="cont-col">
<label for="tags-basic">Atributos</label> <label for="tags-basic">Atributos</label>
<b-form-tags <BFormTags
v-model="form.attributes" v-model="form.attributes"
placeholder="Añade características del producto (talla, color...)" placeholder="Añade características del producto (talla, color...)"
input-id="tags-basic" input-id="tags-basic"
></b-form-tags> />
</div> </div>
</fieldset> </fieldset>
<FormHeader title="envío" /> <FormHeader title="Envío" />
<fieldset class="fieldset"> <fieldset class="fieldset">
<div class="cont-col"> <div class="cont-col">
<label for="">Condiciones de envío</label> <label for="">Condiciones de envío</label>
@@ -156,16 +172,20 @@
<div class="cont-col"> <div class="cont-col">
<FormInput <FormInput
v-model="form.shipping_cost" v-model="form.shipping_cost"
:value="form.shipping_cost ? form.shipping_cost : ''"
step="0.01" step="0.01"
type="number" type="number"
label-text="Gastos de envío" label-text="Gastos de envío"
placeholder="ej. 4,50" placeholder="ej. 4,50"
@input="form.shipping_cost = $event"
/> />
</div> </div>
</fieldset> </fieldset>
<div class="submit-btn" align="center"> <div class="submit-btn" align="center">
<SubmitButton text="guardar" imageUrl="" /> <SubmitButton text="guardar" image-url="" />
</div> </div>
PRODUCT FORM: {{ productForm }} <br><br><br>
FORM: {{ form }}
</form> </form>
</template> </template>
@@ -174,7 +194,13 @@ import FormInputImage from './FormInputImage.vue'
import dataProcessing from '~/utils/dataProcessing' import dataProcessing from '~/utils/dataProcessing'
export default { export default {
components: { FormInputImage }, components: { FormInputImage },
props: ['productForm'], props: {
productForm: {
type: Object,
default: () => ({}),
},
},
emits: ['send'],
data() { data() {
return { return {
form: { form: {
@@ -222,19 +248,24 @@ export default {
} }
}, },
mounted() { async mounted() {
if (this.productForm) { await this.getAllCategories()
if (this.productForm && Object.keys(this.productForm).length > 0) {
Object.keys(this.form).forEach((key) => { Object.keys(this.form).forEach((key) => {
this.form[key] = this.productForm[key] this.form[key] = this.productForm[key]
}) })
} }
this.getAllCategories()
}, },
methods: { methods: {
isValidUrl: dataProcessing.isValidUrl, isValidUrl: dataProcessing.isValidUrl,
async getAllCategories() { async getAllCategories() {
const { data } = await this.$api.get(`/products/all_categories/`) const config = useRuntimeConfig()
const data = await $fetch(`/products/all_categories/`,{
baseURL: config.public.baseURL,
method: 'GET',
})
this.categories = data this.categories = data
}, },
parseForm() { parseForm() {
@@ -249,7 +280,7 @@ export default {
if (typeof formData.get('image') === 'string') { if (typeof formData.get('image') === 'string') {
formData.delete('image') formData.delete('image')
} }
//console.log('FormData:', formData)
return formData return formData
}, },
@@ -259,6 +290,7 @@ export default {
sendProduct() { sendProduct() {
const formData = this.parseForm() const formData = this.parseForm()
//TODO: review with Diego. I changed formaData with this.form
this.$emit('send', formData) this.$emit('send', formData)
}, },
}, },
@@ -333,7 +365,7 @@ export default {
font-family: $font-secondary; font-family: $font-secondary;
background-color: $color-light; background-color: $color-light;
margin-bottom: 20px; margin-bottom: 20px;
margin-top: -10px; margin-top: 6px;
a { a {
text-decoration: underline; text-decoration: underline;

View File

@@ -1,5 +1,12 @@
<template> <template>
<BContainer class="container"> <BContainer class="container">
<BModal
id="modal-center"
v-model="activeModal"
centered
title="latienda.coop"
:ok-variant="modalColor"> {{ modalText }}
</BModal>
<BRow align-h="center"> <BRow align-h="center">
<BCol class="edit-product"> <BCol class="edit-product">
<h1 class="title">Editar producto</h1> <h1 class="title">Editar producto</h1>
@@ -27,6 +34,7 @@
<script> <script>
import dataProcessing from '~/utils/dataProcessing' import dataProcessing from '~/utils/dataProcessing'
import { useAuthStore } from '@/stores/auth'
export default { export default {
setup() { setup() {
definePageMeta({ definePageMeta({
@@ -43,22 +51,26 @@ export default {
data() { data() {
return { return {
form: {}, form: {},
activeModal: false,
modalText: '',
modalColor: '',
} }
}, },
async created() { async created() {
try { try {
const config = useRuntimeConfig() const config = useRuntimeConfig()
const form = await $fetch(`my_products/${this.$route.params.id}/`, { const route = useRoute()
this.form = await $fetch(`my_products/${route.params.id}/`, {
baseURL: config.public.baseURL, baseURL: config.public.baseURL,
method: 'GET', method: 'GET',
headers: { headers: {
Authorization: `Bearer ${this.auth.access}`, Authorization: `Bearer ${this.auth.access}`,
}, },
}) })
if (form.source !== 'MANUAL') { if (this.form.source !== 'MANUAL' && this.form.history) {
try { try {
const result = await $fetch(`history/${form.history}/`, { const result = await $fetch(`history/${this.form.history}/`, {
baseURL: config.public.baseURL, baseURL: config.public.baseURL,
method: 'GET', method: 'GET',
headers: { headers: {
@@ -71,7 +83,6 @@ export default {
form.sync_date = null form.sync_date = null
} }
} }
return { form }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
@@ -81,24 +92,27 @@ export default {
formatDatetime: dataProcessing.formatDatetime, formatDatetime: dataProcessing.formatDatetime,
async submitProduct(value) { 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 { try {
await this.$api.put(`/my_products/${this.$route.params.id}/`, value, { await $fetch(`/my_products/${route.params.id}/`, {
baseURL: config.public.baseURL,
method: 'PUT',
body: value,
headers: { headers: {
'Content-Type': 'multipart/form-data', Authorization: `Bearer ${this.auth.access}`,
}, },
}) })
this.$bvToast.toast(`Producto actualizado correctamente`, { this.modalText = 'Producto actualizado correctamente'
title: 'latienda.coop', this.modalColor = 'success'
autoHideDelay: 5000, this.activeModal = true
appendToast: true, } catch (error) {
}) this.modalText = 'Ha habido un error'
} catch { this.modalColor = 'danger'
this.$bvToast.toast(`Ha habido un error`, { this.activeModal = true
title: 'latienda.coop', console.error('Error updating product:', error)
autoHideDelay: 5000,
appendToast: true,
variant: 'danger',
})
} }
}, },
}, },

View File

@@ -1,15 +1,118 @@
<template> <template>
<div> <BContainer class="container">
<h1>Crear producto /editar/productos/crear</h1> <BModal
</div> 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> </template>
<script> <script>
export default { 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> </script>
<style lang="scss" scoped> <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> </style>