feat: add inline catalog checkout with modals
Replace redirect to /comprar with a 2-step modal flow (cart confirmation + personal data) on the catalog page. Add createCatalogPurchase API endpoint for catalog sales.
This commit is contained in:
@@ -87,6 +87,98 @@
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Modal 1: Confirmación de productos -->
|
||||
<v-dialog v-model="checkoutDialog" max-width="600" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="headline">Confirmar Compra</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list v-if="cartItems.length > 0" class="product-list-scroll">
|
||||
<v-list-item v-for="item in cartItems" :key="item.id">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div>
|
||||
<div class="font-weight-medium">{{ item.name }}</div>
|
||||
<div class="text-caption text-grey">
|
||||
{{ currency(item.price) }} x {{ item.quantity }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-weight-bold text-success">
|
||||
{{ currency(item.price * item.quantity) }}
|
||||
</div>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-divider class="my-3"></v-divider>
|
||||
<div class="d-flex justify-space-between text-h6">
|
||||
<span class="font-weight-bold">Total</span>
|
||||
<span class="font-weight-bold text-success">{{
|
||||
currency(cartStore.cartTotal)
|
||||
}}</span>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="checkoutDialog = false">Cancelar</v-btn>
|
||||
<v-btn color="primary" variant="elevated" @click="onConfirmCheckout"
|
||||
>Confirmar</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Modal 2: Datos personales -->
|
||||
<v-dialog v-model="personalDataDialog" max-width="500" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="headline">Datos de Contacto</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="personalForm">
|
||||
<v-text-field
|
||||
v-model="customerName"
|
||||
label="Nombre completo"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="customerAddress"
|
||||
label="Dirección"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="customerPhone"
|
||||
label="Teléfono"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
></v-text-field>
|
||||
<v-select
|
||||
v-model="pickupMethod"
|
||||
:items="pickupOptions"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
label="Recogida"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
variant="outlined"
|
||||
></v-select>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelPurchase">Cancelar</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="onSubmitPurchase"
|
||||
:loading="isSubmitting"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Finalizar Compra
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
@@ -137,6 +229,20 @@ export default {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
itemsPerPageOptions: [10, 20, 50, 100],
|
||||
checkoutDialog: false,
|
||||
personalDataDialog: false,
|
||||
customerName: "",
|
||||
customerAddress: "",
|
||||
customerPhone: "",
|
||||
pickupMethod: "STORE",
|
||||
pickupOptions: [
|
||||
{ text: "En Sitio", value: "STORE" },
|
||||
{ text: "Domicilio", value: "DELIVERY" },
|
||||
],
|
||||
isSubmitting: false,
|
||||
rules: {
|
||||
required: (value) => !!value || "Requerido.",
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -178,11 +284,11 @@ export default {
|
||||
if (this.totalPages <= 7) {
|
||||
return this.totalPages;
|
||||
}
|
||||
|
||||
|
||||
// Breakpoints responsivos basados en windowWidth
|
||||
// OPTIMIZADO: Reducidos para evitar que desaparezcan los iconos de navegación
|
||||
const width = this.windowWidth;
|
||||
|
||||
|
||||
if (width < 400) {
|
||||
return 3; // Extra small mobile
|
||||
} else if (width < 680) {
|
||||
@@ -255,7 +361,65 @@ export default {
|
||||
}
|
||||
},
|
||||
goToCheckout() {
|
||||
this.$router.push("/comprar");
|
||||
this.checkoutDialog = true;
|
||||
},
|
||||
onConfirmCheckout() {
|
||||
this.checkoutDialog = false;
|
||||
this.personalDataDialog = true;
|
||||
},
|
||||
cancelPurchase() {
|
||||
this.checkoutDialog = false;
|
||||
this.personalDataDialog = false;
|
||||
this.customerName = "";
|
||||
this.customerAddress = "";
|
||||
this.customerPhone = "";
|
||||
this.pickupMethod = "STORE";
|
||||
},
|
||||
async onSubmitPurchase() {
|
||||
const form = this.$refs.personalForm;
|
||||
|
||||
if (form) {
|
||||
const { valid } = await form.validate();
|
||||
if (!valid) return;
|
||||
}
|
||||
this.isSubmitting = true;
|
||||
const payload = {
|
||||
date: this.getCurrentDate(),
|
||||
customer: 1,
|
||||
notes: "",
|
||||
payment_method: "CASH",
|
||||
saleline_set: this.cartItems.map((item) => ({
|
||||
product: item.id,
|
||||
unit_price: item.price,
|
||||
quantity: item.quantity,
|
||||
measuring_unit: item.measuring_unit || "Unidad",
|
||||
})),
|
||||
customer_name: this.customerName,
|
||||
customer_address: this.customerAddress,
|
||||
customer_phone: this.customerPhone,
|
||||
pickup_method: this.pickupMethod,
|
||||
};
|
||||
|
||||
this.api
|
||||
.createCatalogPurchase(payload)
|
||||
.then((data) => {
|
||||
this.cartStore.clearCart();
|
||||
this.personalDataDialog = false;
|
||||
this.$router.push({
|
||||
path: "/summary_purchase",
|
||||
query: { id: parseInt(data.id) },
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error al crear la compra:", error);
|
||||
this.isSubmitting = false;
|
||||
});
|
||||
},
|
||||
getCurrentDate() {
|
||||
const today = new Date();
|
||||
const gmtOffSet = -5;
|
||||
const localDate = new Date(today.getTime() + gmtOffSet * 60 * 60 * 1000);
|
||||
return localDate.toISOString().slice(0, 16);
|
||||
},
|
||||
toggleCart() {
|
||||
this.cartCollapsed = !this.cartCollapsed;
|
||||
@@ -396,4 +560,9 @@ export default {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.product-list-scroll {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,67 +1,71 @@
|
||||
class Api {
|
||||
constructor (apiImplementation) {
|
||||
this.apiImplementation = apiImplementation;
|
||||
}
|
||||
constructor(apiImplementation) {
|
||||
this.apiImplementation = apiImplementation;
|
||||
}
|
||||
|
||||
getCustomers() {
|
||||
return this.apiImplementation.getCustomers();
|
||||
}
|
||||
getCustomers() {
|
||||
return this.apiImplementation.getCustomers();
|
||||
}
|
||||
|
||||
getProducts() {
|
||||
return this.apiImplementation.getProducts();
|
||||
}
|
||||
getProducts() {
|
||||
return this.apiImplementation.getProducts();
|
||||
}
|
||||
|
||||
getPaymentMethods() {
|
||||
return this.apiImplementation.getPaymentMethods();
|
||||
}
|
||||
getPaymentMethods() {
|
||||
return this.apiImplementation.getPaymentMethods();
|
||||
}
|
||||
|
||||
getSummaryPurchase(purchaseId) {
|
||||
return this.apiImplementation.getSummaryPurchase(purchaseId);
|
||||
}
|
||||
getSummaryPurchase(purchaseId) {
|
||||
return this.apiImplementation.getSummaryPurchase(purchaseId);
|
||||
}
|
||||
|
||||
getPurchasesForReconciliation() {
|
||||
return this.apiImplementation.getPurchasesForReconciliation();
|
||||
}
|
||||
getPurchasesForReconciliation() {
|
||||
return this.apiImplementation.getPurchasesForReconciliation();
|
||||
}
|
||||
|
||||
getListReconcliations(page=1, itemsPerPage=10) {
|
||||
return this.apiImplementation.getListReconcliations(page, itemsPerPage);
|
||||
}
|
||||
getListReconcliations(page = 1, itemsPerPage = 10) {
|
||||
return this.apiImplementation.getListReconcliations(page, itemsPerPage);
|
||||
}
|
||||
|
||||
getReconciliation(reconciliationId) {
|
||||
return this.apiImplementation.getReconciliation(reconciliationId);
|
||||
}
|
||||
getReconciliation(reconciliationId) {
|
||||
return this.apiImplementation.getReconciliation(reconciliationId);
|
||||
}
|
||||
|
||||
createPurchase(purchase) {
|
||||
return this.apiImplementation.createPurchase(purchase);
|
||||
}
|
||||
createPurchase(purchase) {
|
||||
return this.apiImplementation.createCatalogPurchase(purchase);
|
||||
}
|
||||
|
||||
createReconciliationJar(reconciliation) {
|
||||
return this.apiImplementation.createReconciliationJar(reconciliation);
|
||||
}
|
||||
createCatalogPurchase(purchase) {
|
||||
return this.apiImplementation.createPurchase(purchase);
|
||||
}
|
||||
|
||||
createCustomer(customer) {
|
||||
return this.apiImplementation.createCustomer(customer);
|
||||
}
|
||||
createReconciliationJar(reconciliation) {
|
||||
return this.apiImplementation.createReconciliationJar(reconciliation);
|
||||
}
|
||||
|
||||
getCSVForTryton() {
|
||||
return this.apiImplementation.getCSVForTryton();
|
||||
}
|
||||
createCustomer(customer) {
|
||||
return this.apiImplementation.createCustomer(customer);
|
||||
}
|
||||
|
||||
getProductsFromTryton() {
|
||||
return this.apiImplementation.getProductsFromTryton();
|
||||
}
|
||||
getCSVForTryton() {
|
||||
return this.apiImplementation.getCSVForTryton();
|
||||
}
|
||||
|
||||
getCustomersFromTryton() {
|
||||
return this.apiImplementation.getCustomersFromTryton();
|
||||
}
|
||||
getProductsFromTryton() {
|
||||
return this.apiImplementation.getProductsFromTryton();
|
||||
}
|
||||
|
||||
sendSalesToTryton(){
|
||||
return this.apiImplementation.sendSalesToTryton();
|
||||
}
|
||||
getCustomersFromTryton() {
|
||||
return this.apiImplementation.getCustomersFromTryton();
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
return this.apiImplementation.getCurrentUser();
|
||||
}
|
||||
sendSalesToTryton() {
|
||||
return this.apiImplementation.sendSalesToTryton();
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
return this.apiImplementation.getCurrentUser();
|
||||
}
|
||||
}
|
||||
|
||||
export default Api;
|
||||
|
||||
@@ -60,6 +60,11 @@ class DjangoApi {
|
||||
return this.postRequest(url, purchase);
|
||||
}
|
||||
|
||||
createCatalogPurchase(purchase) {
|
||||
const url = this.base + "/don_confiao/api/catalog_sales/";
|
||||
return this.postRequest(url, purchase);
|
||||
}
|
||||
|
||||
createReconciliationJar(reconciliation) {
|
||||
const url = this.base + "/don_confiao/reconciliate_jar";
|
||||
return this.postRequest(url, reconciliation);
|
||||
|
||||
Reference in New Issue
Block a user