298 lines
8.7 KiB
Vue
298 lines
8.7 KiB
Vue
<script setup>
|
|
import { computed, ref } from 'vue';
|
|
import Swal from "sweetalert2";
|
|
import PayWithStripeButton from "@/components/displays/department/pos/displays/PayWithStripeButton.vue";
|
|
import {
|
|
customer_id,
|
|
department_id,
|
|
order_id,
|
|
order_items,
|
|
reg_1,
|
|
reset_all_values,
|
|
setStep,
|
|
pushPosRouteState,
|
|
} from "@/components/shop/POSDepartmentProcess.vue";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import { StripeModule } from "@/components/stripe/StripeModule.vue";
|
|
import {
|
|
metadata,
|
|
popups,
|
|
resetPos,
|
|
transactionItems,
|
|
} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
|
import { finalizeCurrentMobileOrder } from "@/components/displays/department/pos/steps/mobile/objects/mobileOrderCompletion.js";
|
|
|
|
const isCompleting = ref(false);
|
|
const completionError = ref(null);
|
|
const completedOrderId = ref(null);
|
|
|
|
const normalizedOrderId = computed(() => {
|
|
const parsed = Number.parseInt(String(order_id.value), 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
});
|
|
|
|
const normalizedDepartmentId = computed(() => {
|
|
const parsed = Number.parseInt(String(department_id.value), 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
});
|
|
|
|
const currentPaymentIntent = computed(() => StripeModule.paymentIntents.paymentIntent.value);
|
|
const paymentIntentState = computed(() => StripeModule.paymentIntents.getPaymentIntentState(currentPaymentIntent.value));
|
|
const hasActivePaymentIntent = computed(() => StripeModule.paymentIntents.isPaymentIntentActive(currentPaymentIntent.value));
|
|
|
|
const orderItemsTotal = computed(() => {
|
|
return (Array.isArray(order_items.value) ? order_items.value : []).reduce((sum, item) => {
|
|
const quantity = Number(item?.quantity ?? 1);
|
|
const price = Number(item?.price ?? item?.product?.price ?? 0);
|
|
return sum + (Number.isFinite(quantity) ? quantity : 0) * (Number.isFinite(price) ? price : 0);
|
|
}, 0);
|
|
});
|
|
|
|
const mobileOrderTotal = computed(() => {
|
|
const transactionTotal = Number(transactionItems.getTransactionTotal?.() ?? 0);
|
|
if (Number.isFinite(transactionTotal) && transactionTotal > 0) {
|
|
return transactionTotal;
|
|
}
|
|
return orderItemsTotal.value;
|
|
});
|
|
|
|
const orderReference = computed(() => metadata.getReference?.() || '');
|
|
const orderNotes = computed(() => metadata.getNotes?.() || '');
|
|
const orderRegistration = computed(() => reg_1.value || 'Unknown vehicle');
|
|
const isBackDisabled = computed(() => isCompleting.value || paymentIntentState.value === 'succeeded');
|
|
|
|
const formatCurrency = (amount) => {
|
|
return new Intl.NumberFormat('da-DK', {
|
|
style: 'currency',
|
|
currency: 'DKK',
|
|
}).format(Number(amount || 0));
|
|
};
|
|
|
|
const navigateToItems = () => {
|
|
setStep(2);
|
|
if (normalizedOrderId.value) {
|
|
pushPosRouteState(`id=${normalizedOrderId.value}&customer_id=${customer_id.value}&step=2`);
|
|
return;
|
|
}
|
|
pushPosRouteState("step=2");
|
|
};
|
|
|
|
const clearSuccessfulMobileCardFlow = () => {
|
|
window.setTimeout(() => {
|
|
reset_all_values();
|
|
order_id.value = 0;
|
|
popups.clear();
|
|
resetPos();
|
|
}, 3000);
|
|
};
|
|
|
|
const completeMobileCardPayment = async () => {
|
|
if (!normalizedOrderId.value) {
|
|
return;
|
|
}
|
|
if (isCompleting.value || completedOrderId.value === normalizedOrderId.value) {
|
|
return;
|
|
}
|
|
|
|
isCompleting.value = true;
|
|
completionError.value = null;
|
|
completedOrderId.value = normalizedOrderId.value;
|
|
|
|
try {
|
|
await finalizeCurrentMobileOrder();
|
|
popups.select('completed_transaction', {
|
|
message: `Order #${normalizedOrderId.value} successfully created.`,
|
|
});
|
|
clearSuccessfulMobileCardFlow();
|
|
} catch (error) {
|
|
completedOrderId.value = null;
|
|
completionError.value = SessionUser.functions.parseErrorMessage(error) || 'Unable to complete card payment.';
|
|
console.error('Failed to complete mobile card payment:', error);
|
|
} finally {
|
|
isCompleting.value = false;
|
|
}
|
|
};
|
|
|
|
const onBackClick = async () => {
|
|
if (paymentIntentState.value === 'succeeded') {
|
|
return;
|
|
}
|
|
|
|
if (!hasActivePaymentIntent.value) {
|
|
navigateToItems();
|
|
return;
|
|
}
|
|
|
|
const result = await Swal.fire({
|
|
title: 'Payment in progress',
|
|
text: 'Resume the current payment or cancel it before returning to the item step.',
|
|
icon: 'warning',
|
|
showDenyButton: true,
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Resume payment',
|
|
denyButtonText: 'Cancel payment',
|
|
cancelButtonText: 'Stay here',
|
|
reverseButtons: true,
|
|
});
|
|
|
|
if (result.isConfirmed || result.dismiss) {
|
|
return;
|
|
}
|
|
|
|
if (result.isDenied && normalizedOrderId.value) {
|
|
try {
|
|
await StripeModule.paymentIntents.deletePaymentIntent(normalizedOrderId.value);
|
|
navigateToItems();
|
|
} catch (error) {
|
|
completionError.value = SessionUser.functions.parseErrorMessage(error) || 'Unable to cancel the current payment.';
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="mobile-card-payment-view" data-testid="pos-mobile-step-3">
|
|
<div class="mobile-card-payment-view__header">
|
|
<div>
|
|
<p class="mobile-card-payment-view__eyebrow">Mobile POS</p>
|
|
<h3 class="mobile-card-payment-view__title">Card payment</h3>
|
|
</div>
|
|
<button
|
|
class="button is-small is-light"
|
|
type="button"
|
|
data-testid="pos-mobile-step-3-back"
|
|
:disabled="isBackDisabled"
|
|
@click="onBackClick"
|
|
>
|
|
Back
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mobile-card-payment-view__summary box">
|
|
<div class="mobile-card-payment-view__summary-row">
|
|
<span>Order</span>
|
|
<strong data-testid="pos-mobile-step-3-order-id">
|
|
#{{ normalizedOrderId || 'Pending' }}
|
|
</strong>
|
|
</div>
|
|
<div class="mobile-card-payment-view__summary-row">
|
|
<span>Total</span>
|
|
<strong data-testid="pos-mobile-step-3-total">
|
|
{{ formatCurrency(mobileOrderTotal) }}
|
|
</strong>
|
|
</div>
|
|
<div class="mobile-card-payment-view__summary-row">
|
|
<span>Vehicle</span>
|
|
<strong data-testid="pos-mobile-step-3-registration">{{ orderRegistration }}</strong>
|
|
</div>
|
|
<div v-if="orderReference" class="mobile-card-payment-view__summary-row">
|
|
<span>Reference</span>
|
|
<strong data-testid="pos-mobile-step-3-reference">{{ orderReference }}</strong>
|
|
</div>
|
|
<div v-if="orderNotes" class="mobile-card-payment-view__notes" data-testid="pos-mobile-step-3-notes">
|
|
{{ orderNotes }}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="paymentIntentState === 'succeeded' && isCompleting"
|
|
class="notification is-info is-light"
|
|
data-testid="pos-mobile-step-3-completing"
|
|
>
|
|
Finalizing the paid order.
|
|
</div>
|
|
|
|
<div v-if="completionError" class="notification is-danger is-light" data-testid="pos-mobile-step-3-error">
|
|
<div>{{ completionError }}</div>
|
|
<button
|
|
v-if="paymentIntentState === 'succeeded'"
|
|
class="button is-small is-danger is-light mobile-card-payment-view__retry"
|
|
type="button"
|
|
data-testid="pos-mobile-step-3-retry-complete"
|
|
:disabled="isCompleting"
|
|
@click="completeMobileCardPayment"
|
|
>
|
|
Retry completion
|
|
</button>
|
|
</div>
|
|
|
|
<p class="mobile-card-payment-view__description">
|
|
Use the selected Stripe Terminal reader or send a Stripe payment link by email for this order.
|
|
</p>
|
|
|
|
<PayWithStripeButton
|
|
:department-id="normalizedDepartmentId"
|
|
:order_id="normalizedOrderId || 0"
|
|
:order_items="order_items"
|
|
:mobile-mode="true"
|
|
:on-payment-success="completeMobileCardPayment"
|
|
label="Start card payment"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.mobile-card-payment-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.mobile-card-payment-view__header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.mobile-card-payment-view__eyebrow {
|
|
margin: 0;
|
|
font-size: 0.72rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.mobile-card-payment-view__title {
|
|
margin: 0.15rem 0 0;
|
|
font-size: 1.2rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.mobile-card-payment-view__summary {
|
|
border-radius: 18px;
|
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.08);
|
|
}
|
|
|
|
.mobile-card-payment-view__summary-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
padding: 0.1rem 0;
|
|
}
|
|
|
|
.mobile-card-payment-view__summary-row span {
|
|
color: #6b7280;
|
|
}
|
|
|
|
.mobile-card-payment-view__description {
|
|
margin: 0;
|
|
color: #6b7280;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.mobile-card-payment-view__notes {
|
|
margin-top: 0.75rem;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
|
color: #374151;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.mobile-card-payment-view__retry {
|
|
margin-top: 0.75rem;
|
|
}
|
|
</style>
|