Files
pleno-vue/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile3.vue
T

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>