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>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -137,6 +229,20 @@ export default {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 20,
|
itemsPerPage: 20,
|
||||||
itemsPerPageOptions: [10, 20, 50, 100],
|
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: {
|
computed: {
|
||||||
@@ -255,7 +361,65 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
goToCheckout() {
|
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() {
|
toggleCart() {
|
||||||
this.cartCollapsed = !this.cartCollapsed;
|
this.cartCollapsed = !this.cartCollapsed;
|
||||||
@@ -396,4 +560,9 @@ export default {
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-list-scroll {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,67 +1,71 @@
|
|||||||
class Api {
|
class Api {
|
||||||
constructor (apiImplementation) {
|
constructor(apiImplementation) {
|
||||||
this.apiImplementation = apiImplementation;
|
this.apiImplementation = apiImplementation;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCustomers() {
|
getCustomers() {
|
||||||
return this.apiImplementation.getCustomers();
|
return this.apiImplementation.getCustomers();
|
||||||
}
|
}
|
||||||
|
|
||||||
getProducts() {
|
getProducts() {
|
||||||
return this.apiImplementation.getProducts();
|
return this.apiImplementation.getProducts();
|
||||||
}
|
}
|
||||||
|
|
||||||
getPaymentMethods() {
|
getPaymentMethods() {
|
||||||
return this.apiImplementation.getPaymentMethods();
|
return this.apiImplementation.getPaymentMethods();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSummaryPurchase(purchaseId) {
|
getSummaryPurchase(purchaseId) {
|
||||||
return this.apiImplementation.getSummaryPurchase(purchaseId);
|
return this.apiImplementation.getSummaryPurchase(purchaseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPurchasesForReconciliation() {
|
getPurchasesForReconciliation() {
|
||||||
return this.apiImplementation.getPurchasesForReconciliation();
|
return this.apiImplementation.getPurchasesForReconciliation();
|
||||||
}
|
}
|
||||||
|
|
||||||
getListReconcliations(page=1, itemsPerPage=10) {
|
getListReconcliations(page = 1, itemsPerPage = 10) {
|
||||||
return this.apiImplementation.getListReconcliations(page, itemsPerPage);
|
return this.apiImplementation.getListReconcliations(page, itemsPerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
getReconciliation(reconciliationId) {
|
getReconciliation(reconciliationId) {
|
||||||
return this.apiImplementation.getReconciliation(reconciliationId);
|
return this.apiImplementation.getReconciliation(reconciliationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPurchase(purchase) {
|
createPurchase(purchase) {
|
||||||
return this.apiImplementation.createPurchase(purchase);
|
return this.apiImplementation.createCatalogPurchase(purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
createReconciliationJar(reconciliation) {
|
createCatalogPurchase(purchase) {
|
||||||
return this.apiImplementation.createReconciliationJar(reconciliation);
|
return this.apiImplementation.createPurchase(purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
createCustomer(customer) {
|
createReconciliationJar(reconciliation) {
|
||||||
return this.apiImplementation.createCustomer(customer);
|
return this.apiImplementation.createReconciliationJar(reconciliation);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCSVForTryton() {
|
createCustomer(customer) {
|
||||||
return this.apiImplementation.getCSVForTryton();
|
return this.apiImplementation.createCustomer(customer);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProductsFromTryton() {
|
getCSVForTryton() {
|
||||||
return this.apiImplementation.getProductsFromTryton();
|
return this.apiImplementation.getCSVForTryton();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCustomersFromTryton() {
|
getProductsFromTryton() {
|
||||||
return this.apiImplementation.getCustomersFromTryton();
|
return this.apiImplementation.getProductsFromTryton();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendSalesToTryton(){
|
getCustomersFromTryton() {
|
||||||
return this.apiImplementation.sendSalesToTryton();
|
return this.apiImplementation.getCustomersFromTryton();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentUser() {
|
sendSalesToTryton() {
|
||||||
return this.apiImplementation.getCurrentUser();
|
return this.apiImplementation.sendSalesToTryton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCurrentUser() {
|
||||||
|
return this.apiImplementation.getCurrentUser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Api;
|
export default Api;
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ class DjangoApi {
|
|||||||
return this.postRequest(url, purchase);
|
return this.postRequest(url, purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createCatalogPurchase(purchase) {
|
||||||
|
const url = this.base + "/don_confiao/api/catalog_sales/";
|
||||||
|
return this.postRequest(url, purchase);
|
||||||
|
}
|
||||||
|
|
||||||
createReconciliationJar(reconciliation) {
|
createReconciliationJar(reconciliation) {
|
||||||
const url = this.base + "/don_confiao/reconciliate_jar";
|
const url = this.base + "/don_confiao/reconciliate_jar";
|
||||||
return this.postRequest(url, reconciliation);
|
return this.postRequest(url, reconciliation);
|
||||||
|
|||||||
Reference in New Issue
Block a user