feat: redesign purchase page with improved UI/UX and responsive layout
This commit is contained in:
@@ -1,147 +1,316 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-form ref="purchase" v-model="valid" @change="onFormChange">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-autocomplete
|
||||
v-model="purchase.customer"
|
||||
:items="filteredClients"
|
||||
:search="client_search"
|
||||
no-data-text="No se hallaron clientes"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
@update:model-value="onFormChange"
|
||||
label="Cliente"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
class="mr-4"
|
||||
></v-autocomplete>
|
||||
<!--<v-btn color="primary" @click="openModal">Agregar Cliente</v-btn>-->
|
||||
<CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer"/>
|
||||
</v-col>
|
||||
<v-col lg="4">
|
||||
<v-text-field
|
||||
v-model="purchase.date"
|
||||
label="Fecha"
|
||||
type="datetime-local"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-textarea
|
||||
v-model="purchase.notes"
|
||||
label="Notas"
|
||||
rows="2"
|
||||
></v-textarea>
|
||||
<v-divider></v-divider>
|
||||
<v-container>
|
||||
<v-toolbar>
|
||||
<v-toolbar-title secondary>Productos</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-container v-for="(line, index) in purchase.saleline_set" :key="line.id">
|
||||
<v-row>
|
||||
<v-col
|
||||
lg="9">
|
||||
<v-autocomplete
|
||||
v-model="line.product"
|
||||
:items="filteredProducts"
|
||||
:search="product_search"
|
||||
@update:modelValue="onProductChange(index)"
|
||||
no-data-text="No se hallaron productos"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
item-subtitle="Price"
|
||||
label="Producto"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props" :title="item.raw.name" :subtitle="formatPrice(item.raw.price)"></v-list-item>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
<v-col
|
||||
lg="2"
|
||||
>
|
||||
<v-text-field
|
||||
v-model.number="line.quantity"
|
||||
label="Cantidad"
|
||||
type="number"
|
||||
:rules="[rules.required,rules.positive]"
|
||||
<v-container fluid class="pa-4 pa-md-6">
|
||||
<v-form ref="purchase" v-model="valid" @change="onFormChange">
|
||||
|
||||
<!-- Encabezado -->
|
||||
<v-sheet class="page-header d-flex align-center pa-4 pa-md-6 mb-4 rounded-lg">
|
||||
<v-icon start size="40" color="white" class="mr-3">mdi-cart-plus</v-icon>
|
||||
<div>
|
||||
<h1 class="text-h5 text-md-h4 font-weight-bold text-white mb-0">Nueva Compra</h1>
|
||||
<p class="text-body-2 text-white text-medium-emphasis mb-0">Registra una nueva venta en el sistema</p>
|
||||
</div>
|
||||
</v-sheet>
|
||||
|
||||
<!-- Loading -->
|
||||
<template v-if="loading">
|
||||
<v-skeleton-loader
|
||||
class="mb-4 rounded-lg"
|
||||
type="card-heading, list-item-two-line, list-item-two-line"
|
||||
></v-skeleton-loader>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- Card: Información del Cliente -->
|
||||
<v-card class="mb-4 rounded-lg" elevation="2">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon color="primary" size="36">mdi-account</v-icon>
|
||||
</template>
|
||||
<v-card-title class="font-weight-bold text-h6">Información del Cliente</v-card-title>
|
||||
<v-card-subtitle>Datos básicos de la compra</v-card-subtitle>
|
||||
</v-card-item>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-autocomplete
|
||||
v-model="purchase.customer"
|
||||
:items="clients"
|
||||
:search="client_search"
|
||||
no-data-text="No se hallaron clientes"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
@update:model-value="onFormChange"
|
||||
label="Cliente"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model.number="line.unit_price"
|
||||
label="Precio"
|
||||
type="number"
|
||||
:rules="[rules.required]"
|
||||
prefix="$"
|
||||
required
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="line.measuring_unit"
|
||||
label="UdM"
|
||||
persistent-placeholder="true"
|
||||
readonly
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
type="number"
|
||||
:value="calculateSubtotal(line)"
|
||||
label="Subtotal"
|
||||
prefix="$"
|
||||
readonly
|
||||
disable
|
||||
persistent-placeholder="true"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn @click="removeLine(index)" color="red">Eliminar</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-alert type="warning" :duration="2000" closable v-model="show_alert_lines">
|
||||
No se puede eliminar la única línea.
|
||||
</v-alert>
|
||||
</v-container>
|
||||
<v-btn @click="addLine" color="blue">Agregar</v-btn>
|
||||
</v-container>
|
||||
<v-divider></v-divider>
|
||||
<v-text-field
|
||||
:value="calculateTotal"
|
||||
label="Total"
|
||||
prefix="$"
|
||||
readonly
|
||||
persistent-placeholder="true"
|
||||
></v-text-field>
|
||||
<v-container v-if="calculateTotal > 0">
|
||||
<v-select
|
||||
:items="payment_methods"
|
||||
v-model="purchase.payment_method"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
label="Pago en"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
<v-btn @click="openCasherModal" v-if="purchase.payment_method === 'CASH'">Calcular Devuelta</v-btn>
|
||||
<CasherModal :total_purchase="calculateTotal" ref="casherModal"</CasherModal>
|
||||
</v-container>
|
||||
<v-btn @click="submit" color="green">Comprar</v-btn>
|
||||
<v-alert type="error" :duration="2000" closable v-model="show_alert_purchase">
|
||||
clearable
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
></v-autocomplete>
|
||||
<CreateCustomerModal ref="customerModal" @customerCreated="handleNewCustomer" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field
|
||||
v-model="purchase.date"
|
||||
label="Fecha"
|
||||
type="datetime-local"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
readonly
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="small"
|
||||
class="mt-1"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="openModal"
|
||||
>
|
||||
Nuevo Cliente
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-0">
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="purchase.notes"
|
||||
label="Notas"
|
||||
rows="2"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Card: Productos -->
|
||||
<v-card class="mb-4 rounded-lg" elevation="2">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon color="green-darken-1" size="36">mdi-package-variant-closed</v-icon>
|
||||
</template>
|
||||
<v-card-title class="font-weight-bold text-h6">Productos</v-card-title>
|
||||
<v-card-subtitle>Agrega los productos de la compra</v-card-subtitle>
|
||||
</v-card-item>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<!-- Encabezados de columnas (solo desktop) -->
|
||||
<div class="d-none d-md-flex px-6 pt-3 text-caption font-weight-bold text-grey">
|
||||
<div class="flex-grow-1 flex-shrink-1" style="min-width:0">Producto</div>
|
||||
<div class="mx-2 text-center" style="width:100px; flex-shrink:0">Cantidad</div>
|
||||
<div class="mx-2 text-end" style="width:110px; flex-shrink:0">Precio</div>
|
||||
<div class="mx-2 text-end" style="width:110px; flex-shrink:0">Subtotal</div>
|
||||
<div style="width:40px; flex-shrink:0"></div>
|
||||
</div>
|
||||
|
||||
<v-card-text class="pa-0 pa-md-4">
|
||||
<div
|
||||
v-for="(line, index) in purchase.saleline_set"
|
||||
:key="line.id"
|
||||
class="product-line pa-3 pa-md-2"
|
||||
>
|
||||
<v-row no-gutters align="start" class="flex-md-nowrap">
|
||||
<!-- Producto -->
|
||||
<v-col cols="12" md class="mb-2 mb-md-0">
|
||||
<v-autocomplete
|
||||
v-model="line.product"
|
||||
:items="products"
|
||||
:search="product_search"
|
||||
@update:modelValue="onProductChange(index)"
|
||||
no-data-text="No se hallaron productos"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
label="Producto"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
class="product-select"
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props" :title="item.raw.name" :subtitle="formatPrice(item.raw.price)"></v-list-item>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</v-col>
|
||||
|
||||
<!-- Cantidad -->
|
||||
<v-col cols="4" md="auto" class="pe-1">
|
||||
<v-text-field
|
||||
v-model.number="line.quantity"
|
||||
label="Cant."
|
||||
type="number"
|
||||
:rules="[rules.required, rules.positive]"
|
||||
required
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
min="0"
|
||||
class="quantity-field"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Precio -->
|
||||
<v-col cols="4" md="auto" class="px-1">
|
||||
<v-text-field
|
||||
v-model.number="line.unit_price"
|
||||
label="Precio"
|
||||
type="number"
|
||||
:rules="[rules.required]"
|
||||
prefix="$"
|
||||
required
|
||||
readonly
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
class="price-field"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Subtotal + UdM -->
|
||||
<v-col cols="4" md="auto" class="ps-1">
|
||||
<v-text-field
|
||||
type="number"
|
||||
:value="calculateSubtotal(line)"
|
||||
label="Subtotal"
|
||||
prefix="$"
|
||||
readonly
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
class="subtotal-field"
|
||||
></v-text-field>
|
||||
<div class="text-caption text-grey text-center mt-1 d-md-none">{{ line.measuring_unit || 'Ud' }}</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Eliminar + UdM desktop -->
|
||||
<v-col cols="12" md="auto" class="d-flex align-center mt-2 mt-md-0 ps-md-2">
|
||||
<v-btn
|
||||
@click="removeLine(index)"
|
||||
color="error"
|
||||
variant="text"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
density="comfortable"
|
||||
class="flex-shrink-0"
|
||||
></v-btn>
|
||||
<span class="text-caption text-grey ms-2 d-none d-md-inline">{{ line.measuring_unit || 'Ud' }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
type="warning"
|
||||
closable
|
||||
v-model="show_alert_lines"
|
||||
density="compact"
|
||||
variant="tonal"
|
||||
class="ma-3"
|
||||
>
|
||||
No se puede eliminar la única línea.
|
||||
</v-alert>
|
||||
|
||||
<v-divider class="my-2"></v-divider>
|
||||
|
||||
<v-btn
|
||||
@click="addLine"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-plus"
|
||||
class="mt-2"
|
||||
>
|
||||
Agregar Producto
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Card: Resumen y Pago -->
|
||||
<v-card v-if="calculateTotal > 0" class="mb-4 rounded-lg" elevation="2">
|
||||
<v-card-item>
|
||||
<template #prepend>
|
||||
<v-icon color="green-darken-2" size="36">mdi-credit-card</v-icon>
|
||||
</template>
|
||||
<v-card-title class="font-weight-bold text-h6">Resumen y Pago</v-card-title>
|
||||
</v-card-item>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row align="center">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
:items="payment_methods"
|
||||
v-model="purchase.payment_method"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
label="Método de Pago"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="text-center text-md-end">
|
||||
<div class="text-body-2 text-grey mb-1">Total de la compra</div>
|
||||
<div class="total-amount">{{ formatPrice(calculateTotal) }}</div>
|
||||
<v-btn
|
||||
v-if="purchase.payment_method === 'CASH'"
|
||||
@click="openCasherModal"
|
||||
color="orange-darken-2"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-cash"
|
||||
size="small"
|
||||
class="mt-1"
|
||||
>
|
||||
Calcular Devuelta
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Botón Comprar -->
|
||||
<div class="d-flex flex-column flex-sm-row justify-space-between align-center ga-4 mt-6">
|
||||
<div class="text-caption text-grey">
|
||||
<v-icon start size="small" color="grey">mdi-information</v-icon>
|
||||
Todos los campos marcados con * son obligatorios
|
||||
</div>
|
||||
<v-btn
|
||||
@click="submit"
|
||||
color="green-darken-1"
|
||||
size="x-large"
|
||||
prepend-icon="mdi-cart-check"
|
||||
variant="elevated"
|
||||
class="px-8 submit-btn"
|
||||
:disabled="calculateTotal <= 0"
|
||||
>
|
||||
Comprar
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
type="error"
|
||||
closable
|
||||
v-model="show_alert_purchase"
|
||||
density="compact"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
>
|
||||
Verifique los campos obligatorios.
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</v-container>
|
||||
|
||||
<CasherModal :total_purchase="calculateTotal" ref="casherModal" />
|
||||
</template>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -161,6 +330,7 @@
|
||||
data() {
|
||||
return {
|
||||
api: inject('api'),
|
||||
loading: true,
|
||||
valid: false,
|
||||
form_changed: false,
|
||||
show_alert_lines: false,
|
||||
@@ -179,54 +349,21 @@
|
||||
required: value => !!value || 'Requerido.',
|
||||
positive: value => value > 0 || 'La cantidad debe ser mayor que 0.',
|
||||
},
|
||||
menuItems: [
|
||||
{ title: 'Inicio', route: '/'},
|
||||
{ title: 'Compras', route:'/compras'},
|
||||
],
|
||||
clients: [],
|
||||
products: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchClients();
|
||||
this.fetchProducts();
|
||||
this.fetchPaymentMethods();
|
||||
},
|
||||
watch: {
|
||||
group () {
|
||||
this.drawer = false
|
||||
},
|
||||
created() {
|
||||
this.fetchClients();
|
||||
this.fetchProducts();
|
||||
this.fetchPaymentMethods();
|
||||
},
|
||||
beforeMount() {
|
||||
window.addEventListener('beforeunload', this.confirmLeave);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('beforeunload', this.confirmLeave);
|
||||
},
|
||||
computed: {
|
||||
calculateTotal() {
|
||||
return this.purchase.saleline_set.reduce((total, saleline) => {
|
||||
return total + this.calculateSubtotal(saleline);
|
||||
}, 0);
|
||||
},
|
||||
filteredClients() {
|
||||
return this.clients.filter(client => {
|
||||
if (this.client_search === '') {
|
||||
return [];
|
||||
} else {
|
||||
return client.name.toLowerCase().includes(this.client_search.toLowerCase());
|
||||
}
|
||||
});
|
||||
},
|
||||
filteredProducts() {
|
||||
return this.products.filter(product => {
|
||||
if (this.product_search === '') {
|
||||
return [];
|
||||
} else {
|
||||
return product.name.toLowerCase().includes(this.product_search.toLowerCase());
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
@@ -250,7 +387,6 @@
|
||||
const today = new Date();
|
||||
const gmtOffSet = -5;
|
||||
const localDate = new Date(today.getTime() + (gmtOffSet * 60 * 60 * 1000));
|
||||
// Formatear la fecha y hora en el formato YYYY-MM-DDTHH:MM
|
||||
const formattedDate = localDate.toISOString().slice(0,16);
|
||||
return formattedDate;
|
||||
},
|
||||
@@ -267,6 +403,9 @@
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleNewCustomer(newCustomer){
|
||||
@@ -330,7 +469,7 @@
|
||||
this.$router.push(route);
|
||||
},
|
||||
formatPrice(price) {
|
||||
return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'COP' }).format(price);
|
||||
return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', minimumFractionDigits: 0 }).format(price);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -339,4 +478,28 @@
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #1565C0 0%, #0D47A1 100%);
|
||||
}
|
||||
|
||||
.product-line:nth-child(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: rgb(46, 125, 50);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.total-amount {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user