Updated charge close to working

This commit is contained in:
2025-09-09 18:26:21 -04:00
parent fd11c9e794
commit 98fe855e65
19 changed files with 1785 additions and 372 deletions

View File

@@ -238,7 +238,7 @@ export default defineComponent({
console.log('Delivery ID:', this.delivery.id) console.log('Delivery ID:', this.delivery.id)
console.log('Service ID:', this.delivery.service_id) console.log('Service ID:', this.delivery.service_id)
console.log('Final payload being sent:', payload) console.log('Final payload being sent:', payload)
console.log('Endpoint:', endpoint)
const response = await axios.post( const response = await axios.post(
`${import.meta.env.VITE_AUTHORIZE_URL}/api/${endpoint}/?customer_id=${this.customer.id}`, `${import.meta.env.VITE_AUTHORIZE_URL}/api/${endpoint}/?customer_id=${this.customer.id}`,
payload, payload,

View File

@@ -94,6 +94,21 @@
</details> </details>
</li> </li>
<!-- Transactions Section -->
<li>
<details>
<summary class="font-bold text-lg">Transactions</summary>
<ul>
<li>
<router-link :to="{ name: 'transactionsAuthorize' }" exact-active-class="active">
Authorize
<span v-if="countsStore.transaction > 0" class="badge badge-secondary">{{ countsStore.transaction }}</span>
</router-link>
</li>
</ul>
</details>
</li>
<!-- Admin Section remains the same --> <!-- Admin Section remains the same -->
<li> <li>
<details> <details>

View File

@@ -46,7 +46,7 @@
</div> </div>
<!-- Create Delivery Form (now in the left column) --> <!-- Create Delivery Form (now in the left column) -->
<div class="bg-base-100 rounded-lg p-4"> <div class="bg-neutral rounded-lg p-4">
<h2 class="text-2xl font-bold mb-4">Create Delivery Order</h2> <h2 class="text-2xl font-bold mb-4">Create Delivery Order</h2>
<form class="space-y-4" @submit.prevent="onDeliverySubmit"> <form class="space-y-4" @submit.prevent="onDeliverySubmit">
<!-- Gallons & Fill --> <!-- Gallons & Fill -->
@@ -55,7 +55,12 @@
<input v-model="formDelivery.gallons_ordered" :disabled="formDelivery.customer_asked_for_fill" <input v-model="formDelivery.gallons_ordered" :disabled="formDelivery.customer_asked_for_fill"
class="input input-bordered input-sm w-full max-w-xs" type="number" placeholder="# gallons" /> class="input input-bordered input-sm w-full max-w-xs" type="number" placeholder="# gallons" />
<div class="flex flex-wrap gap-2 mt-2"> <div class="flex flex-wrap gap-2 mt-2">
<button v-for="amount in quickGallonAmounts" :key="amount" @click.prevent="setGallons(amount)" class="btn btn-xs btn-outline">{{ amount }} gal</button> <button v-for="amount in quickGallonAmounts"
:key="amount"
@click.prevent="setGallons(amount)"
:class="['btn', 'btn-xs', selectedGallonsAmount == amount ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">
{{ amount }} gal
</button>
</div> </div>
<span v-if="v$.formDelivery.gallons_ordered.$error" class="text-red-500 text-xs mt-1"> <span v-if="v$.formDelivery.gallons_ordered.$error" class="text-red-500 text-xs mt-1">
Required unless "Fill" is checked. Required unless "Fill" is checked.
@@ -107,12 +112,9 @@
<div v-if="userCards.length > 0 && formDelivery.credit"> <div v-if="userCards.length > 0 && formDelivery.credit">
<label class="label"><span class="label-text">Select Card</span></label> <label class="label"><span class="label-text">Select Card</span></label>
<select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.credit_card_id"> <div class="flex flex-wrap gap-2 mt-2">
<option disabled :value="0">Select a card</option> <button v-for="card in userCards" :key="card.id" @click.prevent="selectCreditCard(card.id)" :class="['btn', 'btn-xs', formDelivery.credit_card_id === card.id ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">{{ card.type_of_card }} - ****{{ card.last_four_digits }}</button>
<option v-for="card in userCards" :key="card.id" :value="card.id"> </div>
{{ card.type_of_card }} - {{ card.card_number }}
</option>
</select>
<span v-if="v$.formDelivery.credit_card_id.$error" class="text-red-500 text-xs mt-1"> <span v-if="v$.formDelivery.credit_card_id.$error" class="text-red-500 text-xs mt-1">
You must select a credit card when choosing CC option. You must select a credit card when choosing CC option.
</span> </span>
@@ -125,14 +127,23 @@
<label class="label"><span class="label-text font-bold">Expected Delivery Date</span></label> <label class="label"><span class="label-text font-bold">Expected Delivery Date</span></label>
<input v-model="formDelivery.expected_delivery_date" class="input input-bordered input-sm w-full max-w-xs" type="date" /> <input v-model="formDelivery.expected_delivery_date" class="input input-bordered input-sm w-full max-w-xs" type="date" />
<div class="flex flex-wrap gap-2 mt-2"> <div class="flex flex-wrap gap-2 mt-2">
<button @click.prevent="setDeliveryDate(0)" class="btn btn-xs btn-outline">Today</button> <button @click.prevent="setDeliveryDate(1)" :class="['btn', 'btn-xs', isDeliveryDateSelected(1) ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">Tomorrow</button>
<button @click.prevent="setDeliveryDate(1)" class="btn btn-xs btn-outline">Tomorrow</button>
<button @click.prevent="setDeliveryDate(2)" class="btn btn-xs btn-outline">In 2 Days</button> <button @click.prevent="setDeliveryDate(2)" :class="['btn', 'btn-xs', isDeliveryDateSelected(2) ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">In 2 Days</button>
<button @click.prevent="setDeliveryDate(3)" class="btn btn-xs btn-outline">In 3 Days</button> <button @click.prevent="setDeliveryDate(3)" :class="['btn', 'btn-xs', isDeliveryDateSelected(3) ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">In 3 Days</button>
<button @click.prevent="setDeliveryDate(0)" :class="['btn', 'btn-xs', isDeliveryDateSelected(0) ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">Today</button>
</div> </div>
<span v-if="v$.formDelivery.expected_delivery_date.$error" class="text-red-500 text-xs mt-1">Date is required.</span> <span v-if="v$.formDelivery.expected_delivery_date.$error" class="text-red-500 text-xs mt-1">Date is required.</span>
</div> </div>
<!-- Optional Section Divider -->
<div class="flex items-center my-4">
<div class="flex-grow h-px bg-gray-300"></div>
<span class="px-3 text-gray-500 text-sm font-medium">Optional</span>
<div class="flex-grow h-px bg-gray-300"></div>
</div>
<div> <div>
<label class="label"><span class="label-text font-bold">Apply Promotion</span></label> <label class="label"><span class="label-text font-bold">Apply Promotion</span></label>
<select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.promo_id"> <select class="select select-bordered select-sm w-full max-w-xs" v-model="formDelivery.promo_id">
@@ -144,7 +155,7 @@
</div> </div>
<!-- Fees --> <!-- Fees -->
<div class="p-4 border rounded-md"> <div class="p-4 ">
<label class="label-text font-bold">Fees & Options</label> <label class="label-text font-bold">Fees & Options</label>
<div class="flex flex-wrap gap-x-6 gap-y-2"> <div class="flex flex-wrap gap-x-6 gap-y-2">
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Emergency</span><input v-model="formDelivery.emergency" type="checkbox" class="checkbox checkbox-xs" /></label></div> <div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Emergency</span><input v-model="formDelivery.emergency" type="checkbox" class="checkbox checkbox-xs" /></label></div>
@@ -180,7 +191,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="tier in pricingTiers" :key="tier.gallons" class="hover"> <tr v-for="tier in pricingTiers" :key="tier.gallons" :class="isPricingTierSelected(tier.gallons) ? 'bg-blue-600 text-white hover:bg-blue-600' : 'hover'">
<td>{{ tier.gallons }}</td> <td>{{ tier.gallons }}</td>
<td>${{ Number(tier.price).toFixed(2) }}</td> <td>${{ Number(tier.price).toFixed(2) }}</td>
</tr> </tr>
@@ -342,19 +353,19 @@ interface PricingTier {
price: number | string; price: number | string;
} }
interface DeliveryFormData { interface DeliveryFormData {
gallons_ordered: string; gallons_ordered?: string;
customer_asked_for_fill: boolean; customer_asked_for_fill?: boolean;
expected_delivery_date: string; expected_delivery_date?: string;
dispatcher_notes_taken: string; dispatcher_notes_taken?: string;
prime: boolean; prime?: boolean;
emergency: boolean; emergency?: boolean;
same_day: boolean; same_day?: boolean;
credit: boolean; credit?: boolean;
cash: boolean; cash?: boolean;
check: boolean; check?: boolean;
other: boolean; other?: boolean;
credit_card_id: number; credit_card_id?: number;
promo_id: number; promo_id?: number;
} }
interface CardFormData { interface CardFormData {
card_name: string; card_name: string;
@@ -374,7 +385,7 @@ export default defineComponent({
return { return {
v$: useValidate(), v$: useValidate(),
user: null as any, user: null as any,
quickGallonAmounts: [100, 125, 150, 200, 220], quickGallonAmounts: [100, 125, 150, 175, 200, 220],
userCards: [] as UserCard[], userCards: [] as UserCard[],
promos: [] as Promo[], promos: [] as Promo[],
truckDriversList: [] as Driver[], truckDriversList: [] as Driver[],
@@ -463,7 +474,11 @@ export default defineComponent({
return types[this.customer.customer_home_type] || 'Unknown type'; return types[this.customer.customer_home_type] || 'Unknown type';
}, },
isAnyPaymentMethodSelected(): boolean { isAnyPaymentMethodSelected(): boolean {
return this.formDelivery.credit || this.formDelivery.cash || this.formDelivery.check || this.formDelivery.other; return !!(this.formDelivery?.credit || this.formDelivery?.cash || this.formDelivery?.check || this.formDelivery?.other);
},
selectedGallonsAmount(): number {
const value = this.formDelivery.gallons_ordered ?? '';
return Number(value);
} }
}, },
created() { created() {
@@ -488,11 +503,30 @@ export default defineComponent({
this.formDelivery.gallons_ordered = String(amount); this.formDelivery.gallons_ordered = String(amount);
this.formDelivery.customer_asked_for_fill = false; this.formDelivery.customer_asked_for_fill = false;
}, },
selectCreditCard(cardId: number) {
this.formDelivery.credit_card_id = cardId;
},
setDeliveryDate(days: number) { setDeliveryDate(days: number) {
const date = new Date(); const date = new Date();
date.setDate(date.getDate() + days); date.setDate(date.getDate() + days);
this.formDelivery.expected_delivery_date = date.toISOString().split('T')[0]; this.formDelivery.expected_delivery_date = date.toISOString().split('T')[0];
}, },
isDeliveryDateSelected(days: number): boolean {
const date = new Date();
date.setDate(date.getDate() + days);
return this.formDelivery.expected_delivery_date === date.toISOString().split('T')[0];
},
isPricingTierSelected(tierGallons: number | string): boolean {
if (!this.formDelivery.gallons_ordered) return false;
const selectedGallons = Number(this.formDelivery.gallons_ordered);
if (isNaN(selectedGallons)) return false;
const tierNum = Number(tierGallons);
if (isNaN(tierNum)) return false;
const shouldHighlight = selectedGallons === tierNum;
return shouldHighlight;
},
getPricingTiers() { getPricingTiers() {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers"; let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() }) axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })

View File

@@ -1,150 +1,240 @@
<!-- src/pages/delivery/edit.vue --> <!-- src/pages/delivery/edit.vue -->
<template> <template>
<div class="flex"> <div class="flex">
<!-- Main Content --> <!-- Main container with reduced horizontal padding -->
<div class="w-full px-4 md:px-10 py-4"> <div class="w-full px-4 md:px-6 py-4">
<!-- Breadcrumbs Navigation --> <!-- Breadcrumbs Navigation -->
<div class="text-sm breadcrumbs"> <div class="text-sm breadcrumbs">
<ul> <ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li> <li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Edit Oil Delivery #{{ deliveryOrder.id }}</li> <li>Edit Delivery </li>
</ul> </ul>
</div> </div>
<!-- TOP SECTION: Customer and Payment Info --> <!-- Main Title -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 my-6"> <div class="mb-6">
<!-- Customer Info Card --> <h1 class="text-3xl font-bold">Edit Delivery Order #{{ deliveryOrder.id }}</h1>
<div v-if="customer.id" class="bg-neutral rounded-lg p-5">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
</div>
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
View Profile
</router-link>
</div>
<div>
<div>{{ customer.customer_address }}</div>
<div v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</div>
<div>{{ customer.customer_town }}, {{ stateName }} {{ customer.customer_zip }}</div>
<div class="mt-2">{{ customer.customer_phone_number }}</div>
</div>
</div>
<!-- Payment Card on File Card -->
<div v-if="deliveryOrder.payment_type === 1 && userCard.id" class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Card on File</h3>
<div class="space-y-1">
<p><span class="font-semibold">Card Type:</span> {{ userCard.type_of_card }}</p>
<p><span class="font-semibold">Card Number:</span> {{ userCard.card_number }}</p>
<p><span class="font-semibold">Name:</span> {{ userCard.name_on_card }}</p>
<p><span class="font-semibold">Expires:</span> {{ userCard.expiration_month }}/{{ userCard.expiration_year }}</p>
<p><span class="font-semibold">CVV:</span> {{ userCard.security_number }}</p>
</div>
</div>
</div> </div>
<!-- BOTTOM SECTION: Edit Form -->
<div class="bg-neutral rounded-lg p-6">
<h2 class="text-2xl font-bold mb-4">Update Delivery Details</h2>
<form @submit.prevent="onSubmit" class="space-y-4">
<!-- Gallons Ordered & Fill Checkbox -->
<div class="flex items-end gap-4">
<div class="flex-grow">
<label class="label"><span class="label-text font-bold">Gallons Ordered</span></label>
<input v-model="CreateOilOrderForm.basicInfo.gallons_ordered" :disabled="CreateOilOrderForm.basicInfo.customer_asked_for_fill"
class="input input-bordered input-sm w-full max-w-xs" type="number" placeholder="# gallons" />
<span v-if="v$.CreateOilOrderForm.basicInfo.gallons_ordered.$error" class="text-red-500 text-xs mt-1">
{{ v$.CreateOilOrderForm.basicInfo.gallons_ordered.$errors[0].$message }}
</span>
</div>
<div class="form-control pb-1">
<label class="label cursor-pointer justify-start gap-4">
<span class="label-text font-bold">Fill</span>
<input v-model="CreateOilOrderForm.basicInfo.customer_asked_for_fill" type="checkbox" class="checkbox checkbox-sm" />
</label>
</div>
</div>
<!-- Date Fields --> <!--
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> NEW LAYOUT: A single 2-column grid for the whole page.
<div> Gaps and spacing are reduced for a more compact feel.
<label class="label"><span class="label-text font-bold">Order Created Date</span></label> -->
<input v-model="CreateOilOrderForm.basicInfo.created_delivery_date" type="date" class="input input-bordered input-sm w-full" /> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">
<!-- LEFT COLUMN: Primary Information & Actions -->
<div class="space-y-4">
<!-- Customer Info Card -->
<div v-if="customer.id" class="bg-neutral rounded-lg p-5">
<div class="flex justify-between items-center mb-4">
<div>
<div class="text-xl font-bold">{{ customer.customer_first_name }} {{ customer.customer_last_name }}</div>
<div class="text-sm text-gray-400">Account: {{ customer.account_number }}</div>
</div>
<router-link :to="{ name: 'customerProfile', params: { id: customer.id } }" class="btn btn-secondary btn-sm">
View Profile
</router-link>
</div> </div>
<div> <div>
<label class="label"><span class="label-text font-bold">Expected Delivery Date</span></label> <div>{{ customer.customer_address }}</div>
<input v-model="CreateOilOrderForm.basicInfo.expected_delivery_date" type="date" class="input input-bordered input-sm w-full" /> <div v-if="customer.customer_apt && customer.customer_apt !== 'None'">{{ customer.customer_apt }}</div>
<span v-if="v$.CreateOilOrderForm.basicInfo.expected_delivery_date.$error" class="text-red-500 text-xs mt-1"> <div>{{ customer.customer_town }}, {{ stateName }} {{ customer.customer_zip }}</div>
{{ v$.CreateOilOrderForm.basicInfo.expected_delivery_date.$errors[0].$message }} <div class="mt-2">{{ customer.customer_phone_number }}</div>
</span>
</div> </div>
</div> </div>
<!-- Fees & Options --> <!-- Edit Delivery Form (now in the left column) -->
<div class="p-4 border rounded-md"> <div class="bg-neutral rounded-lg p-4">
<label class="label-text font-bold">Fees & Options</label> <form class="space-y-4" @submit.prevent="onSubmit">
<div class="flex flex-wrap gap-x-6 gap-y-2 mt-2"> <!-- Fill Checkbox (moved above Gallons Ordered) -->
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Prime</span><input v-model="CreateOilOrderForm.basicInfo.prime" type="checkbox" class="checkbox checkbox-xs" /></label></div> <div class="form-control">
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Emergency</span><input v-model="CreateOilOrderForm.basicInfo.emergency" type="checkbox" class="checkbox checkbox-xs" /></label></div> <label class="label cursor-pointer justify-start gap-4">
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Same Day</span><input v-model="CreateOilOrderForm.basicInfo.same_day" type="checkbox" class="checkbox checkbox-xs" /></label></div> <span class="label-text font-bold">Fill</span>
<input v-model="CreateOilOrderForm.basicInfo.customer_asked_for_fill" type="checkbox" class="checkbox checkbox-sm" />
</label>
</div>
<!-- Gallons Ordered -->
<div>
<label class="label"><span class="label-text font-bold">Gallons Ordered</span></label>
<input v-model="CreateOilOrderForm.basicInfo.gallons_ordered" :disabled="CreateOilOrderForm.basicInfo.customer_asked_for_fill"
class="input input-bordered input-sm w-full max-w-xs" type="number" placeholder="# gallons" />
<div class="flex flex-wrap gap-2 mt-2">
<button v-for="amount in quickGallonAmounts" :key="amount" @click.prevent="setGallons(amount)" :class="['btn', 'btn-xs', selectedGallonsAmount == amount ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">{{ amount }} gal</button>
</div>
<span v-if="v$.CreateOilOrderForm.basicInfo.gallons_ordered.$error" class="text-red-500 text-xs mt-1">
Required unless "Fill" is checked.
</span>
</div>
<!-- Payment Section -->
<div class="p-4 rounded-md space-y-2 border"
:class="{ 'border-red-500 bg-red-50/50': v$.isAnyPaymentMethodSelected.$error }">
<label class="label-text font-bold">Payment Method</label>
<div v-if="v$.isAnyPaymentMethodSelected.$error" class="text-red-600 text-sm font-medium">
Please select a payment method.
</div>
<div class="flex flex-wrap gap-x-6 gap-y-2">
<div v-if="userCards.length > 0" class="form-control">
<label class="label cursor-pointer gap-2">
<span class="label-text">Credit</span>
<input v-model="CreateOilOrderForm.basicInfo.credit" type="checkbox" class="checkbox checkbox-xs" />
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer gap-2">
<span class="label-text">Cash</span>
<input v-model="CreateOilOrderForm.basicInfo.cash" type="checkbox" class="checkbox checkbox-xs" />
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer gap-2">
<span class="label-text">Check</span>
<input v-model="CreateOilOrderForm.basicInfo.check" type="checkbox" class="checkbox checkbox-xs" />
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer gap-2">
<span class="label-text">Other</span>
<input v-model="CreateOilOrderForm.basicInfo.other" type="checkbox" class="checkbox checkbox-xs" />
</label>
</div>
</div>
<div v-if="userCards.length > 0 && CreateOilOrderForm.basicInfo.credit">
<label class="label"><span class="label-text">Select Card</span></label>
<div class="flex flex-wrap gap-2 mt-2">
<button v-for="card in userCards" :key="card.id" @click.prevent="selectCreditCard(card.id)" :class="['btn', 'btn-xs', CreateOilOrderForm.basicInfo.credit_card_id === card.id ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">{{ card.type_of_card }} - ****{{ card.last_four_digits }}</button>
</div>
<span v-if="v$.CreateOilOrderForm.basicInfo.credit_card_id.$error" class="text-red-500 text-xs mt-1">
You must select a credit card when choosing CC option.
</span>
</div>
<div v-if="userCards.length === 0" class="text-sm text-warning">No cards on file for credit payment.</div>
</div>
<!-- Date Fields -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="label"><span class="label-text font-bold">Order Created Date</span></label>
<input v-model="CreateOilOrderForm.basicInfo.created_delivery_date" type="date" class="input input-bordered input-sm w-full" />
</div>
<div>
<label class="label"><span class="label-text font-bold">Expected Delivery Date</span></label>
<input v-model="CreateOilOrderForm.basicInfo.expected_delivery_date" type="date" class="input input-bordered input-sm w-full" />
<div class="flex flex-wrap gap-2 mt-2">
<button @click.prevent="setDeliveryDate(0)" :class="['btn', 'btn-xs', isDeliveryDateSelected(0) ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">Today</button>
<button @click.prevent="setDeliveryDate(1)" :class="['btn', 'btn-xs', isDeliveryDateSelected(1) ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">Tomorrow</button>
<button @click.prevent="setDeliveryDate(2)" :class="['btn', 'btn-xs', isDeliveryDateSelected(2) ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">In 2 Days</button>
<button @click.prevent="setDeliveryDate(3)" :class="['btn', 'btn-xs', isDeliveryDateSelected(3) ? 'bg-blue-600 text-white border-blue-600' : 'btn-outline']">In 3 Days</button>
</div>
<span v-if="v$.CreateOilOrderForm.basicInfo.expected_delivery_date.$error" class="text-red-500 text-xs mt-1">Date is required.</span>
</div>
</div>
<!-- Optional Section Divider -->
<div class="flex items-center my-4">
<div class="flex-grow h-px bg-gray-300"></div>
<span class="px-3 text-gray-500 text-sm font-medium">Optional</span>
<div class="flex-grow h-px bg-gray-300"></div>
</div>
<div>
<label class="label"><span class="label-text font-bold">Apply Promotion</span></label>
<select class="select select-bordered select-sm w-full max-w-xs" v-model="CreateOilOrderForm.basicInfo.promo_id">
<option :value="0">No Promotion</option>
<option v-for="promo in promos" :key="promo.id" :value="promo.id">
{{ promo.name_of_promotion }} (${{ promo.money_off_delivery }} off)
</option>
</select>
</div>
<!-- Fees & Options -->
<div class="p-4 rounded-md space-y-2">
<label class="label-text font-bold">Fees & Options</label>
<div class="flex flex-wrap gap-x-6 gap-y-2">
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Emergency</span><input v-model="CreateOilOrderForm.basicInfo.emergency" type="checkbox" class="checkbox checkbox-xs" /></label></div>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Prime</span><input v-model="CreateOilOrderForm.basicInfo.prime" type="checkbox" class="checkbox checkbox-xs" /></label></div>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Same Day</span><input v-model="CreateOilOrderForm.basicInfo.same_day" type="checkbox" class="checkbox checkbox-xs" /></label></div>
</div>
</div>
<!-- Notes -->
<div>
<label class="label"><span class="label-text font-bold">Dispatcher Notes</span></label>
<textarea v-model="CreateOilOrderForm.basicInfo.dispatcher_notes_taken" rows="3" class="textarea textarea-bordered w-full" placeholder="Notes for the driver..."></textarea>
</div>
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</form>
</div>
</div>
<!-- RIGHT COLUMN: Reference Information & Secondary Actions -->
<div class="space-y-4">
<!-- Pricing Chart Card -->
<div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Today's Price Per Gallon</h3>
<div class="overflow-x-auto">
<table class="table table-sm w-full">
<thead>
<tr>
<th>Gallons</th>
<th>Total Price</th>
</tr>
</thead>
<tbody>
<tr v-for="tier in pricingTiers" :key="tier.gallons" :class="isPricingTierSelected(tier.gallons) ? 'bg-blue-600 text-white hover:bg-blue-600' : 'hover'">
<td>{{ tier.gallons }}</td>
<td>${{ Number(tier.price).toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
<!-- Payment Type Radio Group --> <!-- Credit Cards Display -->
<div class="p-4 border rounded-md space-y-3"> <div v-if="customer && customer.id" class="bg-neutral rounded-lg p-5">
<label class="label-text font-bold">Payment Method</label> <div class="flex justify-between items-center">
<div class="flex flex-wrap gap-x-6 gap-y-2"> <h2 class="text-xl font-bold">Credit Cards</h2>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Cash</span><input type="radio" v-model="CreateOilOrderForm.basicInfo.payment_type" :value="0" class="radio radio-xs" /></label></div> <router-link :to="{ name: 'cardadd', params: { id: customer.id } }">
<div v-if="userCards.length > 0" class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Card</span><input type="radio" v-model="CreateOilOrderForm.basicInfo.payment_type" :value="1" class="radio radio-xs" /></label></div> <button class="btn btn-xs btn-outline btn-success">Add New</button>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Check</span><input type="radio" v-model="CreateOilOrderForm.basicInfo.payment_type" :value="3" class="radio radio-xs" /></label></div> </router-link>
<div class="form-control"><label class="label cursor-pointer gap-2"><span class="label-text">Other</span><input type="radio" v-model="CreateOilOrderForm.basicInfo.payment_type" :value="4" class="radio radio-xs" /></label></div>
</div> </div>
<!-- Customer Card Selection --> <div class="mt-2 text-sm" v-if="userCards.length === 0">
<div v-if="userCards.length > 0 && CreateOilOrderForm.basicInfo.payment_type === 1"> <p class="text-warning font-semibold">No cards on file.</p>
<label class="label"><span class="label-text">Select Card</span></label> </div>
<select v-model="CreateOilOrderForm.basicInfo.credit_card_id" class="select select-bordered select-sm w-full max-w-xs"> <div class="mt-4 space-y-3">
<option disabled :value="0">Select a card</option> <div v-for="card in userCards" :key="card.id" class="p-3 rounded-lg border" :class="card.main_card ? 'bg-primary/10 border-primary' : 'bg-base-200 border-base-300'">
<option v-for="card in userCards" :key="card.id" :value="card.id"> <div class="flex justify-between items-start">
{{ card.type_of_card }} {{ card.card_number }} <div>
</option> <div class="font-bold text-sm">{{ card.name_on_card }}</div>
</select> <div class="text-xs opacity-70">{{ card.type_of_card }}</div>
</div>
<div v-if="card.main_card" class="badge badge-primary badge-sm">Primary</div>
</div>
<div class="mt-2 text-sm font-mono tracking-wider">
<p>{{ card.card_number }}</p>
<p>Exp: <span v-if="card.expiration_month < 10">0</span>{{ card.expiration_month }} / {{ card.expiration_year }}</p>
<p>CVV: {{ card.security_number }}</p>
</div>
<div class="flex justify-end gap-2 mt-2">
<a @click.prevent="editCard(card.id)" class="link link-hover text-xs">Edit</a>
<a @click.prevent="removeCard(card.id)" class="link link-hover text-error text-xs">Remove</a>
</div>
</div>
</div> </div>
</div> </div>
<!-- Delivery Status & Driver --> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="label"><span class="label-text font-bold">Delivery Status</span></label>
<select v-model="CreateOilOrderForm.basicInfo.delivery_status" class="select select-bordered select-sm w-full">
<option v-for="status in deliveryStatus" :key="status.value" :value="status.value">
{{ status.text }}
</option>
</select>
</div>
<!-- <div>
<label class="label"><span class="label-text font-bold">Assigned Driver</span></label>
<select v-model="CreateOilOrderForm.basicInfo.driver_employee_id" class="select select-bordered select-sm w-full">
<option disabled :value="0">Select a driver</option>
<option v-for="driver in truckDriversList" :key="driver.id" :value="driver.id">
{{ driver.employee_first_name }} {{ driver.employee_last_name }}
</option>
</select>
</div> -->
</div>
<!-- Dispatcher Notes -->
<div>
<label class="label"><span class="label-text font-bold">Dispatcher Notes</span></label>
<textarea v-model="CreateOilOrderForm.basicInfo.dispatcher_notes_taken" rows="3" class="textarea textarea-bordered w-full"></textarea>
</div>
<!-- Submit Button -->
<div class="pt-2">
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
@@ -158,13 +248,14 @@ import Header from '../../layouts/headers/headerauth.vue'
import SideBar from '../../layouts/sidebar/sidebar.vue' import SideBar from '../../layouts/sidebar/sidebar.vue'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../layouts/footers/footer.vue'
import useValidate from "@vuelidate/core"; import useValidate from "@vuelidate/core";
import { required, minLength } from "@vuelidate/validators"; import { required, minLength, requiredIf } from "@vuelidate/validators";
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
// Interfaces to describe the shape of your data // Interfaces to describe the shape of your data
interface Customer { account_number: string; id: number; customer_first_name: string; customer_last_name: string; customer_address: string; customer_apt: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; } interface Customer { account_number: string; id: number; customer_first_name: string; customer_last_name: string; customer_address: string; customer_apt: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; }
interface DeliveryOrder { id: string; customer_id: number; payment_type: number; payment_card_id: number; gallons_ordered: number; customer_asked_for_fill: boolean | number; delivery_status: number; driver_employee_id: number; promo_id: number; expected_delivery_date: string; when_ordered: string; prime: boolean | number; emergency: boolean | number; same_day: boolean | number; dispatcher_notes: string; } interface DeliveryOrder { id: string; customer_id: number; payment_type: number; payment_card_id: number; gallons_ordered: number; customer_asked_for_fill: boolean | number; delivery_status: number; driver_employee_id: number; promo_id: number; expected_delivery_date: string; when_ordered: string; prime: boolean | number; emergency: boolean | number; same_day: boolean | number; dispatcher_notes: string; }
interface UserCard { id: number; type_of_card: string; card_number: string; name_on_card: string; expiration_month: string; expiration_year: string; last_four_digits: string; security_number: string; } interface UserCard { id: number; type_of_card: string; card_number: string; name_on_card: string; expiration_month: number; expiration_year: number; last_four_digits: string; security_number: string; main_card: boolean; }
interface PricingTier { gallons: number; price: string | number; }
const STATE_MAP: { [key: number]: string } = { 0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York' }; const STATE_MAP: { [key: number]: string } = { 0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York' };
@@ -174,10 +265,12 @@ export default defineComponent({
data() { data() {
return { return {
v$: useValidate(), v$: useValidate(),
quickGallonAmounts: [100, 125, 150, 175, 200, 220],
deliveryStatus: [] as any[], deliveryStatus: [] as any[],
truckDriversList: [] as any[], truckDriversList: [] as any[],
userCards: [] as UserCard[], userCards: [] as UserCard[],
promos: [] as any[], promos: [] as any[],
pricingTiers: [] as PricingTier[],
customer: {} as Customer, customer: {} as Customer,
deliveryOrder: {} as DeliveryOrder, deliveryOrder: {} as DeliveryOrder,
userCard: {} as UserCard, userCard: {} as UserCard,
@@ -197,6 +290,10 @@ export default defineComponent({
promo_id: 0, promo_id: 0,
payment_type: 0, payment_type: 0,
credit_card_id: 0, credit_card_id: 0,
credit: false,
cash: false,
check: false,
other: false,
}, },
}, },
} }
@@ -206,11 +303,31 @@ export default defineComponent({
return { return {
CreateOilOrderForm: { CreateOilOrderForm: {
basicInfo: { basicInfo: {
gallons_ordered: { required, minLength: minLength(1) }, gallons_ordered: {
// RESTORED: Add validation for expected date required: requiredIf(function(this: any) {
return !this.CreateOilOrderForm.basicInfo.customer_asked_for_fill;
}),
minValue: function(this: any, value: string) {
if (this.CreateOilOrderForm.basicInfo.customer_asked_for_fill) return true;
if (!value) return true; // if empty, required will catch it
const num = parseInt(value, 10);
return num >= 1;
}
},
expected_delivery_date: { required }, expected_delivery_date: { required },
credit_card_id: {
creditCardRequired: function(this: any, value: number) {
if (this.CreateOilOrderForm.basicInfo.credit) {
return value !== 0;
}
return true;
}
},
}, },
}, },
isAnyPaymentMethodSelected: {
mustBeTrue: (value: boolean) => value === true,
},
}; };
}, },
@@ -221,6 +338,25 @@ export default defineComponent({
} }
return ''; return '';
}, },
isPricingTierSelected() {
return (tierGallons: number | string): boolean => {
if (!this.CreateOilOrderForm.basicInfo.gallons_ordered) return false;
const selectedGallons = Number(this.CreateOilOrderForm.basicInfo.gallons_ordered);
if (isNaN(selectedGallons)) return false;
const tierNum = Number(tierGallons);
if (isNaN(tierNum)) return false;
return selectedGallons === tierNum;
};
},
isAnyPaymentMethodSelected(): boolean {
return !!(this.CreateOilOrderForm.basicInfo?.credit || this.CreateOilOrderForm.basicInfo?.cash || this.CreateOilOrderForm.basicInfo?.check || this.CreateOilOrderForm.basicInfo?.other);
},
selectedGallonsAmount(): number {
const value = this.CreateOilOrderForm.basicInfo.gallons_ordered ?? '';
return Number(value);
}
}, },
mounted() { mounted() {
@@ -233,6 +369,7 @@ export default defineComponent({
this.getPromos(); this.getPromos();
this.getDriversList(); this.getDriversList();
this.getDeliveryStatusList(); this.getDeliveryStatusList();
this.getPricingTiers();
this.getDeliveryOrder(deliveryId); this.getDeliveryOrder(deliveryId);
}, },
@@ -244,6 +381,7 @@ export default defineComponent({
this.deliveryOrder = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE this.deliveryOrder = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE
// RESTORED: Populate all form fields from the API response // RESTORED: Populate all form fields from the API response
const paymentType = this.deliveryOrder.payment_type;
this.CreateOilOrderForm.basicInfo = { this.CreateOilOrderForm.basicInfo = {
gallons_ordered: String(this.deliveryOrder.gallons_ordered), gallons_ordered: String(this.deliveryOrder.gallons_ordered),
customer_asked_for_fill: !!this.deliveryOrder.customer_asked_for_fill, customer_asked_for_fill: !!this.deliveryOrder.customer_asked_for_fill,
@@ -256,8 +394,13 @@ export default defineComponent({
driver_employee_id: this.deliveryOrder.driver_employee_id || 0, driver_employee_id: this.deliveryOrder.driver_employee_id || 0,
dispatcher_notes_taken: this.deliveryOrder.dispatcher_notes, dispatcher_notes_taken: this.deliveryOrder.dispatcher_notes,
promo_id: this.deliveryOrder.promo_id || 0, promo_id: this.deliveryOrder.promo_id || 0,
payment_type: this.deliveryOrder.payment_type, payment_type: paymentType,
credit_card_id: this.deliveryOrder.payment_card_id || 0, credit_card_id: this.deliveryOrder.payment_card_id || 0,
// Set the correct payment method checkbox based on payment_type
credit: paymentType === 1,
cash: paymentType === 0,
check: paymentType === 3,
other: paymentType === 4,
}; };
this.getCustomer(this.deliveryOrder.customer_id); this.getCustomer(this.deliveryOrder.customer_id);
@@ -304,23 +447,79 @@ export default defineComponent({
axios.get(`${import.meta.env.VITE_BASE_URL}/query/deliverystatus`, { withCredentials: true }) axios.get(`${import.meta.env.VITE_BASE_URL}/query/deliverystatus`, { withCredentials: true })
.then((response: any) => { this.deliveryStatus = response.data; }); .then((response: any) => { this.deliveryStatus = response.data; });
}, },
getPricingTiers() {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
.then((response: any) => {
const tiersObject = response.data;
this.pricingTiers = Object.entries(tiersObject).map(([gallons, price]) => ({
gallons: parseInt(gallons, 10),
price: price as string | number,
}));
})
.catch(() => {
notify({ title: "Pricing Error", text: "Could not retrieve today's pricing.", type: "error" });
});
},
editCard(card_id: number) {
this.$router.push({ name: "cardedit", params: { id: card_id } });
},
removeCard(card_id: number) {
if (window.confirm("Are you sure you want to remove this card?")) {
let path = `${import.meta.env.VITE_BASE_URL}/payment/card/remove/${card_id}`;
axios.delete(path, { headers: authHeader() })
.then(() => {
notify({ title: "Card Removed", type: "success" });
this.getPaymentCards(this.customer.id);
})
.catch(() => {
notify({ title: "Error", text: "Could not remove card.", type: "error" });
});
}
},
selectCreditCard(cardId: number) {
this.CreateOilOrderForm.basicInfo.credit_card_id = cardId;
},
setGallons(amount: number) {
this.CreateOilOrderForm.basicInfo.gallons_ordered = String(amount);
this.CreateOilOrderForm.basicInfo.customer_asked_for_fill = false;
},
setDeliveryDate(days: number) {
const date = new Date();
date.setDate(date.getDate() + days);
this.CreateOilOrderForm.basicInfo.expected_delivery_date = date.toISOString().split('T')[0];
},
isDeliveryDateSelected(days: number): boolean {
const date = new Date();
date.setDate(date.getDate() + days);
return this.CreateOilOrderForm.basicInfo.expected_delivery_date === date.toISOString().split('T')[0];
},
async onSubmit() {
const isFormValid = await this.v$.CreateOilOrderForm.$validate();
const isPaymentValid = await this.v$.isAnyPaymentMethodSelected.$validate();
onSubmit() { if (!isFormValid || !isPaymentValid) {
this.v$.$validate(); notify({ title: "Form Incomplete", text: "Please review the fields marked in red.", type: "warn" });
if (this.v$.$error) {
notify({ type: 'error', title: 'Validation Error', text: 'Please check the form fields.' });
return; return;
} }
const formInfo = this.CreateOilOrderForm.basicInfo; const formInfo = this.CreateOilOrderForm.basicInfo;
// Convert checkboxes back to payment_type number for API
let paymentType = 0; // Default to cash
if (formInfo.credit) paymentType = 1;
else if (formInfo.cash) paymentType = 0;
else if (formInfo.other) paymentType = 4;
else if (formInfo.check) paymentType = 3;
// The payload now automatically includes all the restored fields // The payload now automatically includes all the restored fields
const payload = { const payload = {
...formInfo, ...formInfo,
cash: formInfo.payment_type === 0, payment_type: paymentType,
credit: formInfo.payment_type === 1, cash: formInfo.cash,
check: formInfo.payment_type === 3, credit: formInfo.credit,
other: formInfo.payment_type === 4, check: formInfo.check,
credit_card_id: formInfo.payment_type === 1 ? formInfo.credit_card_id : null, other: formInfo.other,
credit_card_id: formInfo.credit ? formInfo.credit_card_id : null,
}; };
axios.post(`${import.meta.env.VITE_BASE_URL}/delivery/edit/${this.deliveryOrder.id}`, payload, { withCredentials: true, headers: authHeader() }) axios.post(`${import.meta.env.VITE_BASE_URL}/delivery/edit/${this.deliveryOrder.id}`, payload, { withCredentials: true, headers: authHeader() })

View File

@@ -20,13 +20,10 @@
<!-- Today's Stats Card --> <!-- Today's Stats Card -->
<div class="stats stats-vertical sm:stats-horizontal shadow bg-base-100 text-center text-sm"> <div class="stats stats-vertical sm:stats-horizontal shadow bg-base-100 text-center text-sm">
<div class="stat p-3"> <div class="stat p-3">
<div class="stat-title text-xs">Today's Deliveries</div> <div class="stat-title text-xs"> Deliveries</div>
<div class="stat-value text-lg">{{ delivery_count }}</div> <div class="stat-value text-lg">{{ delivery_count }}</div>
</div> </div>
<div class="stat p-3">
<div class="stat-title text-xs">Completed</div>
<div class="stat-value text-lg">{{ delivery_count_delivered }} / {{ delivery_count }}</div>
</div>
</div> </div>
</div> </div>

View File

@@ -538,26 +538,11 @@ async onSubmit() {
// If a valid, approved, pre-auth transaction is found... // If a valid, approved, pre-auth transaction is found...
if (transactionResponse.data && transactionResponse.data.transaction_type === 1 && transactionResponse.data.status === 0) { if (transactionResponse.data && transactionResponse.data.transaction_type === 1 && transactionResponse.data.status === 0) {
// ...redirect to the capture page. The delivery is already updated. // ...redirect to the capture page. The delivery is already updated.
const gallons = this.FinalizeOilOrderForm.gallons_delivered || '0';
const pricePerGallon = parseFloat(this.deliveryOrder.customer_price);
let finalAmount = parseFloat(gallons) * pricePerGallon;
if (this.deliveryOrder.prime == 1) {
finalAmount += parseFloat(this.pricing.price_prime.toString()) || 0;
}
if (this.deliveryOrder.same_day == 1) {
finalAmount += parseFloat(this.pricing.price_same_day.toString()) || 0;
}
this.$router.push({ this.$router.push({
name: 'captureAuthorize', name: 'captureAuthorize',
params: { id: this.$route.params.id }, params: { id: this.$route.params.id }
query: {
gallons: gallons,
amount: finalAmount.toFixed(2).toString()
}
}); });
} else { } else {

View File

@@ -130,8 +130,8 @@
</div> </div>
<!-- Pricing & Fees --> <!-- Pricing & Fees -->
<div class="p-4 border rounded-md space-y-2"> <div class="p-4 border rounded-md space-y-2">
<label class="label-text font-bold">Estimated Total</label> <label class="label-text font-bold">Estimated Total</label>
<!-- Finalized View --> <!-- Finalized View -->
<div v-if="deliveryOrder.promo_id !== null"> <div v-if="deliveryOrder.promo_id !== null">
<div>Before Discount: ${{ total_amount }}</div> <div>Before Discount: ${{ total_amount }}</div>
@@ -144,6 +144,35 @@
<div v-if="deliveryOrder.same_day == 1" class="text-sm text-gray-400">+ ${{ pricing.price_same_day }} Same Day Fee</div> <div v-if="deliveryOrder.same_day == 1" class="text-sm text-gray-400">+ ${{ pricing.price_same_day }} Same Day Fee</div>
</div> </div>
<!-- Transaction Summary -->
<div v-if="transaction && transaction.auth_net_transaction_id" class="p-4 border rounded-md">
<label class="label-text font-bold">Transaction Summary</label>
<div class="mt-2 space-y-2">
<div class="flex justify-between">
<span>Transaction ID:</span>
<span class="font-mono">{{ transaction.auth_net_transaction_id }}</span>
</div>
<div class="flex justify-between">
<span>Pre-Auth Amount:</span>
<span>${{ transaction.pre_auth_amount || '0.00' }}</span>
</div>
<div class="flex justify-between">
<span>Charge Amount:</span>
<span>${{ transaction.charge_amount || '0.00' }}</span>
</div>
<div class="flex justify-between">
<span>Date:</span>
<span>{{ format_date(transaction.transaction_date) }}</span>
</div>
<div class="flex justify-between">
<span>Status:</span>
<span :class="transaction.status === 0 ? 'text-success' : 'text-error'">
{{ transaction.status === 0 ? 'Approved' : 'Declined' }}
</span>
</div>
</div>
</div>
<!-- Delivery Summary --> <!-- Delivery Summary -->
<div v-if="deliveryOrder.gallons_delivered && parseFloat(deliveryOrder.gallons_delivered) > 0" class="p-4 border rounded-md"> <div v-if="deliveryOrder.gallons_delivered && parseFloat(deliveryOrder.gallons_delivered) > 0" class="p-4 border rounded-md">
<label class="label-text font-bold">Delivery Summary FINAL</label> <label class="label-text font-bold">Delivery Summary FINAL</label>
@@ -193,50 +222,52 @@
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<!-- <!--
START: Replaced Payment Section START: Replaced Payment Section
--> -->
<div class="p-4 border rounded-md"> <div class="p-4 border rounded-md">
<label class="label-text font-bold">Payment Method</label> <label class="label-text font-bold">Payment Method</label>
<div class="mt-1"> <div class="mt-1">
<div class="text-lg"> <div class="text-lg">
<span v-if="deliveryOrder.payment_type == 0">Cash</span> <span v-if="deliveryOrder.payment_type == 0">Cash</span>
<span v-else-if="deliveryOrder.payment_type == 1">Credit Card</span> <span v-else-if="deliveryOrder.payment_type == 1">Credit Card</span>
<span v-else-if="deliveryOrder.payment_type == 2">Credit Card & Cash</span> <span v-else-if="deliveryOrder.payment_type == 2">Credit Card & Cash</span>
<span v-else-if="deliveryOrder.payment_type == 3">Check</span> <span v-else-if="deliveryOrder.payment_type == 3">Check</span>
<span v-else-if="deliveryOrder.payment_type == 4">Other</span> <span v-else-if="deliveryOrder.payment_type == 4">Other</span>
<span v-else>Not Specified</span> <span v-else>Not Specified</span>
</div> </div>
<!-- <!--
This is the new, styled card display. This is the new, styled card display.
It uses the same logic but applies the better CSS classes. It uses the same logic but applies the better CSS classes.
--> -->
<div v-if="userCardfound && [1, 2, 3].includes(deliveryOrder.payment_type)" <div v-if="userCardfound && [1, 2, 3].includes(deliveryOrder.payment_type)"
class="p-4 rounded-lg border mt-2" class="p-4 rounded-lg border mt-2"
:class="userCard.main_card ? 'bg-primary/10 border-primary' : 'bg-base-200 border-base-300'"> :class="userCard.main_card ? 'bg-primary/10 border-primary' : 'bg-base-200 border-base-300'">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div> <div>
<div class="font-bold">{{ userCard.name_on_card }}</div> <div class="font-bold">{{ userCard.name_on_card }}</div>
<div class="text-xs opacity-70">{{ userCard.type_of_card }}</div> <div class="text-xs opacity-70">{{ userCard.type_of_card }}</div>
</div>
<div v-if="userCard.main_card" class="badge badge-primary badge-sm">Primary</div>
</div>
<div class="mt-3 text-sm font-mono tracking-wider">
<!-- Using last_four_digits is more secure and looks cleaner -->
<p>{{ userCard.card_number }}</p>
<p>
Exp:
<!-- Adds a leading zero for single-digit months -->
<span v-if="Number(userCard.expiration_month) < 10">0</span>{{ userCard.expiration_month }} / {{ userCard.expiration_year }}
</p>
<p>CVV: {{ userCard.security_number }}</p>
</div>
</div>
</div>
</div> </div>
<div v-if="userCard.main_card" class="badge badge-primary badge-sm">Primary</div>
</div>
<div class="mt-3 text-sm font-mono tracking-wider">
<!-- Using last_four_digits is more secure and looks cleaner -->
<p>{{ userCard.card_number }}</p>
<p>
Exp:
<!-- Adds a leading zero for single-digit months -->
<span v-if="Number(userCard.expiration_month) < 10">0</span>{{ userCard.expiration_month }} / {{ userCard.expiration_year }}
</p>
<p>CVV: {{ userCard.security_number }}</p>
</div>
</div>
</div>
</div>
<!-- Notes & Options --> <!-- Notes & Options -->
<div class="p-4 border rounded-md"> <div class="p-4 border rounded-md">
@@ -379,6 +410,7 @@ export default defineComponent({
driver_last_name: '', driver_last_name: '',
promo_id: 0, promo_id: 0,
}, },
transaction: null as any,
} }
}, },
@@ -396,7 +428,8 @@ export default defineComponent({
this.getOilOrder(this.$route.params.id); this.getOilOrder(this.$route.params.id);
this.getOilOrderMoney(this.$route.params.id); this.getOilOrderMoney(this.$route.params.id);
this.sumdelivery(this.$route.params.id); this.sumdelivery(this.$route.params.id);
this.getOilPricing() this.getOilPricing();
this.getTransaction(this.$route.params.id);
}, },
methods: { methods: {
@@ -647,9 +680,20 @@ getOilOrder(delivery_id: any) {
return total.toFixed(2); return total.toFixed(2);
}, },
getTransaction(delivery_id: any) {
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/transaction/delivery/${delivery_id}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
console.log("Transaction API response:", response.data); // Debug log to see actual response structure
this.transaction = response.data;
})
.catch((error: any) => {
console.error("Error fetching transaction:", error);
this.transaction = null;
});
},
}, },
}) })
</script> </script>

View File

@@ -0,0 +1,518 @@
<!-- src/pages/pay/oil/authorize_preauthcharge.vue -->
<template>
<div class="flex">
<!-- Main Content -->
<div class="flex-1 px-8 py-6">
<!-- Breadcrumbs & Header -->
<div class="text-sm breadcrumbs mb-6">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'payOil', params: { id: deliveryId } }">Payment Confirmation</router-link></li>
<li>Payment Authorization</li>
</ul>
</div>
<!-- 2x2 Grid Layout -->
<div class="max-w-6xl">
<h1 class="text-3xl font-bold mb-8">Payment Authorization Authorize.net</h1>
<!-- Top Row: Charge Breakdown and Payment Method -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<!-- Charge Breakdown -->
<div class="bg-base-100 rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Charge Breakdown</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>Gallons Ordered:</span>
<span>{{ delivery.gallons_ordered || 0 }} gallons</span>
</div>
<div class="flex justify-between">
<span>Price per Gallon:</span>
<span>${{ delivery.customer_price || 0 }}</span>
</div>
<div class="flex justify-between font-semibold">
<span>Subtotal:</span>
<span>${{ calculateSubtotal() }}</span>
</div>
<div v-if="delivery.prime == 1" class="flex justify-between text-sm">
<span>Prime Fee:</span>
<span>+ ${{ pricing.price_prime || 0 }}</span>
</div>
<div v-if="delivery.same_day == 1" class="flex justify-between text-sm">
<span>Same Day Fee:</span>
<span>+ ${{ pricing.price_same_day || 0 }}</span>
</div>
<div v-if="delivery.emergency == 1" class="flex justify-between text-sm">
<span>Emergency Fee:</span>
<span>+ ${{ pricing.price_emergency || 0 }}</span>
</div>
<div v-if="promo_active" class="flex justify-between text-success">
<span>{{ promo.name_of_promotion }}:</span>
<span>- ${{ discount }}</span>
</div>
<hr class="my-3">
<div class="flex justify-between font-bold text-lg">
<span>Total:</span>
<span>${{ promo_active ? total_amount_after_discount : total_amount }}</span>
</div>
</div>
</div>
<!-- Credit Card Display -->
<div class="bg-base-100 rounded-lg p-6">
<h3 class="text-lg font-semibold mb-4">Payment Method</h3>
<div v-if="selectedCard" class="bg-base-200 p-4 rounded-md">
<div class="flex justify-between items-center mb-2">
<div class="font-semibold">{{ selectedCard.type_of_card }}</div>
<div v-if="selectedCard.main_card" class="badge badge-primary">Primary</div>
</div>
<div class="font-mono text-sm">
<div> {{ selectedCard.card_number }}</div>
<div>{{ selectedCard.name_on_card }}</div>
<div>Expires: {{ selectedCard.expiration_month }}/{{ selectedCard.expiration_year }}</div>
</div>
</div>
<div v-else class="text-gray-500 p-4">
No payment method selected
</div>
</div>
</div>
<!-- Error/Success Messages -->
<div class="mb-6">
<div v-if="error" class="alert alert-error">
<span>{{ error }}</span>
</div>
<div v-if="success" class="alert alert-success">
<span>{{ success }}</span>
</div>
</div>
<!-- Bottom Section: Charge Amount Input and Action Buttons -->
<div class="bg-base-100 rounded-lg p-6">
<div class="flex flex-col lg:flex-row lg:items-center gap-6">
<!-- Charge Amount Input - Compact -->
<div class="flex-1">
<h3 class="text-lg font-semibold mb-2">Charge Amount</h3>
<div class="flex">
<span class="inline-flex items-center px-3 text-sm bg-base-200 rounded-l-lg border border-r-0">$</span>
<input
v-model="chargeAmount"
type="number"
step="0.01"
class="input input-bordered flex-1 rounded-l-none"
placeholder="Enter amount"
:disabled="loading"
/>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3 align-bottom">
<router-link :to="{ name: 'payOil', params: { id: deliveryId } }">
<button class="btn btn-ghost">Cancel</button>
</router-link>
<button
@click="handlePreauthorize"
class="btn btn-warning"
:disabled="loading || !chargeAmount"
>
<span v-if="loading && action === 'preauthorize'" class="loading loading-spinner loading-sm"></span>
Preauthorize
</button>
<button
@click="handleChargeNow"
class="btn btn-primary"
:disabled="loading || !chargeAmount"
>
<span v-if="loading && action === 'charge'" class="loading loading-spinner loading-sm"></span>
Charge Now
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue'
import axios from 'axios'
import authHeader from '../../../services/auth.header'
import { notify } from "@kyvg/vue3-notification"
export default defineComponent({
name: 'AuthorizePreauthCharge',
data() {
return {
deliveryId: this.$route.params.id as string,
loaded: false,
chargeAmount: 0,
loading: false,
action: '', // 'preauthorize' or 'charge'
error: '',
success: '',
user: {
user_id: 0,
},
delivery: {
id: 0,
customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
gallons_ordered: 0,
customer_asked_for_fill: 0,
gallons_delivered: 0,
customer_filled: 0,
delivery_status: 0,
when_ordered: '',
when_delivered: '',
expected_delivery_date: '',
automatic: 0,
oil_id: 0,
supplier_price: 0,
customer_price: 0,
customer_temperature: 0,
dispatcher_notes: '',
prime: 0,
promo_id: 0,
emergency: 0,
same_day: 0,
payment_type: 0,
payment_card_id: 0,
driver_employee_id: 0,
driver_first_name: '',
driver_last_name: '',
pre_charge_amount: 0,
total_price: 0,
service_id: null,
},
credit_cards: [
{
id: 0,
name_on_card: '',
main_card: false,
card_number: '',
expiration_month: '',
type_of_card: '',
last_four_digits: '',
expiration_year: '',
security_number: '',
}
],
customer: {
id: 0,
user_id: 0,
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_address: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
},
pricing: {
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
date: "",
},
promo_active: false,
promo: {
name_of_promotion: '',
description: '',
money_off_delivery: 0,
text_on_ticket: ''
},
total_amount: 0,
discount: 0,
total_amount_after_discount: 0,
}
},
computed: {
selectedCard(): any {
return this.credit_cards.find((card: any) => card.id === this.delivery.payment_card_id)
}
},
mounted() {
this.loadData(this.deliveryId)
},
created() {
this.watchRoute()
},
methods: {
watchRoute() {
watch(
() => this.$route.params.id,
(newId) => {
if (newId !== this.deliveryId) {
this.resetState()
this.deliveryId = newId as string
this.loadData(newId as string)
}
}
)
},
resetState() {
this.loading = false
this.action = ''
this.error = ''
this.success = ''
this.chargeAmount = 0
this.promo_active = false
this.total_amount = 0
this.discount = 0
this.total_amount_after_discount = 0
this.deliveryId = this.$route.params.id as string
},
loadData(deliveryId: string) {
this.userStatus()
this.getOilOrder(deliveryId)
this.sumdelivery(deliveryId)
this.getOilPricing()
this.updatestatus()
},
updatestatus() {
let path = import.meta.env.VITE_BASE_URL + '/delivery/updatestatus';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.update)
console.log("Updated Status of Deliveries")
})
},
sumdelivery(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data.ok) {
this.total_amount = response.data.total_amount;
this.discount = response.data.discount;
this.total_amount_after_discount = response.data.total_amount_after_discount;
// Auto-populate charge amount with the calculated total
if (this.promo_active) {
this.chargeAmount = this.total_amount_after_discount;
} else {
this.chargeAmount = this.total_amount;
}
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
},
getPromo(promo_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/promo/" + promo_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
this.promo = response.data
this.promo_active = true
// Update charge amount when promo is applied
if (this.total_amount_after_discount > 0) {
this.chargeAmount = this.total_amount_after_discount
}
}
})
},
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
},
getOilPricing() {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/table";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.pricing = response.data;
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
},
getOilOrder(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/delivery/order/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data && response.data.ok) {
this.delivery = response.data.delivery;
this.getCustomer(this.delivery.customer_id)
this.getCreditCards(this.delivery.customer_id)
if (this.delivery.promo_id != null) {
this.getPromo(this.delivery.promo_id);
this.promo_active = true;
}
} else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
}
})
.catch((error: any) => {
console.error("API Error in getOilOrder:", error);
notify({
title: "Error",
text: "Could not get delivery",
type: "error",
});
});
},
getCreditCards(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + user_id;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.credit_cards = response.data
})
},
getCustomer(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer = response.data
})
},
calculateSubtotal() {
const gallons = this.delivery.gallons_ordered || 0
const pricePerGallon = this.delivery.customer_price || 0
return (gallons * pricePerGallon).toFixed(2)
},
async handlePreauthorize() {
await this.processPayment('preauthorize')
},
async handleChargeNow() {
await this.processPayment('charge')
},
async processPayment(actionType: string) {
if (!this.selectedCard) {
this.error = 'No credit card found for this customer'
return
}
this.loading = true
this.action = actionType
this.error = ''
this.success = ''
try {
const endpoint = actionType === 'preauthorize' ? 'authorize' : 'charge'
const payload = {
card_number: (this.selectedCard as any).card_number,
expiration_date: `${(this.selectedCard as any).expiration_month}${(this.selectedCard as any).expiration_year}`,
cvv: (this.selectedCard as any).security_number,
[actionType === 'preauthorize' ? 'preauthorize_amount' : 'charge_amount']: this.chargeAmount.toString(),
transaction_type: actionType === 'preauthorize' ? 1 : 0,
service_id: this.delivery.service_id || null,
delivery_id: this.delivery.id,
card_id: (this.selectedCard as any).id
}
console.log('=== DEBUG: Payment payload ===')
console.log('Delivery ID:', this.delivery.id)
console.log('Final payload being sent:', payload)
const response = await axios.post(
`${import.meta.env.VITE_AUTHORIZE_URL}/api/${endpoint}/?customer_id=${this.customer.id}`,
payload,
{ withCredentials: true }
)
if (response.data && response.data.status === 0) {
this.success = `${actionType === 'preauthorize' ? 'Preauthorization' : 'Charge'} successful! Transaction ID: ${response.data.auth_net_transaction_id || 'N/A'}`
setTimeout(() => {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
}, 2000)
} else {
throw new Error(`Payment ${actionType} failed: ${response.data?.status || 'Unknown error'}`)
}
} catch (error: any) {
this.error = error.response?.data?.detail || `Failed to ${actionType} payment`
notify({
title: "Error",
text: this.error,
type: "error",
})
} finally {
this.loading = false
this.action = ''
}
}
},
})
</script>
<style scoped></style>

View File

@@ -65,72 +65,6 @@
</div> </div>
</div> </div>
<!-- Delivery Details Card -->
<div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Delivery Details</h3>
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div>
<div class="font-bold">Status</div>
<div class="opacity-80">
<span v-if="deliveryOrder.delivery_status == 0">waiting</span>
<span v-else-if="deliveryOrder.delivery_status == 1">delivered</span>
<span v-else-if="deliveryOrder.delivery_status == 2">Today</span>
<span v-else-if="deliveryOrder.delivery_status == 3">Tomorrow</span>
<span v-else-if="deliveryOrder.delivery_status == 4">Partial Delivery</span>
<span v-else-if="deliveryOrder.delivery_status == 5">misdelivery</span>
<span v-else-if="deliveryOrder.delivery_status == 6">unknown</span>
<span v-else-if="deliveryOrder.delivery_status == 10">Finalized</span>
</div>
</div>
<div>
<div class="font-bold">Scheduled Date</div>
<div class="opacity-80">{{ deliveryOrder.expected_delivery_date }}</div>
</div>
<div>
<div class="font-bold">When Ordered</div>
<div class="opacity-80">{{ deliveryOrder.when_ordered }}</div>
</div>
<div>
<div class="font-bold">When Delivered</div>
<div class="opacity-80">{{ deliveryOrder.when_delivered }}</div>
</div>
<div>
<div class="font-bold">Driver</div>
<div class="opacity-80">{{ deliveryOrder.driver_first_name }} {{ deliveryOrder.driver_last_name }}</div>
</div>
</div>
</div>
<!-- Credit Card Details -->
<div v-if="userCardfound" class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Payment Method</h3>
<div class="bg-base-100 p-4 rounded-md">
<div class="flex justify-between items-center mb-2">
<div class="font-semibold">{{ userCard.type_of_card }}</div>
<div v-if="userCard.main_card" class="badge badge-primary">Primary</div>
</div>
<div class="font-mono text-sm">
<div>**** **** **** {{ userCard.last_four_digits }}</div>
<div>{{ userCard.name_on_card }}</div>
<div>Expires: {{ userCard.expiration_month }}/{{ userCard.expiration_year }}</div>
</div>
</div>
</div>
</div>
<!-- RIGHT COLUMN: Capture Form -->
<div class="space-y-6">
<!-- Gallons Delivered Display -->
<div class="bg-base-100 rounded-lg p-5">
<h2 class="text-2xl font-bold mb-4">Gallons Delivered</h2>
<div class="text-center">
<div class="text-4xl font-bold text-primary">{{ gallonsDelivered || '0.00' }}</div>
<div class="text-sm opacity-70">gallons</div>
</div>
</div>
<!-- Financial Summary Card --> <!-- Financial Summary Card -->
<div class="bg-neutral rounded-lg p-5"> <div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Financial Summary</h3> <h3 class="text-xl font-bold mb-4">Financial Summary</h3>
@@ -165,6 +99,71 @@
</div> </div>
</div> </div>
<!-- Delivery Details Card -->
<div class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Delivery Details</h3>
<div class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
<div>
<div class="font-bold">Status</div>
<div class="opacity-80">
<span v-if="deliveryOrder.delivery_status == 0">waiting</span>
<span v-else-if="deliveryOrder.delivery_status == 1">delivered</span>
<span v-else-if="deliveryOrder.delivery_status == 2">Today</span>
<span v-else-if="deliveryOrder.delivery_status == 3">Tomorrow</span>
<span v-else-if="deliveryOrder.delivery_status == 4">Partial Delivery</span>
<span v-else-if="deliveryOrder.delivery_status == 5">misdelivery</span>
<span v-else-if="deliveryOrder.delivery_status == 6">unknown</span>
<span v-else-if="deliveryOrder.delivery_status == 10">Finalized</span>
</div>
</div>
<div>
<div class="font-bold">Scheduled Date</div>
<div class="opacity-80">{{ deliveryOrder.expected_delivery_date }}</div>
</div>
<div>
<div class="font-bold">When Ordered</div>
<div class="opacity-80">{{ deliveryOrder.when_ordered }}</div>
</div>
<div>
<div class="font-bold">When Delivered</div>
<div class="opacity-80">{{ deliveryOrder.when_delivered }}</div>
</div>
</div>
</div>
<!-- Credit Card Details -->
<div v-if="userCardfound" class="bg-neutral rounded-lg p-5">
<h3 class="text-xl font-bold mb-4">Payment Method</h3>
<div class="p-2 rounded-lg border" :class="userCard.main_card ? 'bg-primary/10 border-primary' : 'bg-base-200 border-base-300'">
<div class="flex justify-between items-start">
<div>
<div class="font-bold text-sm">{{ userCard.name_on_card }}</div>
<div class="text-xs opacity-70">{{ userCard.type_of_card }}</div>
</div>
<div v-if="userCard.main_card" class="badge badge-primary badge-sm">Primary</div>
</div>
<div class="mt-1 text-sm font-mono tracking-wider">
<p>**** **** **** {{ userCard.last_four_digits }}</p>
<p>Exp: <span v-if="Number(userCard.expiration_month) < 10">0</span>{{ userCard.expiration_month }} / {{ userCard.expiration_year }}</p>
</div>
</div>
</div>
</div>
<!-- RIGHT COLUMN: Capture Form -->
<div class="space-y-6">
<!-- Gallons Delivered Display -->
<div class="bg-base-100 rounded-lg p-5">
<h2 class="text-2xl font-bold mb-4">Gallons Delivered</h2>
<div class="text-center">
<div class="text-4xl font-bold text-primary">{{ gallonsDelivered || '0.00' }}</div>
<div class="text-sm opacity-70">gallons</div>
</div>
</div>
<!-- Capture Payment Form --> <!-- Capture Payment Form -->
<div class="bg-base-100 rounded-lg p-5"> <div class="bg-base-100 rounded-lg p-5">
<h2 class="text-2xl font-bold mb-4">Capture Payment</h2> <h2 class="text-2xl font-bold mb-4">Capture Payment</h2>
@@ -173,24 +172,25 @@
<label class="label"> <label class="label">
<span class="label-text font-bold">Capture Amount</span> <span class="label-text font-bold">Capture Amount</span>
</label> </label>
<div v-if="captureAmount > preAuthAmount" class="text-sm text-error mb-2">
Cannot capture more than the preauthorization amount.
</div>
<input <input
v-model="captureAmount" v-model="captureAmount"
class="input input-bordered input-lg w-full" class="input input-bordered input-sm w-full"
type="number" type="number"
step="0.01" step="0.01"
placeholder="0.00" placeholder="0.00"
/> />
</div> </div>
<div class="alert alert-info">
<span>Pre-authorized transaction found. Ready to capture payment.</span>
</div>
<div class="flex gap-4"> <div class="flex gap-4">
<button <button
@click="capturePayment" @click="capturePayment"
class="btn btn-primary flex-1" :class="`btn flex-1 ${captureAmount > 0 ? 'btn-success' : 'btn-error'}`"
:disabled="loading || !captureAmount" :disabled="loading || !captureAmount || captureAmount > preAuthAmount"
> >
<span v-if="loading" class="loading loading-spinner loading-sm"></span> <span v-if="loading" class="loading loading-spinner loading-sm"></span>
Capture Payment Capture Payment
@@ -219,10 +219,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../../services/auth.header'
import Header from '../../layouts/headers/headerauth.vue' import Header from '../../../layouts/headers/headerauth.vue'
import SideBar from '../../layouts/sidebar/sidebar.vue' import SideBar from '../../../layouts/sidebar/sidebar.vue'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../../layouts/footers/footer.vue'
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
export default defineComponent({ export default defineComponent({
@@ -240,6 +240,7 @@ export default defineComponent({
userCardfound: false, userCardfound: false,
gallonsDelivered: '', gallonsDelivered: '',
captureAmount: 0, captureAmount: 0,
preAuthAmount: 0,
transaction: null as any, transaction: null as any,
userCard: { userCard: {
@@ -311,10 +312,6 @@ export default defineComponent({
}, },
mounted() { mounted() {
this.gallonsDelivered = this.$route.query.gallons as string || '';
// Use the amount from the query params, passed from the finalize page
this.captureAmount = parseFloat(this.$route.query.amount as string) || 0;
this.getOilOrder(this.$route.params.id) this.getOilOrder(this.$route.params.id)
this.getOilPricing() this.getOilPricing()
this.getTransaction() this.getTransaction()
@@ -327,11 +324,15 @@ export default defineComponent({
.then((response: any) => { .then((response: any) => {
if (response.data && response.data.ok) { if (response.data && response.data.ok) {
this.deliveryOrder = response.data.delivery; this.deliveryOrder = response.data.delivery;
this.gallonsDelivered = this.deliveryOrder.gallons_delivered;
this.getCustomer(this.deliveryOrder.customer_id); this.getCustomer(this.deliveryOrder.customer_id);
if ([1, 2, 3].includes(this.deliveryOrder.payment_type)) { if ([1, 2, 3].includes(this.deliveryOrder.payment_type)) {
this.getPaymentCard(this.deliveryOrder.payment_card_id); this.getPaymentCard(this.deliveryOrder.payment_card_id);
} }
// Calculate capture amount if pricing is already loaded
this.calculateCaptureAmount();
} else { } else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data."); console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
} }
@@ -371,6 +372,8 @@ export default defineComponent({
axios.get(path, { withCredentials: true }) axios.get(path, { withCredentials: true })
.then((response: any) => { .then((response: any) => {
this.pricing = response.data; this.pricing = response.data;
// Calculate capture amount if delivery order is already loaded
this.calculateCaptureAmount();
}) })
.catch((error: any) => { .catch((error: any) => {
notify({ title: "Error", text: "Could not get oil pricing", type: "error" }); notify({ title: "Error", text: "Could not get oil pricing", type: "error" });
@@ -383,6 +386,10 @@ export default defineComponent({
axios.get(path, { withCredentials: true, headers: authHeader() }) axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => { .then((response: any) => {
this.transaction = response.data; this.transaction = response.data;
this.preAuthAmount = parseFloat(response.data.preauthorize_amount || 0);
if (response.data.status !== 0) { // Not approved
this.preAuthAmount = 0;
}
}) })
.catch((error: any) => { .catch((error: any) => {
notify({ title: "Error", text: "No pre-authorized transaction found", type: "error" }); notify({ title: "Error", text: "No pre-authorized transaction found", type: "error" });
@@ -452,6 +459,29 @@ export default defineComponent({
} }
}, },
calculateCaptureAmount() {
// Only calculate if we have both delivery order and pricing data
if (this.deliveryOrder.id && this.pricing.price_for_customer) {
const gallons = typeof this.gallonsDelivered === 'string' ? parseFloat(this.gallonsDelivered) : Number(this.gallonsDelivered) || 0;
const pricePerGallon = typeof this.deliveryOrder.customer_price === 'string' ? parseFloat(this.deliveryOrder.customer_price) : Number(this.deliveryOrder.customer_price) || 0;
let total = gallons * pricePerGallon;
// Add prime fee if applicable
if (this.deliveryOrder.prime == 1) {
const primeFee = typeof this.pricing.price_prime === 'string' ? parseFloat(this.pricing.price_prime) : Number(this.pricing.price_prime) || 0;
total += primeFee;
}
// Add same day fee if applicable
if (this.deliveryOrder.same_day === 1) {
const sameDayFee = typeof this.pricing.price_same_day === 'string' ? parseFloat(this.pricing.price_same_day) : Number(this.pricing.price_same_day) || 0;
total += sameDayFee;
}
this.captureAmount = total;
}
},
cancelCapture() { cancelCapture() {
this.$router.push({ name: 'finalizeTicket', params: { id: this.$route.params.id } }); this.$router.push({ name: 'finalizeTicket', params: { id: this.$route.params.id } });
}, },
@@ -459,4 +489,4 @@ export default defineComponent({
}) })
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -153,12 +153,12 @@
<div class="bg-neutral rounded-lg p-5"> <div class="bg-neutral rounded-lg p-5">
<div class="flex flex-wrap gap-4 justify-between items-center"> <div class="flex flex-wrap gap-4 justify-between items-center">
<!-- Pay Authorize Button --> <!-- Pay Authorize Button -->
<button class="btn btn-warning" @click="showPaymentPopup = true"> <button class="btn btn-warning" @click="$router.push({ name: 'authorizePreauthCharge', params: { id: $route.params.id } })">
Pay Authorize Pay Authorize.net
</button> </button>
<!-- A single confirm button is cleaner --> <!-- A single confirm button is cleaner -->
<button class="btn btn-primary" @click="checkoutOilUpdatePayment(delivery.payment_type)"> <button class="btn btn-primary" @click="checkoutOilUpdatePayment(delivery.payment_type)">
Confirm & Process Payment Pay Tiger
</button> </button>
<router-link v-if="delivery && delivery.id" :to="{ name: 'deliveryEdit', params: { id: delivery.id } }"> <router-link v-if="delivery && delivery.id" :to="{ name: 'deliveryEdit', params: { id: delivery.id } }">
<button class="btn btn-sm btn-ghost">Edit Delivery</button> <button class="btn btn-sm btn-ghost">Edit Delivery</button>
@@ -171,33 +171,17 @@
</div> </div>
</div> </div>
<!-- Payment Authorization Popup -->
<PaymentAuthorizePopup
:show="showPaymentPopup"
:delivery="delivery"
:customer="customer"
:credit-cards="credit_cards"
:pricing="pricing"
:promo-active="promo_active"
:promo="promo"
:total-amount="total_amount"
:total-amount-after-discount="total_amount_after_discount"
:discount="discount"
@close="showPaymentPopup = false"
@payment-success="handlePaymentSuccess"
/>
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../../services/auth.header'
import Header from '../../layouts/headers/headerauth.vue' import Header from '../../../layouts/headers/headerauth.vue'
import SideBar from '../../layouts/sidebar/sidebar.vue' import SideBar from '../../../layouts/sidebar/sidebar.vue'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../../layouts/footers/footer.vue'
import PaymentAuthorizePopup from '../../components/PaymentAuthorizePopup.vue'
import useValidate from "@vuelidate/core"; import useValidate from "@vuelidate/core";
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import { minLength, required } from "@vuelidate/validators"; import { minLength, required } from "@vuelidate/validators";
@@ -209,7 +193,6 @@ export default defineComponent({
Header, Header,
SideBar, SideBar,
Footer, Footer,
PaymentAuthorizePopup,
}, },
data() { data() {
@@ -306,7 +289,6 @@ export default defineComponent({
total_amount: 0, total_amount: 0,
discount: 0, discount: 0,
total_amount_after_discount: 0, total_amount_after_discount: 0,
showPaymentPopup: false,
} }
}, },
validations() { validations() {
@@ -530,23 +512,7 @@ export default defineComponent({
}); });
}, },
handlePaymentSuccess(data: any) {
// Handle successful payment
console.log('Payment successful:', data)
// Show success notification
notify({
title: "Success",
text: "Payment authorized successfully",
type: "success",
});
// Close the popup
this.showPaymentPopup = false
// Redirect to customer profile
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
},
}, },
}) })

View File

@@ -1,7 +1,9 @@
import PayOil from './pay_oil.vue'; import PayOil from './oil/pay_oil.vue';
import CaptureAuthorize from './capture_authorize.vue'; import AuthorizePreauthCharge from './oil/authorize_preauthcharge.vue';
import CaptureAuthorize from './oil/capture_authorize.vue';
import ChargeServiceAuthorize from './service/capture_authorize.vue';
const payRoutes = [ const payRoutes = [
@@ -10,11 +12,21 @@ const payRoutes = [
name: 'payOil', name: 'payOil',
component: PayOil, component: PayOil,
}, },
{
path: '/pay/oil/authorize/:id',
name: 'authorizePreauthCharge',
component: AuthorizePreauthCharge,
},
{ {
path: '/pay/capture/authorize/:id', path: '/pay/capture/authorize/:id',
name: 'captureAuthorize', name: 'captureAuthorize',
component: CaptureAuthorize, component: CaptureAuthorize,
}, },
{
path: '/pay/service/capture/authorize/:id',
name: 'chargeServiceAuthorize',
component: ChargeServiceAuthorize,
},
] ]

View File

@@ -0,0 +1,344 @@
<template>
<div class="flex">
<!-- Main container with responsive horizontal padding -->
<div class="w-full px-4 py-4 md:px-6">
<div v-if="service" class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'customer' }">Customers</router-link></li>
<li>
<router-link :to="{ name: 'customerProfile', params: { id: service.customer_id } }">
{{ service.customer_name }}
</router-link>
</li>
<li>Charge Service #{{ service.id }}</li>
</ul>
</div>
<!-- Page Header -->
<div class="flex flex-wrap items-center justify-between gap-2 mt-4">
<h1 class="text-3xl font-bold">
Charge Service #{{ service?.id }}
</h1>
<router-link v-if="service" :to="{ name: 'customerProfile', params: { id: service.customer_id } }">
<button class="btn btn-sm btn-secondary">Back to Customer</button>
</router-link>
</div>
<!-- Main Content Grid -->
<div v-if="service" class="grid grid-cols-1 gap-6 mt-6 lg:grid-cols-2">
<!-- LEFT COLUMN: Customer and Service Details -->
<div class="space-y-6">
<!-- Customer Info Card -->
<div class="p-5 rounded-lg bg-neutral">
<div class="mb-2 text-xl font-bold">Customer</div>
<div>
<div class="font-bold">{{ service.customer_name }}</div>
<div>{{ service.customer_address }}</div>
<div v-if="service.customer_apt && service.customer_apt !== 'None'">{{ service.customer_apt }}</div>
<div>
{{ service.customer_town }}, {{ stateName }} {{ service.customer_zip }}
</div>
</div>
</div>
<!-- Service Details Card -->
<div class="p-5 rounded-lg bg-neutral">
<h3 class="mb-4 text-xl font-bold">Service Details</h3>
<div class="grid grid-cols-1 gap-y-3 text-sm">
<div>
<div class="font-bold">Service Type</div>
<div class="opacity-80">{{ serviceTypeName }}</div>
</div>
<div>
<div class="font-bold">Scheduled Date</div>
<div class="opacity-80">{{ service.scheduled_date }}</div>
</div>
<div v-if="service.description">
<div class="font-bold">Description</div>
<div class="opacity-80">{{ service.description }}</div>
</div>
</div>
</div>
</div>
<!-- RIGHT COLUMN: Cards and Payment Form -->
<div class="space-y-6">
<!-- Credit Cards Display -->
<div class="p-5 rounded-lg bg-neutral">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold">Credit Cards</h3>
<router-link :to="{ name: 'cardadd', params: { id: service.customer_id } }">
<button class="btn btn-xs btn-outline btn-success">Add New</button>
</router-link>
</div>
<div v-if="userCards.length === 0" class="mt-2 text-sm opacity-70">
<p class="font-semibold text-warning">No cards on file.</p>
</div>
<div class="mt-4 space-y-3">
<div
v-for="card in userCards"
:key="card.id"
class="p-2 border rounded-lg cursor-pointer transition-colors"
:class="{
'bg-blue-500 text-white border-blue-500': selectedCard?.id === card.id,
'bg-primary/10 border-primary': selectedCard?.id !== card.id && card.main_card,
'bg-base-200 border-base-300': selectedCard?.id !== card.id && !card.main_card,
}"
@click="selectCard(card)"
>
<div class="flex items-start justify-between">
<div>
<div class="text-sm font-bold">{{ card.name_on_card }}</div>
<div class="text-xs opacity-70">{{ card.type_of_card }}</div>
</div>
<div v-if="card.main_card" class="badge badge-primary badge-sm">Primary</div>
</div>
<div class="mt-1 font-mono text-sm tracking-wider">
<p>{{ card.card_number }}</p>
<p>Exp: {{ formattedExpiration(card) }}</p>
<p>{{ card.security_number }}</p>
</div>
</div>
</div>
<!-- Payment Form - always shown -->
<div class="pt-4 mt-6 space-y-4 border-t border-base-300">
<div class="form-control">
<label class="label">
<span class="font-bold label-text">Charge Amount</span>
</label>
<input
v-model="chargeAmount"
class="w-full input input-bordered input-sm"
type="number"
step="0.01"
placeholder="0.00"
/>
</div>
<div class="flex gap-4">
<button
class="flex-1 btn btn-success"
:disabled="isSubmitting || !chargeAmount || chargeAmount <= 0 || !selectedCard"
@click="chargeService"
>
<span v-if="isSubmitting" class="loading loading-spinner loading-sm"></span>
Charge Service
</button>
<button class="btn btn-ghost" :disabled="isSubmitting" @click="cancelCharge">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Loading State -->
<div v-else class="p-10 text-center">
<span class="loading loading-spinner loading-lg"></span>
<p class="mt-2">Loading service details...</p>
</div>
</div>
</div>
<Footer />
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import authHeader from '../../../services/auth.header';
import { notify } from "@kyvg/vue3-notification";
import Footer from '../../../layouts/footers/footer.vue';
// --- Interfaces for Type Safety ---
interface UserCard {
id: number;
name_on_card: string;
type_of_card: string;
main_card: boolean;
card_number: string;
expiration_month: number;
expiration_year: number;
security_number: string;
}
interface Service {
id: number;
customer_id: number;
customer_name: string;
customer_address: string;
customer_apt: string;
customer_town: string;
customer_state: number;
customer_zip: string;
type_service_call: number;
scheduled_date: string;
description: string;
service_cost: number;
}
// --- Component State ---
const route = useRoute();
const router = useRouter();
const isSubmitting = ref(false);
const service = ref<Service | null>(null);
const userCards = ref<UserCard[]>([]);
const selectedCard = ref<UserCard | null>(null);
const chargeAmount = ref<number>(0);
const transaction = ref(null as any);
const preAuthAmount = ref<number>(0);
// --- Computed Properties for Cleaner Template ---
const stateName = computed(() => {
const stateMap: Record<number, string> = {
0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine',
4: 'Vermont', 5: 'Maine', 6: 'New York',
};
return service.value ? stateMap[service.value.customer_state] || 'Unknown state' : '';
});
const serviceTypeName = computed(() => {
const typeMap: Record<number, string> = {
0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other',
};
return service.value ? typeMap[service.value.type_service_call] || 'Unknown' : '';
});
const formattedExpiration = (card: UserCard) => {
const month = String(card.expiration_month).padStart(2, '0');
return `${month} / ${card.expiration_year}`;
};
// --- Methods ---
/**
* Toggles the selection of a credit card.
* If the clicked card is already selected, it deselects it.
* Otherwise, it selects the new card.
*/
const selectCard = (card: UserCard) => {
if (selectedCard.value?.id === card.id) {
selectedCard.value = null; // Deselect if clicked again
} else {
selectedCard.value = card; // Select the new card
}
};
const chargeService = async () => {
if (!selectedCard.value || !chargeAmount.value || chargeAmount.value <= 0) {
notify({ title: "Error", text: "Please select a card and enter a valid amount.", type: "error" });
return;
}
isSubmitting.value = true;
try {
const card = selectedCard.value;
const expMonth = String(card.expiration_month).padStart(2, '0');
const expYear = String(card.expiration_year).toString().slice(-2);
const payload = {
charge_amount: chargeAmount.value,
service_id: service.value!.id,
delivery_id: null,
transaction_type: 0,
card_id: card.id,
card_number: card.card_number,
expiration_date: `${expMonth}${expYear}`,
cvv: card.security_number,
};
console.log(payload)
console.log(card)
const response = await axios.post(
`${import.meta.env.VITE_AUTHORIZE_URL}/api/charge/${service.value!.customer_id}`,
payload,
{ withCredentials: true, headers: authHeader() }
);
if (response.data?.status === 0) {
// Payment approved: now update the service cost in the database
await axios.put(
`${import.meta.env.VITE_BASE_URL}/service/update/${service.value!.id}`,
{
service_cost: chargeAmount.value
},
{ withCredentials: true, headers: authHeader() }
);
notify({ title: "Success", text: "Service charged successfully!", type: "success" });
router.push({ name: 'customerProfile', params: { id: service.value!.customer_id } });
} else {
const reason = response.data?.rejection_reason || "The charge was declined.";
notify({ title: "Charge Declined", text: reason, type: "error" });
}
} catch (error: any) {
const detail = error.response?.data?.detail || "Failed to charge service due to a server error.";
notify({ title: "Error", text: detail, type: "error" });
console.error("Charge Service Error:", error);
} finally {
isSubmitting.value = false;
}
};
const cancelCharge = () => {
if (service.value) {
router.push({ name: 'customerProfile', params: { id: service.value.customer_id } });
}
};
const getTransaction = () => {
const serviceId = route.params.id;
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/transaction/service/${serviceId}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
transaction.value = response.data;
preAuthAmount.value = parseFloat(response.data.preauthorize_amount || service.value?.service_cost || 0);
if (response.data.status !== 0) { // Not approved
preAuthAmount.value = 0;
}
})
.catch((error: any) => {
console.error("No pre-authorized transaction found for service:", error);
preAuthAmount.value = service.value?.service_cost || 0; // fallback to service cost
});
};
// --- Lifecycle Hook ---
onMounted(async () => {
const serviceId = route.params.id;
if (!serviceId) {
notify({ title: "Error", text: "No service ID provided.", type: "error" });
router.push({ name: 'customer' });
return;
}
try {
// Fetch Service Details
const servicePath = `${import.meta.env.VITE_BASE_URL}/service/${serviceId}`;
const serviceResponse = await axios.get(servicePath, { withCredentials: true, headers: authHeader() });
if (serviceResponse.data?.ok) {
service.value = serviceResponse.data.service;
chargeAmount.value = service.value?.service_cost || 0;
// Fetch Customer Cards
const cardsPath = `${import.meta.env.VITE_BASE_URL}/payment/cards/${service.value!.customer_id}`;
const cardsResponse = await axios.get(cardsPath, { withCredentials: true, headers: authHeader() });
userCards.value = cardsResponse.data;
// Fetch pre-auth transaction
getTransaction();
} else {
throw new Error(serviceResponse.data?.error || "Failed to fetch service data.");
}
} catch (error) {
console.error("Error loading page data:", error);
notify({ title: "Error", text: "Service not found or could not be loaded.", type: "error" });
router.push({ name: 'customer' });
}
});
</script>

View File

@@ -38,18 +38,20 @@
<table class="table w-full"> <table class="table w-full">
<thead> <thead>
<tr> <tr>
<th>ID</th>
<th>Date / Time</th> <th>Date / Time</th>
<th>Customer</th> <th>Customer</th>
<th>Address</th> <th>Address</th>
<th>Service Type</th> <th>Service Type</th>
<th>Description</th> <th>Description</th>
<th class="text-right">Cost</th> <th class="text-right">Amount</th>
<th class="text-right">Actions</th> <th class="text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- Removed @click from tr to avoid conflicting actions --> <!-- Removed @click from tr to avoid conflicting actions -->
<tr v-for="service in services" :key="service.id" class=" hover:bg-blue-600"> <tr v-for="service in services" :key="service.id" class=" hover:bg-blue-600">
<td class="align-top">{{ service.id }}</td>
<td class="align-top"> <td class="align-top">
<div>{{ formatDate(service.scheduled_date) }}</div> <div>{{ formatDate(service.scheduled_date) }}</div>
<div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div> <div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div>
@@ -76,8 +78,9 @@
</div> </div>
</td> </td>
<td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td> <td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td>
<td class="text-right align-top"> <td class="text-right align-top space-x-2">
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button> <button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
<router-link v-if="service.service_cost !== undefined && service.service_cost !== '' && Number(service.service_cost) === 0" :to="{ name: 'chargeServiceAuthorize', params: { id: service.id } }" class="btn btn-sm btn-success">Charge</router-link>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -91,6 +94,7 @@
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div> <div>
<h2 class="card-title text-base">{{ service.customer_name }}</h2> <h2 class="card-title text-base">{{ service.customer_name }}</h2>
<p class="text-xs text-gray-500">ID: {{ service.id }}</p>
<p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p> <p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p>
</div> </div>
<div class="badge badge-outline text-right" :style="{ 'border-color': getServiceTypeColor(service.type_service_call), color: getServiceTypeColor(service.type_service_call) }"> <div class="badge badge-outline text-right" :style="{ 'border-color': getServiceTypeColor(service.type_service_call), color: getServiceTypeColor(service.type_service_call) }">
@@ -116,8 +120,9 @@
</div> </div>
</div> </div>
<div class="card-actions justify-end mt-2"> <div class="card-actions justify-end mt-2 space-x-2">
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button> <button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
<router-link v-if="service.service_cost !== undefined && service.service_cost !== '' && Number(service.service_cost) === 0" :to="{ name: 'chargeServiceAuthorize', params: { id: service.id } }" class="btn btn-sm btn-success">Charge</router-link>
</div> </div>
</div> </div>
</div> </div>
@@ -311,4 +316,4 @@ export default defineComponent({
} }
}, },
}) })
</script> </script>

View File

@@ -38,17 +38,19 @@
<table class="table w-full"> <table class="table w-full">
<thead> <thead>
<tr> <tr>
<th>ID</th>
<th>Date / Time</th> <th>Date / Time</th>
<th>Customer</th> <th>Customer</th>
<th>Address</th> <th>Address</th>
<th>Service Type</th> <th>Service Type</th>
<th>Description</th> <th>Description</th>
<th class="text-right">Cost</th> <th class="text-right">Amount</th>
<th class="text-right">Actions</th> <th class="text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="service in services" :key="service.id" class="hover:bg-blue-600"> <tr v-for="service in services" :key="service.id" class="hover:bg-blue-600">
<td class="align-top">{{ service.id }}</td>
<td class="align-top"> <td class="align-top">
<div>{{ formatDate(service.scheduled_date) }}</div> <div>{{ formatDate(service.scheduled_date) }}</div>
<div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div> <div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div>
@@ -82,8 +84,9 @@
</div> </div>
</td> </td>
<td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td> <td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td>
<td class="text-right align-top"> <td class="text-right align-top space-x-2">
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button> <button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
<router-link v-if="service.service_cost !== undefined && service.service_cost !== '' && Number(service.service_cost) === 0" :to="{ name: 'chargeServiceAuthorize', params: { id: service.id } }" class="btn btn-sm btn-success">Charge</router-link>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -97,6 +100,7 @@
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div> <div>
<h2 class="card-title text-base">{{ service.customer_name }}</h2> <h2 class="card-title text-base">{{ service.customer_name }}</h2>
<p class="text-xs text-gray-500">ID: {{ service.id }}</p>
<p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p> <p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p>
</div> </div>
<!-- Mobile view already uses a badge, which is great! No changes needed here. --> <!-- Mobile view already uses a badge, which is great! No changes needed here. -->
@@ -122,8 +126,9 @@
</div> </div>
</div> </div>
<div class="card-actions justify-end mt-2"> <div class="card-actions justify-end mt-2 space-x-2">
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button> <button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
<router-link v-if="service.service_cost !== undefined && service.service_cost !== '' && Number(service.service_cost) === 0" :to="{ name: 'chargeServiceAuthorize', params: { id: service.id } }" class="btn btn-sm btn-success">Charge</router-link>
</div> </div>
</div> </div>
</div> </div>
@@ -324,4 +329,4 @@ export default defineComponent({
} }
}, },
}) })
</script> </script>

View File

@@ -38,17 +38,19 @@
<table class="table w-full"> <table class="table w-full">
<thead> <thead>
<tr> <tr>
<th>ID</th>
<th>Date / Time</th> <th>Date / Time</th>
<th>Customer</th> <th>Customer</th>
<th>Address</th> <th>Address</th>
<th>Service Type</th> <th>Service Type</th>
<th>Description</th> <th>Description</th>
<th class="text-right">Cost</th> <th class="text-right">Amount</th>
<th class="text-right">Actions</th> <th class="text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="service in services" :key="service.id" class="hover:bg-blue-600"> <tr v-for="service in services" :key="service.id" class="hover:bg-blue-600">
<td class="align-top">{{ service.id }}</td>
<td class="align-top"> <td class="align-top">
<div>{{ formatDate(service.scheduled_date) }}</div> <div>{{ formatDate(service.scheduled_date) }}</div>
<div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div> <div class="text-xs opacity-70">{{ formatTime(service.scheduled_date) }}</div>
@@ -82,8 +84,9 @@
</div> </div>
</td> </td>
<td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td> <td class="text-right font-mono align-top">{{ formatCurrency(service.service_cost) }}</td>
<td class="text-right align-top"> <td class="text-right align-top space-x-2">
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button> <button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
<router-link v-if="service.service_cost !== undefined && service.service_cost !== '' && Number(service.service_cost) === 0" :to="{ name: 'chargeServiceAuthorize', params: { id: service.id } }" class="btn btn-sm btn-success">Charge</router-link>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -97,6 +100,7 @@
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div> <div>
<h2 class="card-title text-base">{{ service.customer_name }}</h2> <h2 class="card-title text-base">{{ service.customer_name }}</h2>
<p class="text-xs text-gray-500">ID: {{ service.id }}</p>
<p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p> <p class="text-xs text-gray-400">{{ service.customer_address }}, {{ service.customer_town }}</p>
</div> </div>
<!-- Mobile view already uses a badge, which is great! No changes needed here. --> <!-- Mobile view already uses a badge, which is great! No changes needed here. -->
@@ -122,8 +126,9 @@
</div> </div>
</div> </div>
<div class="card-actions justify-end mt-2"> <div class="card-actions justify-end mt-2 space-x-2">
<button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button> <button @click="openEditModal(service)" class="btn btn-sm btn-primary">View</button>
<router-link v-if="service.service_cost !== undefined && service.service_cost !== '' && Number(service.service_cost) === 0" :to="{ name: 'chargeServiceAuthorize', params: { id: service.id } }" class="btn btn-sm btn-success">Charge</router-link>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,238 @@
<!-- src/pages/transactions/authorize/index.vue -->
<template>
<div class="flex">
<div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Title -->
<div class="text-sm breadcrumbs">
<ul>
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li>Transactions</li>
<li>Authorize</li>
</ul>
</div>
<h1 class="text-3xl font-bold mt-4">Authorize.net</h1>
<!-- Main Content Card -->
<div class="bg-neutral rounded-lg p-4 sm:p-6 mt-6">
<!-- Header: Title and Count -->
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-4">
<h2 class="text-lg font-bold">All Transactions</h2>
</div>
<div class="divider"></div>
<!-- DESKTOP VIEW: Table -->
<div class="overflow-x-auto hidden xl:block">
<table class="table w-full">
<thead>
<tr>
<th>Transaction #</th>
<th>Transaction ID</th>
<th>Customer</th>
<th>Date</th>
<th>Status</th>
<th>Pre-Auth Amount</th>
<th>Charge Amount</th>
<th>Type</th>
<th>Source</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<template v-for="transaction in transactions" :key="transaction.id">
<tr class="hover:bg-blue-600 hover:text-white">
<td>{{ transaction.id }}</td>
<td>{{ transaction.auth_net_transaction_id || 'N/A' }}</td>
<td>
{{ transaction.customer_name || 'N/A' }}
</td>
<td>{{ formatDate(transaction.created_at) }}</td>
<td>
<span :class="'badge badge-sm ' + getStatusClass(transaction.status)">{{ getStatusText(transaction.status) }}</span>
</td>
<td>
${{ transaction.preauthorize_amount || '0.00' }}
</td>
<td>
${{ transaction.charge_amount || '0.00' }}
</td>
<td>
{{ transaction.transaction_type === 0 ? 'Charge' : transaction.transaction_type === 1 ? 'Auth' : 'Capture' }}
</td>
<td>
{{ getSourceText(transaction) }}
</td>
<td>
<div class="flex gap-1">
<router-link v-if="transaction.delivery_id" :to="{ name: 'deliveryOrder', params: { id: transaction.delivery_id } }" class="btn btn-xs btn-info">
View
</router-link>
<template v-if="(Number(transaction.preauthorize_amount) >= 0 && Number(transaction.charge_amount) === 0 && (transaction.delivery_id || transaction.service_id))">
<router-link v-if="!transaction.auth_net_transaction_id" :to="getPreauthRoute(transaction)" class="btn btn-xs btn-warning">
Preauth/Charge
</router-link>
<router-link v-else-if="Number(transaction.preauthorize_amount) > 0" :to="getCaptureRoute(transaction)" class="btn btn-xs btn-primary">
Capture
</router-link>
</template>
</div>
</td>
</tr>
<!-- Rejection Reason Row (Full Width) -->
<tr v-if="transaction.rejection_reason && transaction.rejection_reason.trim()" class="bg-transparent border-t border-gray-300">
<td colspan="10" class="px-4 py-3 text-red-800 font-medium">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
<span class="text-red-700">{{ transaction.rejection_reason }}</span>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- MOBILE VIEW: Cards -->
<div class="xl:hidden space-y-4">
<div v-for="transaction in transactions" :key="transaction.id" class="card bg-base-100 shadow-md">
<div class="card-body p-4">
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-base">
<router-link v-if="transaction.customer_id" :to="{ name: 'customerProfile', params: { id: transaction.customer_id } }" class="link link-primary">
{{ transaction.customer_name || 'N/A' }}
</router-link>
<span v-else>{{ transaction.customer_name || 'N/A' }}</span>
</h2>
<p class="text-xs text-gray-400">Transaction #{{ transaction.id }}</p>
</div>
<div :class="'badge badge-' + getStatusClass(transaction.status)">
{{ getStatusText(transaction.status) }}
</div>
</div>
<div class="text-sm mt-2 space-y-1">
<p><strong>Transaction ID:</strong> {{ transaction.auth_net_transaction_id || 'N/A' }}</p>
<p><strong>Date:</strong> {{ formatDate(transaction.created_at) }}</p>
<p><strong>Pre-Auth:</strong> ${{ transaction.preauthorize_amount || '0.00' }}</p>
<p><strong>Charge:</strong> ${{ transaction.charge_amount || '0.00' }}</p>
<p><strong>Type:</strong> {{ transaction.transaction_type === 0 ? 'Charge' : transaction.transaction_type === 1 ? 'Auth' : 'Capture' }}</p>
<p><strong>Source:</strong> <router-link v-if="transaction.delivery_id" :to="{ name: 'deliveryOrder', params: { id: transaction.delivery_id } }" class="link link-primary">{{ getSourceText(transaction) }}</router-link><span v-else>{{ getSourceText(transaction) }}</span></p>
<!-- Rejection Reason in Mobile View -->
<div v-if="transaction.rejection_reason && transaction.rejection_reason.trim()" class="bg-transparent border border-gray-300 rounded-md p-3 mt-2">
<div class="flex items-start">
<svg class="w-4 h-4 mr-2 text-red-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
<span class="text-red-700 text-sm">{{ transaction.rejection_reason }}</span>
</div>
</div>
<!-- Action Buttons -->
<div class="mt-3 space-y-2">
<router-link v-if="transaction.delivery_id" :to="{ name: 'deliveryOrder', params: { id: transaction.delivery_id } }" class="btn btn-xs btn-info btn-block">
View Delivery
</router-link>
<template v-if="(Number(transaction.preauthorize_amount) >= 0 && Number(transaction.charge_amount) === 0 && (transaction.delivery_id || transaction.service_id))">
<router-link v-if="!transaction.auth_net_transaction_id" :to="getPreauthRoute(transaction)" class="btn btn-xs btn-warning btn-block">
Preauth/Charge
</router-link>
<router-link v-else-if="Number(transaction.preauthorize_amount) > 0" :to="getCaptureRoute(transaction)" class="btn btn-xs btn-primary btn-block">
Capture Payment
</router-link>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<Footer />
</template>
<script lang="ts">
import {defineComponent} from 'vue'
import axios from 'axios'
import authHeader from '../../../services/auth.header'
import Header from '../../../layouts/headers/headerauth.vue'
import SideBar from '../../../layouts/sidebar/sidebar.vue'
import Footer from '../../../layouts/footers/footer.vue'
export default defineComponent({
name: 'transactionsAuthorize',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
transactions: [] as any[],
}
},
created() {
this.getTransactions()
},
methods: {
getTransactions() {
let path = import.meta.env.VITE_BASE_URL + '/payment/transactions/authorize/1';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.transactions = response.data
}).catch(() => {
this.transactions = []
})
},
getStatusClass(status: number) {
return status === 0 ? 'badge-success' : 'badge-error'
},
getStatusText(status: number) {
return status === 0 ? 'Approved' : 'Declined'
},
getSourceText(transaction: any) {
if (transaction.delivery_id) {
return 'Delivery'
} else if (transaction.service_id) {
return 'Service'
} else {
return 'Other'
}
},
formatDate(dateStr: string) {
return dateStr.split('T')[0]; // YYYY-MM-DD
},
getCaptureRoute(transaction: any) {
if (transaction.service_id) {
return { name: 'chargeServiceAuthorize', params: { id: transaction.service_id } };
} else if (transaction.delivery_id) {
return { name: 'captureAuthorize', params: { id: transaction.delivery_id } };
}
return {}; // fallback, though condition should prevent this
},
getPreauthRoute(transaction: any) {
if (transaction.service_id) {
return { name: 'chargeServiceAuthorize', params: { id: transaction.service_id } };
} else if (transaction.delivery_id) {
return { name: 'authorizePreauthCharge', params: { id: transaction.delivery_id } };
}
return {}; // fallback, though condition should prevent this
},
},
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,11 @@
import AuthorizePage from './authorize/index.vue';
const transactionsRoutes = [
{
path: '/transactions/authorize',
name: 'transactionsAuthorize',
component: AuthorizePage,
},
];
export default transactionsRoutes;

View File

@@ -12,6 +12,7 @@ import adminRoutes from "../pages/admin/routes.ts";
import tickerRoutes from "../pages/ticket/routes.ts"; import tickerRoutes from "../pages/ticket/routes.ts";
import moneyRoutes from "../pages/money/routes.ts"; import moneyRoutes from "../pages/money/routes.ts";
import serviceRoutes from "../pages/service/routes.ts"; import serviceRoutes from "../pages/service/routes.ts";
import transactionsRoutes from '../pages/transactions/routes.ts';
// Import your page components // Import your page components
import Home from '../pages/Index.vue'; import Home from '../pages/Index.vue';
@@ -54,6 +55,7 @@ const routes = [
...protectRoutes(autoRoutes), ...protectRoutes(autoRoutes),
...protectRoutes(adminRoutes), ...protectRoutes(adminRoutes),
...protectRoutes(serviceRoutes), ...protectRoutes(serviceRoutes),
...protectRoutes(transactionsRoutes),
{ {
path: '', path: '',

View File

@@ -15,6 +15,7 @@ export const useCountsStore = defineStore('counts', () => {
const automatic = ref(0) const automatic = ref(0)
const upcoming_service = ref(0) const upcoming_service = ref(0)
const today_service = ref(0) const today_service = ref(0)
const transaction = ref(0)
// --- ACTIONS --- // --- ACTIONS ---
// A single action to fetch ALL counts from our new, efficient endpoint. // A single action to fetch ALL counts from our new, efficient endpoint.
@@ -32,6 +33,7 @@ export const useCountsStore = defineStore('counts', () => {
automatic.value = counts.automatic; automatic.value = counts.automatic;
upcoming_service.value = counts.upcoming_service; upcoming_service.value = counts.upcoming_service;
today_service.value = counts.today_service || 0; today_service.value = counts.today_service || 0;
transaction.value = counts.transaction || 0;
} }
} catch { } catch {
// No error param, as requested // No error param, as requested
@@ -50,6 +52,7 @@ export const useCountsStore = defineStore('counts', () => {
automatic, automatic,
upcoming_service, upcoming_service,
today_service, today_service,
transaction,
fetchSidebarCounts, fetchSidebarCounts,
} }
}) })