major claude changes

This commit is contained in:
2026-01-28 21:55:14 -05:00
parent f9d0e4c0fd
commit f9b5364c53
81 changed files with 11155 additions and 10086 deletions

21
package-lock.json generated
View File

@@ -19,14 +19,12 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"moment": "^2.30.1",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"v-pagination-3": "^0.1.7", "v-pagination-3": "^0.1.7",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-debounce": "^5.0.0", "vue-debounce": "^5.0.0",
"vue-router": "^4.2.5", "vue-router": "^4.2.5",
"vue3-pdfmake": "^2.2.0", "vue3-pdfmake": "^2.2.0"
"vuelidate": "^0.7.7"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.12", "@types/leaflet": "^1.9.12",
@@ -4482,14 +4480,6 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"engines": {
"node": "*"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -9129,15 +9119,6 @@
} }
} }
}, },
"node_modules/vuelidate": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/vuelidate/-/vuelidate-0.7.7.tgz",
"integrity": "sha512-pT/U2lDI67wkIqI4tum7cMSIfGcAMfB+Phtqh2ttdXURwvHRBJEAQ0tVbUsW9Upg83Q5QH59bnCoXI7A9JDGnA==",
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -20,14 +20,12 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"moment": "^2.30.1",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"v-pagination-3": "^0.1.7", "v-pagination-3": "^0.1.7",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-debounce": "^5.0.0", "vue-debounce": "^5.0.0",
"vue-router": "^4.2.5", "vue-router": "^4.2.5",
"vue3-pdfmake": "^2.2.0", "vue3-pdfmake": "^2.2.0"
"vuelidate": "^0.7.7"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.12", "@types/leaflet": "^1.9.12",

View File

@@ -116,9 +116,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent, PropType } from 'vue'
import axios from 'axios' import axios, { AxiosError } from 'axios'
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import { TRANSACTION_STATUS } from '../constants/status'
import type {
DeliveryFormData,
CustomerFormData,
CreditCardFormData,
PricingData,
PromoData
} from '../types/models'
export default defineComponent({ export default defineComponent({
name: 'PaymentAuthorizePopup', name: 'PaymentAuthorizePopup',
@@ -128,19 +136,19 @@ export default defineComponent({
default: false default: false
}, },
delivery: { delivery: {
type: Object, type: Object as PropType<DeliveryFormData>,
required: true required: true
}, },
customer: { customer: {
type: Object, type: Object as PropType<CustomerFormData>,
required: true required: true
}, },
creditCards: { creditCards: {
type: Array, type: Array as PropType<CreditCardFormData[]>,
default: () => [] default: () => []
}, },
pricing: { pricing: {
type: Object, type: Object as PropType<PricingData>,
required: true required: true
}, },
promoActive: { promoActive: {
@@ -148,7 +156,7 @@ export default defineComponent({
default: false default: false
}, },
promo: { promo: {
type: Object, type: Object as PropType<PromoData>,
default: () => ({}) default: () => ({})
}, },
totalAmount: { totalAmount: {
@@ -175,9 +183,9 @@ export default defineComponent({
} }
}, },
computed: { computed: {
selectedCard(): any { selectedCard(): CreditCardFormData | undefined {
// Find the card that matches the delivery's payment_card_id // Find the card that matches the delivery's payment_card_id
return this.creditCards.find((card: any) => card.id === this.delivery.payment_card_id) return this.creditCards.find((card) => card.id === this.delivery.payment_card_id)
} }
}, },
watch: { watch: {
@@ -223,14 +231,14 @@ export default defineComponent({
try { try {
const endpoint = actionType === 'preauthorize' ? 'authorize' : 'charge' const endpoint = actionType === 'preauthorize' ? 'authorize' : 'charge'
const payload = { const payload = {
card_number: (this.selectedCard as any).card_number, card_number: this.selectedCard.card_number,
expiration_date: `${(this.selectedCard as any).expiration_month}${(this.selectedCard as any).expiration_year}`, expiration_date: `${this.selectedCard.expiration_month}${this.selectedCard.expiration_year}`,
cvv: (this.selectedCard as any).security_number, cvv: this.selectedCard.security_number,
[actionType === 'preauthorize' ? 'preauthorize_amount' : 'charge_amount']: this.chargeAmount.toString(), [actionType === 'preauthorize' ? 'preauthorize_amount' : 'charge_amount']: this.chargeAmount.toString(),
transaction_type: actionType === 'preauthorize' ? 1 : 0, transaction_type: actionType === 'preauthorize' ? 1 : 0,
service_id: this.delivery.service_id || null, // Add service_id from delivery service_id: this.delivery.service_id || null, // Add service_id from delivery
delivery_id: this.delivery.id, // Add delivery_id from delivery delivery_id: this.delivery.id, // Add delivery_id from delivery
card_id: (this.selectedCard as any).id // Add card_id from selected card card_id: this.selectedCard.id // Add card_id from selected card
} }
console.log('=== DEBUG: PaymentAuthorizePopup payload ===') console.log('=== DEBUG: PaymentAuthorizePopup payload ===')
@@ -245,7 +253,7 @@ export default defineComponent({
{ withCredentials: true } { withCredentials: true }
) )
if (response.data && response.data.status === 0) { if (response.data && response.data.status === TRANSACTION_STATUS.APPROVED) {
this.success = `${actionType === 'preauthorize' ? 'Preauthorization' : 'Charge'} successful! Transaction ID: ${response.data.auth_net_transaction_id || 'N/A'}` this.success = `${actionType === 'preauthorize' ? 'Preauthorization' : 'Charge'} successful! Transaction ID: ${response.data.auth_net_transaction_id || 'N/A'}`
this.$emit('payment-success', { this.$emit('payment-success', {
action: actionType, action: actionType,
@@ -255,7 +263,8 @@ export default defineComponent({
} else { } else {
throw new Error(`Payment ${actionType} failed: ${response.data?.status || 'Unknown error'}`) throw new Error(`Payment ${actionType} failed: ${response.data?.status || 'Unknown error'}`)
} }
} catch (error: any) { } catch (err: unknown) {
const error = err as AxiosError<{ detail?: string }>
this.error = error.response?.data?.detail || `Failed to ${actionType} payment` this.error = error.response?.data?.detail || `Failed to ${actionType} payment`
notify({ notify({
title: "Error", title: "Error",

View File

@@ -52,49 +52,51 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { mapState, mapActions } from 'pinia'; import { useSearchStore } from '../stores/search'
import { useSearchStore } from '../stores/search'; // Adjust path if needed
export default defineComponent({ // Store
name: 'SearchResults', const searchStore = useSearchStore()
data() {
return {
stateMap: {
0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY',
} as Record<number, string>,
};
},
computed: { // Template ref
...mapState(useSearchStore, ['searchTerm', 'searchResults', 'isLoading']), const searchContainer = ref<HTMLElement>()
},
methods: { // Reactive data
...mapActions(useSearchStore, ['clearSearch']), const stateMap = ref({
0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY',
} as Record<number, string>)
getStateName(stateValue: number | string): string { // Computed properties
const stateNumber = Number(stateValue); const searchTerm = computed(() => searchStore.searchTerm)
return this.stateMap[stateNumber] || 'N/A'; const searchResults = computed(() => searchStore.searchResults)
}, const isLoading = computed(() => searchStore.isLoading)
handleClickOutside(event: MouseEvent) { // Functions
const searchContainer = this.$refs.searchContainer as HTMLElement; const getStateName = (stateValue: number | string): string => {
const searchInput = document.getElementById('customer-search-input'); const stateNumber = Number(stateValue);
return stateMap.value[stateNumber] || 'N/A';
}
if (searchContainer && !searchContainer.contains(event.target as Node) && searchInput && !searchInput.contains(event.target as Node)) { const clearSearch = () => {
this.clearSearch(); searchStore.clearSearch();
} }
},
},
mounted() { const handleClickOutside = (event: MouseEvent) => {
document.addEventListener('mousedown', this.handleClickOutside); const container = searchContainer.value;
}, const searchInput = document.getElementById('customer-search-input');
beforeUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside); if (container && !container.contains(event.target as Node) && searchInput && !searchInput.contains(event.target as Node)) {
}, searchStore.clearSearch();
}); }
</script> }
// Lifecycle
onMounted(() => {
document.addEventListener('mousedown', handleClickOutside);
})
onBeforeUnmount(() => {
document.removeEventListener('mousedown', handleClickOutside);
})
</script>

95
src/constants/status.ts Normal file
View File

@@ -0,0 +1,95 @@
/**
* EAMCO Office Frontend Status Constants
*
* This file contains all status code constants used throughout the frontend
* to eliminate magic numbers and improve code maintainability.
*/
export const DELIVERY_STATUS = {
WAITING: 0,
CANCELLED: 1,
OUT_FOR_DELIVERY: 2,
TOMORROW: 3,
PARTIAL_DELIVERY: 4,
ISSUE: 5,
UNKNOWN: 6,
PENDING_PAYMENT: 9,
FINALIZED: 10,
DELIVERED: 11, // New: Replaces previous use of 1 for delivered
} as const;
export const PAYMENT_STATUS = {
UNPAID: 0,
PRE_AUTHORIZED: 1,
PROCESSING: 2,
PAID: 3,
FAILED: 4,
} as const;
export const AUTO_STATUS = {
DEFAULT: 0,
WILL_CALL: 1,
READY_FOR_FINALIZATION: 3,
} as const;
export const TRANSACTION_STATUS = {
APPROVED: 0,
DECLINED: 1,
} as const;
export const CUSTOMER_AUTOMATIC_STATUS = {
WILL_CALL: 0,
AUTOMATIC: 1,
} as const;
// Helper functions for type-safe status access
export type DeliveryStatusType = typeof DELIVERY_STATUS[keyof typeof DELIVERY_STATUS];
export type PaymentStatusType = typeof PAYMENT_STATUS[keyof typeof PAYMENT_STATUS];
export type AutoStatusType = typeof AUTO_STATUS[keyof typeof AUTO_STATUS];
export type TransactionStatusType = typeof TRANSACTION_STATUS[keyof typeof TRANSACTION_STATUS];
export type CustomerAutomaticStatus = typeof CUSTOMER_AUTOMATIC_STATUS[keyof typeof CUSTOMER_AUTOMATIC_STATUS];
// Utility functions for status display
export function getDeliveryStatusLabel(status: DeliveryStatusType): string {
switch (status) {
case DELIVERY_STATUS.WAITING: return 'Waiting';
case DELIVERY_STATUS.CANCELLED: return 'Cancelled';
case DELIVERY_STATUS.OUT_FOR_DELIVERY: return 'Out for Delivery';
case DELIVERY_STATUS.TOMORROW: return 'Tomorrow';
case DELIVERY_STATUS.PARTIAL_DELIVERY: return 'Partial Delivery';
case DELIVERY_STATUS.ISSUE: return 'Issue';
case DELIVERY_STATUS.UNKNOWN: return 'Unknown';
case DELIVERY_STATUS.PENDING_PAYMENT: return 'Pending Payment';
case DELIVERY_STATUS.FINALIZED: return 'Finalized';
case DELIVERY_STATUS.DELIVERED: return 'Delivered';
default: return 'N/A';
}
}
export function getPaymentStatusLabel(status: PaymentStatusType): string {
switch (status) {
case PAYMENT_STATUS.UNPAID: return 'Unpaid';
case PAYMENT_STATUS.PRE_AUTHORIZED: return 'Pre-authorized';
case PAYMENT_STATUS.PROCESSING: return 'Processing';
case PAYMENT_STATUS.PAID: return 'Paid';
case PAYMENT_STATUS.FAILED: return 'Failed';
default: return 'Unknown';
}
}
export function getAutoStatusLabel(status: AutoStatusType): string {
switch (status) {
case AUTO_STATUS.DEFAULT: return 'Default';
case AUTO_STATUS.WILL_CALL: return 'Will Call';
case AUTO_STATUS.READY_FOR_FINALIZATION: return 'Ready for Finalization';
default: return 'Unknown';
}
}
export function getTransactionStatusLabel(status: TransactionStatusType): string {
switch (status) {
case TRANSACTION_STATUS.APPROVED: return 'Approved';
case TRANSACTION_STATUS.DECLINED: return 'Declined';
default: return 'Unknown';
}
}

View File

@@ -212,9 +212,9 @@
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
// NO CHANGES to the script block were made. All your logic remains intact. import { ref, computed, onMounted } from 'vue'
import { defineComponent } from 'vue' import { useRouter } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { useSearchStore } from '../../stores/search' // Adjust path if needed import { useSearchStore } from '../../stores/search' // Adjust path if needed
@@ -231,180 +231,182 @@ interface RoutingOption {
label: string; label: string;
} }
// Router
const router = useRouter()
export default defineComponent({ // Reactive data
data() { const user = ref({} as User)
return { const currentPhone = ref('')
// Initialize with empty objects to prevent template errors const routingOptions = ref([
user: {} as User, { value: 'main', label: '407323' },
currentPhone: '', { value: 'sip', label: '407323_auburnoil' },
routingOptions: [ { value: 'cellphone1', label: 'Ed Cell' },
{ value: 'main', label: '407323' }, { value: 'cellphone2', label: 'Aneta Cell' },
{ value: 'sip', label: '407323_auburnoil' }, { value: 'test_did', label: 'Test DID' }
{ value: 'cellphone1', label: 'Ed Cell' }, ] as RoutingOption[])
{ value: 'cellphone2', label: 'Aneta Cell' }, const selectedOption = ref<RoutingOption | null>(null)
{ value: 'test_did', label: 'Test DID' } const isRouteModalVisible = ref(false)
] as RoutingOption[], const routeModalMode = ref('confirm')
selectedOption: null as RoutingOption | null, const routeResponse = ref(null as any)
isRouteModalVisible: false, const isTestModalVisible = ref(false)
routeModalMode: 'confirm', const isTestLoading = ref(false)
routeResponse: null as any, const testResponse = ref(null as any)
isTestModalVisible: false,
isTestLoading: false,
testResponse: null as any
}
},
computed: { // Computed properties
searchStore() { const searchStore = computed(() => useSearchStore())
return useSearchStore();
}, const userInitials = computed((): string => {
userInitials(): string { if (!user.value || !user.value.user_name) return '';
if (!this.user || !this.user.user_name) return ''; const parts = user.value.user_name.split(' ');
const parts = this.user.user_name.split(' '); return parts.length > 1
return parts.length > 1 ? `${parts[0][0]}${parts[1][0]}`.toUpperCase()
? `${parts[0][0]}${parts[1][0]}`.toUpperCase() : user.value.user_name.substring(0, 2).toUpperCase();
: this.user.user_name.substring(0, 2).toUpperCase(); })
},
currentDate(): string {
const now = new Date();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const year = now.getFullYear().toString().slice(-2);
return `${month}/${day}/${year}`;
},
dayOfWeek(): string {
const now = new Date();
return now.toLocaleDateString('en-US', { weekday: 'long' });
}
},
created() { const currentDate = computed((): string => {
this.userStatus(); const now = new Date();
}, const month = (now.getMonth() + 1).toString().padStart(2, '0');
mounted() { const day = now.getDate().toString().padStart(2, '0');
this.updatestatus(); const year = now.getFullYear().toString().slice(-2);
this.fetchCurrentPhone(); return `${month}/${day}/${year}`;
}, })
methods: {
userStatus() {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
}) const dayOfWeek = computed((): string => {
.then((response: any) => { const now = new Date();
if (response.data.ok) { return now.toLocaleDateString('en-US', { weekday: 'long' });
this.user = response.data.user; })
} else {
localStorage.removeItem('user'); // Lifecycle
this.$router.push('/login'); onMounted(() => {
} userStatus()
}) updatestatus()
}, fetchCurrentPhone()
updatestatus() { })
let path = import.meta.env.VITE_BASE_URL + '/delivery/updatestatus';
axios({ // Functions
method: 'get', const userStatus = () => {
url: path, let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
headers: authHeader(), axios({
}).then((response: any) => { method: "get",
if (response.data.update) url: path,
console.log("Updated Status of Deliveries") withCredentials: true,
}) headers: authHeader(),
},
logout() { })
// Clear auth data .then((response: any) => {
const authStore = useAuthStore(); if (response.data.ok) {
authStore.clearAuth(); user.value = response.data.user;
// Redirect to login
this.$router.push({ name: 'login' });
},
fetchCurrentPhone() {
const path = import.meta.env.VITE_BASE_URL + '/admin/voip_routing';
axios({
method: 'get',
url: path,
headers: authHeader(),
withCredentials: true,
})
.then((response: any) => {
if (response.data.current_phone) {
this.currentPhone = response.data.current_phone;
}
})
.catch((error: any) => {
console.error('Failed to fetch current routing:', error);
});
},
routeTo(route: string): Promise<any> {
const path = `${import.meta.env.VITE_VOIPMS_URL}/route/${route}`;
return axios({
method: 'post',
url: path,
withCredentials: true, headers: authHeader()
})
.then((response: any) => {
this.routeResponse = response.data;
// Find the corresponding label
const option = this.routingOptions.find(opt => opt.value === route);
if (option) {
this.currentPhone = option.label;
}
return response.data;
});
},
showConfirmRoute(option: RoutingOption) {
this.selectedOption = option;
if (option.value === 'test_did') {
this.testDid();
} else { } else {
this.isRouteModalVisible = true;
this.routeModalMode = 'confirm'; localStorage.removeItem('user');
router.push('/login');
} }
}, })
proceedRoute() { }
if (this.selectedOption && this.selectedOption.value !== 'test_did') {
this.routeModalMode = 'loading'; const updatestatus = () => {
this.routeTo(this.selectedOption.value) let path = import.meta.env.VITE_BASE_URL + '/delivery/updatestatus';
.then(() => { axios({
this.routeModalMode = 'result'; method: 'get',
}) url: path,
.catch((error: any) => { headers: authHeader(),
this.routeResponse = { error: error.message }; }).then((response: any) => {
this.routeModalMode = 'result'; if (response.data.update)
}); console.log("Updated Status of Deliveries")
})
}
const logout = () => {
// Clear auth data
const authStore = useAuthStore();
authStore.clearAuth();
// Redirect to login
router.push({ name: 'login' });
}
const fetchCurrentPhone = () => {
const path = import.meta.env.VITE_BASE_URL + '/admin/voip_routing';
axios({
method: 'get',
url: path,
headers: authHeader(),
withCredentials: true,
})
.then((response: any) => {
if (response.data.current_phone) {
currentPhone.value = response.data.current_phone;
} }
}, })
closeRouteModal() { .catch((error: any) => {
this.isRouteModalVisible = false; console.error('Failed to fetch current routing:', error);
this.routeModalMode = 'confirm'; });
this.routeResponse = null; }
this.selectedOption = null;
}, const routeTo = (route: string): Promise<any> => {
testDid() { const path = `${import.meta.env.VITE_VOIPMS_URL}/route/${route}`;
this.isTestModalVisible = true; return axios({
this.isTestLoading = true; method: 'post',
const path = `${import.meta.env.VITE_VOIPMS_URL}/test/did`; url: path,
axios({ withCredentials: true, headers: authHeader()
method: 'get', })
url: path, .then((response: any) => {
withCredentials: true, headers: authHeader() routeResponse.value = response.data;
}) // Find the corresponding label
.then((response: any) => { const option = routingOptions.value.find(opt => opt.value === route);
this.testResponse = response.data; if (option) {
this.isTestLoading = false; currentPhone.value = option.label;
}) }
.catch((error: any) => { return response.data;
this.testResponse = { status: 'error', message: error.message }; });
this.isTestLoading = false; }
});
} const showConfirmRoute = (option: RoutingOption) => {
selectedOption.value = option;
if (option.value === 'test_did') {
testDid();
} else {
isRouteModalVisible.value = true;
routeModalMode.value = 'confirm';
} }
}); }
const proceedRoute = () => {
if (selectedOption.value && selectedOption.value.value !== 'test_did') {
routeModalMode.value = 'loading';
routeTo(selectedOption.value.value)
.then(() => {
routeModalMode.value = 'result';
})
.catch((error: any) => {
routeResponse.value = { error: error.message };
routeModalMode.value = 'result';
});
}
}
const closeRouteModal = () => {
isRouteModalVisible.value = false;
routeModalMode.value = 'confirm';
routeResponse.value = null;
selectedOption.value = null;
}
const testDid = () => {
isTestModalVisible.value = true;
isTestLoading.value = true;
const path = `${import.meta.env.VITE_VOIPMS_URL}/test/did`;
axios({
method: 'get',
url: path,
withCredentials: true, headers: authHeader()
})
.then((response: any) => {
testResponse.value = response.data;
isTestLoading.value = false;
})
.catch((error: any) => {
testResponse.value = { status: 'error', message: error.message };
isTestLoading.value = false;
});
}
</script> </script>

View File

@@ -15,31 +15,19 @@
<script lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import { defineComponent } from "vue"; // Reactive data (though not used in template)
const user = ref(null)
const loaded = ref(false)
const clicked = ref(false)
const hovered = ref(false)
// Lifecycle
onMounted(() => {
export default defineComponent({ // Component mounted
name: "HeaderNoAuth", })
mounted() {
},
data() {
return {
user: null,
loaded: false,
clicked: false,
hovered: false,
};
},
methods: {
},
});
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -110,200 +110,185 @@
</div> </div>
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
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'
export default defineComponent({ // Props
name: 'Home', const props = defineProps<{
clickCount?: number
}>()
components: { // Router
Header, const router = useRouter()
SideBar,
Footer,
},
props: {
clickCount: Number
},
data() {
return {
token: null,
call_count:0,
delivery_count: 0,
delivery_count_delivered: 0,
price_from_supplier: 0,
today_oil_price: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
user: {
user_id: 0,
user_name: '',
},
employee: {
id: '',
user_id: '',
employee_last_name: "",
employee_first_name: "",
employee_town: "",
employee_address: "",
employee_apt: "",
employee_zip: "",
employee_birthday: "",
employee_phone_number: "",
employee_start_date: "",
employee_end_date: "",
employee_type: '',
employee_state: '',
},
total_gallons_past_week: 0,
total_profit_past_week: 0,
total_deliveries: 0,
// Reactive data
loaded: false, const token = ref(null)
const call_count = ref(0)
} const delivery_count = ref(0)
}, const delivery_count_delivered = ref(0)
const price_from_supplier = ref(0)
created() { const today_oil_price = ref(0)
this.userStatus() const price_for_employee = ref(0)
this.today_delivery_count() const price_same_day = ref(0)
this.today_delivery_delivered() const price_prime = ref(0)
this.today_price_oil() const price_emergency = ref(0)
this.totalgallonsweek() const user = ref({
this.totalprofitweek() user_id: 0,
user_name: '',
},
methods: {
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;
this.employeeStatus()
} else {
localStorage.removeItem('user');
this.$router.push('/login');
}
})
},
totalgallonsweek() {
let path = import.meta.env.VITE_BASE_URL + '/stats/gallons/week';
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.total_gallons_past_week = response.data.total;
})
},
totalprofitweek() {
let path = import.meta.env.VITE_BASE_URL + '/money/profit/week';
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.total_profit_past_week = response.data.total_profit;
this.total_deliveries = response.data.total_deliveries;
})
},
employeeStatus() {
let path = import.meta.env.VITE_BASE_URL + '/employee/userid/' + this.user.user_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.employee = response.data;
this.loaded = true;
})
},
total_calls() {
let path = import.meta.env.VITE_BASE_URL + '/stats/call/count/today'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.call_count = response.data.data;
})
},
today_delivery_count() {
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/today'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.delivery_count = response.data.data;
})
},
today_delivery_delivered() {
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/delivered/today'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
console.log(response.data)
this.delivery_count_delivered = response.data.data;
})
},
today_price_oil() {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.price_from_supplier = response.data.price_from_supplier;
this.today_oil_price = response.data.price_for_customer;
this.price_for_employee = response.data.price_for_employee;
this.price_same_day = response.data.price_same_day;
this.price_prime = response.data.price_prime;
this.price_emergency = response.data.price_emergency;
})
},
},
}) })
</script> const employee = ref({
id: '',
user_id: '',
employee_last_name: "",
employee_first_name: "",
employee_town: "",
employee_address: "",
employee_apt: "",
employee_zip: "",
employee_birthday: "",
employee_phone_number: "",
employee_start_date: "",
employee_end_date: "",
employee_type: '',
employee_state: '',
})
const total_gallons_past_week = ref(0)
const total_profit_past_week = ref(0)
const total_deliveries = ref(0)
const loaded = ref(false)
<style scoped></style> // Lifecycle
<script setup lang="ts"> onMounted(() => {
userStatus()
today_delivery_count()
today_delivery_delivered()
today_price_oil()
totalgallonsweek()
totalprofitweek()
})
// Functions
const 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) {
user.value = response.data.user;
employeeStatus()
} else {
localStorage.removeItem('user');
router.push('/login');
}
})
}
const totalgallonsweek = () => {
let path = import.meta.env.VITE_BASE_URL + '/stats/gallons/week';
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
total_gallons_past_week.value = response.data.total;
})
}
const totalprofitweek = () => {
let path = import.meta.env.VITE_BASE_URL + '/money/profit/week';
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
total_profit_past_week.value = response.data.total_profit;
total_deliveries.value = response.data.total_deliveries;
})
}
const employeeStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/employee/userid/' + user.value.user_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
employee.value = response.data;
loaded.value = true;
})
}
const total_calls = () => {
let path = import.meta.env.VITE_BASE_URL + '/stats/call/count/today'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
call_count.value = response.data.data;
})
}
const today_delivery_count = () => {
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/today'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
delivery_count.value = response.data.data;
})
}
const today_delivery_delivered = () => {
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/delivered/today'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
console.log(response.data)
delivery_count_delivered.value = response.data.data;
})
}
const today_price_oil = () => {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
price_from_supplier.value = response.data.price_from_supplier;
today_oil_price.value = response.data.price_for_customer;
price_for_employee.value = response.data.price_for_employee;
price_same_day.value = response.data.price_same_day;
price_prime.value = response.data.price_prime;
price_emergency.value = response.data.price_emergency;
})
}
</script> </script>

View File

@@ -1,10 +1,10 @@
import OilPrice from '../admin/oilprice.vue'; const OilPrice = () => import('../admin/oilprice.vue');
import Promo from '../admin/promo/promo.vue'; const Promo = () => import('../admin/promo/promo.vue');
import PromoCreate from '../admin/promo/create.vue'; const PromoCreate = () => import('../admin/promo/create.vue');
import PromoEdit from '../admin/promo/edit.vue'; const PromoEdit = () => import('../admin/promo/edit.vue');
const adminRoutes = [ const adminRoutes = [
@@ -35,5 +35,3 @@ const adminRoutes = [
export default adminRoutes export default adminRoutes
//sourceMappingURL=index.ts.map //sourceMappingURL=index.ts.map

View File

@@ -1,7 +1,7 @@
import Login from '../auth/login.vue'; const Login = () => import('../auth/login.vue');
import Register from '../auth/register.vue'; const Register = () => import('../auth/register.vue');
import changePassword from '../auth/changepassword.vue'; const changePassword = () => import('../auth/changepassword.vue');
import lostPassword from '../auth/lostpassword.vue'; const lostPassword = () => import('../auth/lostpassword.vue');
const authRoutes = [ const authRoutes = [
{ {
@@ -37,4 +37,4 @@ const authRoutes = [
}, },
] ]
export default authRoutes; export default authRoutes;

View File

@@ -1,5 +1,5 @@
<!-- src/pages/authorize/PaymentForm.vue --> <!-- src/pages/authorize/PaymentForm.vue -->
<!-- <template> <template>
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">HVAC Payment</h1> <h1 class="text-2xl font-bold mb-4">HVAC Payment</h1>
@@ -87,89 +87,91 @@
</div> </div>
</div> </div>
<div v-if="transactionResult" class="mt-8 p-4 rounded-lg" :class="transactionResult.status === 'approved' || transactionResult.status === 'authorized' || transactionResult.status === 'captured' ? 'bg-green-200' : 'bg-red-200'"> <div v-if="transactionResult" class="mt-8 p-4 rounded-lg" :class="transactionResult.status === 0 || transactionResult.status === 1 || transactionResult.status === 2 ? 'bg-green-200' : 'bg-red-200'">
<h3 class="font-bold">Transaction Result</h3> <h3 class="font-bold">Transaction Result</h3>
<p>Status: {{ transactionResult.status }}</p> <p>Status: {{ transactionResult.status }}</p>
<p v-if="transactionResult.auth_net_transaction_id">Transaction ID: {{ transactionResult.auth_net_transaction_id }}</p> <p v-if="transactionResult.transaction_id">Transaction ID: {{ transactionResult.transaction_id }}</p>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import axios from 'axios'; import { ref } from 'vue'
import axios from 'axios'
import { AuthorizeTransaction } from '../../types/models'
const API_URL = 'http://localhost:8000/api'; // Your FastAPI backend URL const API_URL = 'http://localhost:8000/api'; // Your FastAPI backend URL
export default { // Reactive data
data() { const directCharge = ref({
return { cardNumber: '',
directCharge: { expirationDate: '',
cardNumber: '', cvv: '',
expirationDate: '', amount: 0,
cvv: '', })
amount: 0,
}, const authorization = ref({
authorization: { cardNumber: '',
cardNumber: '', expirationDate: '',
expirationDate: '', cvv: '',
cvv: '', amount: 0,
amount: 0, })
},
capture: { const capture = ref({
amount: 0, amount: 0,
}, })
authorizedTransactionId: null,
transactionResult: null, const authorizedTransactionId = ref<string | null>(null)
customerId: 1 // Assuming a customer with ID 1 exists for this example const transactionResult = ref<AuthorizeTransaction | null>(null)
}; const customerId = ref(1) // Assuming a customer with ID 1 exists for this example
},
methods: { // Functions
async submitDirectCharge() { const submitDirectCharge = async () => {
try { try {
const response = await axios.post(`${API_URL}/charge/?customer_id=${this.customerId}`, { const response = await axios.post(`${API_URL}/charge/?customer_id=${customerId.value}`, {
card_number: this.directCharge.cardNumber, card_number: directCharge.value.cardNumber,
expiration_date: this.directCharge.expirationDate, expiration_date: directCharge.value.expirationDate,
cvv: this.directCharge.cvv, cvv: directCharge.value.cvv,
amount: this.directCharge.amount, amount: directCharge.value.amount,
transaction_type: 'charge' transaction_type: 'charge'
}); });
this.transactionResult = response.data; transactionResult.value = response.data;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
this.transactionResult = { status: 'Error processing transaction' }; transactionResult.value = null;
} }
}, }
async submitAuthorization() {
try { const submitAuthorization = async () => {
const response = await axios.post(`${API_URL}/authorize/?customer_id=${this.customerId}`, { try {
card_number: this.authorization.cardNumber, const response = await axios.post(`${API_URL}/authorize/?customer_id=${customerId.value}`, {
expiration_date: this.authorization.expirationDate, card_number: authorization.value.cardNumber,
cvv: this.authorization.cvv, expiration_date: authorization.value.expirationDate,
amount: this.authorization.amount, cvv: authorization.value.cvv,
transaction_type: 'auth' amount: authorization.value.amount,
}); transaction_type: 'auth'
this.transactionResult = response.data; });
if (response.data.status === 'authorized') { transactionResult.value = response.data;
this.authorizedTransactionId = response.data.auth_net_transaction_id; if (response.data.status === 'authorized') {
this.capture.amount = this.authorization.amount; // Pre-fill capture amount authorizedTransactionId.value = response.data.auth_net_transaction_id;
} capture.value.amount = authorization.value.amount; // Pre-fill capture amount
} catch (error) { }
console.error(error); } catch (error) {
this.transactionResult = { status: 'Error processing authorization' }; console.error(error);
} transactionResult.value = null;
}, }
async submitCapture() { }
try {
const response = await axios.post(`${API_URL}/capture/`, { const submitCapture = async () => {
amount: this.capture.amount, try {
auth_net_transaction_id: this.authorizedTransactionId const response = await axios.post(`${API_URL}/capture/`, {
}); amount: capture.value.amount,
this.transactionResult = response.data; auth_net_transaction_id: authorizedTransactionId.value
} catch (error) { });
console.error(error); transactionResult.value = response.data;
this.transactionResult = { status: 'Error processing capture' }; } catch (error) {
} console.error(error);
}, transactionResult.value = null;
}, }
}; }
</script> --> </script>

View File

@@ -177,138 +177,118 @@
</div> </div>
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, computed, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { AutoDelivery } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../layouts/footers/footer.vue'
// Define a type for the delivery object for better code quality // Reactive data
interface AutoDelivery { const user = ref(null)
id: number; const deliveries = ref<AutoDelivery[]>([])
customer_id: number; // --- NEW: Data properties for sorting ---
last_fill: string | null; const sortKey = ref<'tank_level_percent' | 'hot_water_summer' | keyof AutoDelivery>('tank_level_percent')
estimated_gallons_left: number; const sortAsc = ref(true)
days_since_last_fill: number;
customer_full_name: string; // Computed properties
customer_address: string; // --- NEW: Computed property to handle sorting ---
customer_town: string; const sortedDeliveries = computed((): AutoDelivery[] => {
house_factor: number; // Create a copy to avoid mutating the original array
tank_size: number; const sorted = [...deliveries.value];
auto_status: number;
hot_water_summer: number; sorted.sort((a, b) => {
open_ticket_id?: number | null; // First, prioritize auto_status = 3 to be at the top
if (a.auto_status === 3 && b.auto_status !== 3) {
return -1;
}
if (a.auto_status !== 3 && b.auto_status === 3) {
return 1;
}
// Then sort by the selected key
let valA: any;
let valB: any;
// Special case for our calculated percentage
if (sortKey.value === 'tank_level_percent') {
valA = getTankLevelPercentage(a);
valB = getTankLevelPercentage(b);
} else {
valA = a[sortKey.value as keyof AutoDelivery];
valB = b[sortKey.value as keyof AutoDelivery];
// Special handling for hot_water_summer to ensure it's number
if (sortKey.value === 'hot_water_summer') {
valA = valA || 0;
valB = valB || 0;
}
}
// Handle nulls or different types if necessary
if (valA === null) return 1;
if (valB === null) return -1;
// Comparison logic
if (valA < valB) {
return sortAsc.value ? -1 : 1;
}
if (valA > valB) {
return sortAsc.value ? 1 : -1;
}
return 0;
});
return sorted;
})
// Lifecycle
onMounted(() => {
userStatus();
get_oil_orders();
})
// Functions
// --- NEW: Method to handle sorting ---
const sortBy = (key: keyof AutoDelivery | 'tank_level_percent' | 'hot_water_summer') => {
if (sortKey.value === key) {
// If clicking the same key, reverse the direction
sortAsc.value = !sortAsc.value;
} else {
// If clicking a new key, set it and default to ascending
sortKey.value = key;
sortAsc.value = true;
}
} }
export default defineComponent({ // --- NEW: Helper method for percentage calculation ---
name: 'AutomaticHome', const getTankLevelPercentage = (oil: AutoDelivery): number => {
components: { if (!oil.tank_size || oil.tank_size === 0 || oil.last_fill === null) {
Footer, return 0; // Return 0 if tank size is invalid or it's a new customer
}, }
data() { return (oil.estimated_gallons_left / oil.tank_size) * 100;
return { }
user: null,
deliveries: [] as AutoDelivery[],
// --- NEW: Data properties for sorting ---
sortKey: 'tank_level_percent' as keyof AutoDelivery | 'tank_level_percent' | 'hot_water_summer',
sortAsc: true,
}
},
computed: {
// --- NEW: Computed property to handle sorting ---
sortedDeliveries(): AutoDelivery[] {
// Create a copy to avoid mutating the original array
const sorted = [...this.deliveries];
sorted.sort((a, b) => { const userStatus = () => {
// First, prioritize auto_status = 3 to be at the top const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
if (a.auto_status === 3 && b.auto_status !== 3) { axios.get(path, { withCredentials: true, headers: authHeader() })
return -1; .then((response: any) => {
} if (response.data.ok) {
if (a.auto_status !== 3 && b.auto_status === 3) { user.value = response.data.user;
return 1;
}
// Then sort by the selected key
let valA: any;
let valB: any;
// Special case for our calculated percentage
if (this.sortKey === 'tank_level_percent') {
valA = this.getTankLevelPercentage(a);
valB = this.getTankLevelPercentage(b);
} else {
valA = a[this.sortKey as keyof AutoDelivery];
valB = b[this.sortKey as keyof AutoDelivery];
// Special handling for hot_water_summer to ensure it's number
if (this.sortKey === 'hot_water_summer') {
valA = valA || 0;
valB = valB || 0;
}
}
// Handle nulls or different types if necessary
if (valA === null) return 1;
if (valB === null) return -1;
// Comparison logic
if (valA < valB) {
return this.sortAsc ? -1 : 1;
}
if (valA > valB) {
return this.sortAsc ? 1 : -1;
}
return 0;
});
return sorted;
}
},
created() {
this.userStatus();
this.get_oil_orders();
},
methods: {
// --- NEW: Method to handle sorting ---
sortBy(key: keyof AutoDelivery | 'tank_level_percent' | 'hot_water_summer') {
if (this.sortKey === key) {
// If clicking the same key, reverse the direction
this.sortAsc = !this.sortAsc;
} else {
// If clicking a new key, set it and default to ascending
this.sortKey = key;
this.sortAsc = true;
} }
}, })
// --- NEW: Helper method for percentage calculation --- .catch(() => {
getTankLevelPercentage(oil: AutoDelivery): number { user.value = null;
if (!oil.tank_size || oil.tank_size === 0 || oil.last_fill === null) { });
return 0; // Return 0 if tank size is invalid or it's a new customer }
}
return (oil.estimated_gallons_left / oil.tank_size) * 100; const get_oil_orders = () => {
},
userStatus() {
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null;
});
},
get_oil_orders() {
const path = import.meta.env.VITE_AUTO_URL + '/delivery/all/customers'; const path = import.meta.env.VITE_AUTO_URL + '/delivery/all/customers';
axios.get(path, { withCredentials: true, headers: authHeader() }) axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => { .then((response: any) => {
this.deliveries = response.data; deliveries.value = response.data;
}) })
.catch((error: any) => { .catch((error: any) => {
console.error("Failed to fetch automatic deliveries:", error); console.error("Failed to fetch automatic deliveries:", error);
}); });
}, }
},
})
</script> </script>

View File

@@ -1,7 +1,7 @@
import AutomaticHome from './home.vue'; const AutomaticHome = () => import('./home.vue');
import AutomaticView from './view.vue'; const AutomaticView = () => import('./view.vue');
const autoRoutes = [ const autoRoutes = [
{ {

View File

@@ -54,26 +54,26 @@
</div> </div>
</div> </div>
<!-- Payment Status Card --> <!-- Payment Status Card -->
<div class="bg-neutral rounded-lg p-5"> <div class="bg-neutral rounded-lg p-5">
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="col-span-2"> <div class="col-span-2">
<div class="font-bold text-sm">Payment Status</div> <div class="font-bold text-sm">Payment Status</div>
<div class="badge badge-lg" <div class="badge badge-lg"
:class="{ :class="{
'badge-success': autoTicket.payment_status == 3, 'badge-success': autoTicket.payment_status === PAYMENT_STATUS.PAID,
'badge-info': autoTicket.payment_status == 1, 'badge-info': autoTicket.payment_status === PAYMENT_STATUS.PRE_AUTHORIZED,
'badge-error': autoTicket.payment_status == 0 || autoTicket.payment_status == null, 'badge-error': autoTicket.payment_status === PAYMENT_STATUS.UNPAID || autoTicket.payment_status == null,
'badge-warning': [2, 4].includes(autoTicket.payment_status) 'badge-warning': [PAYMENT_STATUS.PROCESSING, PAYMENT_STATUS.FAILED].includes(autoTicket.payment_status)
}"> }">
<span v-if="autoTicket.payment_status == 0 || autoTicket.payment_status == null">Unpaid</span> <span v-if="autoTicket.payment_status === PAYMENT_STATUS.UNPAID || autoTicket.payment_status == null">Unpaid</span>
<span v-else-if="autoTicket.payment_status == 1">Pre-authorized</span> <span v-else-if="autoTicket.payment_status === PAYMENT_STATUS.PRE_AUTHORIZED">Pre-authorized</span>
<span v-else-if="autoTicket.payment_status == 2">Processing</span> <span v-else-if="autoTicket.payment_status === PAYMENT_STATUS.PROCESSING">Processing</span>
<span v-else-if="autoTicket.payment_status == 3">Paid</span> <span v-else-if="autoTicket.payment_status === PAYMENT_STATUS.PAID">Paid</span>
<span v-else-if="autoTicket.payment_status == 4">Failed</span> <span v-else-if="autoTicket.payment_status === PAYMENT_STATUS.FAILED">Failed</span>
<span v-else>Unknown</span> <span v-else>Unknown</span>
</div> </div>
</div> </div>
<div> <div>
<div class="font-bold text-sm">Fill Date</div> <div class="font-bold text-sm">Fill Date</div>
<div>{{ autoTicket.fill_date }}</div> <div>{{ autoTicket.fill_date }}</div>
@@ -150,8 +150,8 @@
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span>Status:</span> <span>Status:</span>
<span :class="transaction.status === 0 ? 'text-success' : 'text-error'"> <span :class="transaction.status === TRANSACTION_STATUS.APPROVED ? 'text-success' : 'text-error'">
{{ transaction.status === 0 ? 'Approved' : 'Declined' }} {{ transaction.status === TRANSACTION_STATUS.APPROVED ? 'Approved' : 'Declined' }}
</span> </span>
</div> </div>
</div> </div>
@@ -242,25 +242,21 @@
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 {
interface UserCard { PAYMENT_STATUS,
id: number; AUTO_STATUS,
last_four: string; TRANSACTION_STATUS,
type_of_card: string; getPaymentStatusLabel,
expiration_month: number; getTransactionStatusLabel
expiration_year: number; } from '../../constants/status';
name_on_card: string;
card_number: string;
security_number: string;
main_card?: boolean;
}
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 useValidate from "@vuelidate/core"; import useValidate from "@vuelidate/core";
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import moment from 'moment'; import dayjs from 'dayjs';
import {AutoDelivery, Customer, AuthorizeTransaction, CreditCard} from '../../types/models';
export default defineComponent({ export default defineComponent({
name: 'automaticDeliveryView', name: 'automaticDeliveryView',
@@ -273,62 +269,25 @@ export default defineComponent({
data() { data() {
return { return {
v$: useValidate(), v$: useValidate(),
autoTicket: { autoTicket: {} as any,
id: 0, autoDelivery: {} as AutoDelivery,
customer_id: 0, customer: {} as Customer,
account_number: '', transaction: {} as AuthorizeTransaction,
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_full_name: '',
customer_apt: '',
fill_date: '',
oil_prices_id: 0,
gallons_delivered: '',
price_per_gallon: '',
total_amount_customer: '',
payment_type: 0,
payment_card_id: null,
payment_status: null,
open_ticket_id: null,
} as any,
autoDelivery: {
id: 0,
customer_id: 0,
account_number: '',
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_full_name: '',
last_fill: '',
days_since_last_fill: 0,
last_updated: '',
estimated_gallons_left: 0,
estimated_gallons_left_prev_day: 0,
tank_height: '',
tank_size: '',
house_factor: 0,
auto_status: 0,
open_ticket_id: null,
},
customer: {
id: 0,
user_id: 0,
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
},
transaction: null as any,
userCardfound: false, userCardfound: false,
userCard: {} as UserCard, userCard: {} as CreditCard,
}
},
computed: {
// Expose constants to template
PAYMENT_STATUS() {
return PAYMENT_STATUS;
},
AUTO_STATUS() {
return AUTO_STATUS;
},
TRANSACTION_STATUS() {
return TRANSACTION_STATUS;
} }
}, },
@@ -339,10 +298,11 @@ export default defineComponent({
methods: { methods: {
format_date(value: string) { format_date(value: string) {
if (value) { if (value) {
return moment(String(value)).format('LLLL') return dayjs(String(value)).format('LLLL')
} }
}, },
getTypeColor(transactionType: number) { getTypeColor(transactionType: number | undefined) {
if (transactionType === undefined) return 'text-gray-600';
switch (transactionType) { switch (transactionType) {
case 1: return 'text-blue-600'; // Auth case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge case 0: return 'text-orange-600'; // Charge
@@ -360,7 +320,7 @@ export default defineComponent({
} }
}, },
getCustomer(customerId: number) { getCustomer(customerId: number | undefined) {
if (!customerId) return; if (!customerId) return;
const path = `${import.meta.env.VITE_BASE_URL}/customer/${customerId}`; const path = `${import.meta.env.VITE_BASE_URL}/customer/${customerId}`;
axios.get(path, { withCredentials: true }) axios.get(path, { withCredentials: true })
@@ -384,12 +344,12 @@ export default defineComponent({
this.userCard = response.data; this.userCard = response.data;
this.userCardfound = true; this.userCardfound = true;
} else { } else {
this.userCard = {} as UserCard; this.userCard = {} as CreditCard;
this.userCardfound = false; this.userCardfound = false;
} }
}) })
.catch((_error: any) => { .catch((_error: any) => {
this.userCard = {} as UserCard; this.userCard = {} as CreditCard;
this.userCardfound = false; this.userCardfound = false;
}); });
}, },
@@ -438,7 +398,7 @@ export default defineComponent({
}) })
.catch((error: any) => { .catch((error: any) => {
console.error("No transaction found for delivery:", error); console.error("No transaction found for delivery:", error);
this.transaction = null; this.transaction = {} as AuthorizeTransaction;
}); });
}, },
}, },

View File

@@ -1,8 +1,8 @@
import CardHome from '../card/home.vue'; const CardHome = () => import('../card/home.vue');
import AddCardCreate from '../card/addcard.vue'; const AddCardCreate = () => import('../card/addcard.vue');
import EditCard from "./editcard.vue"; const EditCard = () => import("./editcard.vue");
const cardRoutes = [ const cardRoutes = [

View File

@@ -120,193 +120,189 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { ServicePlan, Customer } from '../../types/models'
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";
interface ServicePlan { const route = useRoute()
id: number; const router = useRouter()
customer_id: number;
contract_plan: number; // Reactive data
contract_years: number; const customerId = ref(route.params.id as string)
contract_start_date: string; const customerName = ref('')
const servicePlan = ref(null as ServicePlan | null)
const formData = ref({
contract_plan: 0,
contract_years: 1,
contract_start_date: '',
})
const renewalDate = ref('')
// Computed
const computedEndDate = computed(() => {
return (startDate: string, years: number): string => {
if (!startDate) return 'N/A';
const date = new Date(startDate);
date.setFullYear(date.getFullYear() + years);
return date.toISOString().split('T')[0];
}
})
// Functions
const loadCustomer = async () => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${customerId.value}`;
const response = await axios.get(path, { headers: authHeader() });
const customer: Customer = response.data;
customerName.value = `${customer.customer_first_name} ${customer.customer_last_name}`;
} catch (error) {
console.error('Failed to load customer:', error);
notify({ title: "Error", text: "Failed to load customer information.", type: "error" });
}
} }
interface Customer { const loadServicePlan = async () => {
id: number; try {
customer_first_name: string; const path = `${import.meta.env.VITE_BASE_URL}/service/plans/customer/${customerId.value}`;
customer_last_name: string; const response = await axios.get(path, { headers: authHeader() });
if (response.data && response.data.contract_plan !== undefined) {
servicePlan.value = response.data;
formData.value = {
contract_plan: response.data.contract_plan,
contract_years: response.data.contract_years,
contract_start_date: response.data.contract_start_date,
};
}
} catch (error) {
// Plan doesn't exist yet, that's okay
console.log('No existing service plan found');
}
} }
export default defineComponent({ const onSubmit = async () => {
name: 'ServicePlanEdit', try {
components: { Footer }, const payload = {
data() { customer_id: parseInt(customerId.value),
return { ...formData.value
customerId: this.$route.params.id as string, };
customerName: '',
servicePlan: null as ServicePlan | null, let response;
formData: { if (servicePlan.value) {
// Update existing plan
const path = `${import.meta.env.VITE_BASE_URL}/service/plans/update/${customerId.value}`;
response = await axios.put(path, payload, { headers: authHeader() });
} else {
// Create new plan
const path = `${import.meta.env.VITE_BASE_URL}/service/plans/create`;
response = await axios.post(path, payload, { headers: authHeader() });
}
if (response.data.ok) {
notify({
title: "Success",
text: `Service plan ${servicePlan.value ? 'updated' : 'created'} successfully!`,
type: "success"
});
// Redirect to profile page after successful submission
router.push({ name: 'customerProfile', params: { id: customerId.value } });
}
} catch (error) {
console.error('Failed to save service plan:', error);
notify({ title: "Error", text: "Failed to save service plan.", type: "error" });
}
}
const deletePlan = async () => {
if (!confirm('Are you sure you want to delete this service plan?')) return;
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/plans/delete/${customerId.value}`;
const response = await axios.delete(path, { headers: authHeader() });
if (response.data.ok) {
notify({ title: "Success", text: "Service plan deleted successfully!", type: "success" });
servicePlan.value = null;
formData.value = {
contract_plan: 0, contract_plan: 0,
contract_years: 1, contract_years: 1,
contract_start_date: '', contract_start_date: '',
},
renewalDate: '',
}
},
created() {
this.loadCustomer();
this.loadServicePlan();
},
methods: {
async loadCustomer() {
try {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${this.customerId}`;
const response = await axios.get(path, { headers: authHeader() });
const customer: Customer = response.data;
this.customerName = `${customer.customer_first_name} ${customer.customer_last_name}`;
} catch (error) {
console.error('Failed to load customer:', error);
notify({ title: "Error", text: "Failed to load customer information.", type: "error" });
}
},
async loadServicePlan() {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/plans/customer/${this.customerId}`;
const response = await axios.get(path, { headers: authHeader() });
if (response.data && response.data.contract_plan !== undefined) {
this.servicePlan = response.data;
this.formData = {
contract_plan: response.data.contract_plan,
contract_years: response.data.contract_years,
contract_start_date: response.data.contract_start_date,
};
}
} catch (error) {
// Plan doesn't exist yet, that's okay
console.log('No existing service plan found');
}
},
async onSubmit() {
try {
const payload = {
customer_id: parseInt(this.customerId),
...this.formData
};
let response;
if (this.servicePlan) {
// Update existing plan
const path = `${import.meta.env.VITE_BASE_URL}/service/plans/update/${this.customerId}`;
response = await axios.put(path, payload, { headers: authHeader() });
} else {
// Create new plan
const path = `${import.meta.env.VITE_BASE_URL}/service/plans/create`;
response = await axios.post(path, payload, { headers: authHeader() });
}
if (response.data.ok) {
notify({
title: "Success",
text: `Service plan ${this.servicePlan ? 'updated' : 'created'} successfully!`,
type: "success"
});
// Redirect to profile page after successful submission
this.$router.push({ name: 'customerProfile', params: { id: this.customerId } });
}
} catch (error) {
console.error('Failed to save service plan:', error);
notify({ title: "Error", text: "Failed to save service plan.", type: "error" });
}
},
async deletePlan() {
if (!confirm('Are you sure you want to delete this service plan?')) return;
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/plans/delete/${this.customerId}`;
const response = await axios.delete(path, { headers: authHeader() });
if (response.data.ok) {
notify({ title: "Success", text: "Service plan deleted successfully!", type: "success" });
this.servicePlan = null;
this.formData = {
contract_plan: 0,
contract_years: 1,
contract_start_date: '',
};
}
} catch (error) {
console.error('Failed to delete service plan:', error);
notify({ title: "Error", text: "Failed to delete service plan.", type: "error" });
}
},
renewContract() {
if (!this.renewalDate) {
notify({ title: "Error", text: "Please select a renewal date.", type: "error" });
return;
}
this.formData.contract_years += 1;
this.formData.contract_start_date = this.renewalDate;
this.renewalDate = '';
notify({
title: "Success",
text: "Contract renewed! Years increased by 1 and start date updated.",
type: "success"
});
},
getPlanName(planType: number): string {
const planNames: { [key: number]: string } = {
1: 'Standard Plan',
2: 'Premium Plan'
}; };
return planNames[planType] || 'No Plan';
},
formatEndDate(startDate: string, years: number): string {
if (!startDate) return 'N/A';
const date = new Date(startDate);
date.setFullYear(date.getFullYear() + years);
return date.toISOString().split('T')[0];
},
getStatusText(startDate: string, years: number): string {
if (!startDate) return 'Unknown';
const endDate = new Date(startDate);
endDate.setFullYear(endDate.getFullYear() + years);
const now = new Date();
if (now > endDate) {
return 'Expired';
} else if (now > new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1000)) {
return 'Expiring Soon';
} else {
return 'Active';
}
},
getStatusBadge(startDate: string, years: number): string {
if (!startDate) return 'badge-ghost';
const endDate = new Date(startDate);
endDate.setFullYear(endDate.getFullYear() + years);
const now = new Date();
if (now > endDate) {
return 'badge-error';
} else if (now > new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1000)) {
return 'badge-warning';
} else {
return 'badge-success';
}
} }
}, } catch (error) {
console.error('Failed to delete service plan:', error);
notify({ title: "Error", text: "Failed to delete service plan.", type: "error" });
}
}
const renewContract = () => {
if (!renewalDate.value) {
notify({ title: "Error", text: "Please select a renewal date.", type: "error" });
return;
}
formData.value.contract_years += 1;
formData.value.contract_start_date = renewalDate.value;
renewalDate.value = '';
notify({
title: "Success",
text: "Contract renewed! Years increased by 1 and start date updated.",
type: "success"
});
}
const getPlanName = (planType: number): string => {
const planNames: { [key: number]: string } = {
1: 'Standard Plan',
2: 'Premium Plan'
};
return planNames[planType] || 'No Plan';
}
const formatEndDate = (startDate: string, years: number): string => {
if (!startDate) return 'N/A';
const date = new Date(startDate);
date.setFullYear(date.getFullYear() + years);
return date.toISOString().split('T')[0];
}
const getStatusText = (startDate: string, years: number): string => {
if (!startDate) return 'Unknown';
const endDate = new Date(startDate);
endDate.setFullYear(endDate.getFullYear() + years);
const now = new Date();
if (now > endDate) {
return 'Expired';
} else if (now > new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1000)) {
return 'Expiring Soon';
} else {
return 'Active';
}
}
const getStatusBadge = (startDate: string, years: number): string => {
if (!startDate) return 'badge-ghost';
const endDate = new Date(startDate);
endDate.setFullYear(endDate.getFullYear() + years);
const now = new Date();
if (now > endDate) {
return 'badge-error';
} else if (now > new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1000)) {
return 'badge-warning';
} else {
return 'badge-success';
}
}
// Lifecycle
onMounted(() => {
loadCustomer();
loadServicePlan();
}) })
</script> </script>

View File

@@ -140,120 +140,114 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { StateOption, HomeTypeOption } from '../../types/models'
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 { email, minLength, required } from "@vuelidate/validators"; import { email, minLength, required } from "@vuelidate/validators";
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
interface SelectOption { // Reactive data
text: string; const router = useRouter()
value: number; const user = ref(null)
const stateList = ref<StateOption[]>([])
const custList = ref<HomeTypeOption[]>([])
// Form object
const CreateCustomerForm = ref({
customer_last_name: "",
customer_first_name: "",
customer_town: "",
customer_address: "",
customer_apt: "",
customer_zip: "",
customer_email: "",
customer_phone_number: "",
customer_description: "",
customer_home_type: 0,
customer_state: 0,
})
// Validation rules
const rules = {
CreateCustomerForm: {
customer_last_name: { required, minLength: minLength(1) },
customer_first_name: { required, minLength: minLength(1) },
customer_town: { required, minLength: minLength(1) },
customer_zip: { required, minLength: minLength(5) },
customer_email: { email },
customer_phone_number: { required },
customer_home_type: { required },
customer_state: { required },
customer_address: { required },
},
} }
export default defineComponent({ // Vuelidate instance
name: 'CustomerCreate', const v$ = useValidate(rules, { CreateCustomerForm })
components: {
Footer,
},
data() {
return {
v$: useValidate(),
user: null,
stateList: [] as SelectOption[],
custList: [] as SelectOption[],
// --- REFACTORED: Simplified, flat form object ---
CreateCustomerForm: {
customer_last_name: "",
customer_first_name: "",
customer_town: "",
customer_address: "",
customer_apt: "",
customer_zip: "",
customer_email: "",
customer_phone_number: "",
customer_description: "",
// --- FIX: Initialized as numbers for proper v-model binding ---
customer_home_type: 0,
customer_state: 0,
},
}
},
validations() {
return {
// --- REFACTORED: Validation rules point to the flat form object ---
CreateCustomerForm: {
customer_last_name: { required, minLength: minLength(1) },
customer_first_name: { required, minLength: minLength(1) },
customer_town: { required, minLength: minLength(1) },
customer_zip: { required, minLength: minLength(5) },
customer_email: { email }, // Optional, so only validate format
customer_phone_number: { required },
customer_home_type: { required },
customer_state: { required },
customer_address: { required },
},
};
},
created() {
this.userStatus();
},
mounted() {
this.getCustomerTypeList();
this.getStatesList();
},
methods: {
acceptNumber() {
const x = this.CreateCustomerForm.customer_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
if (x) {
this.CreateCustomerForm.customer_phone_number = !x[2] ? x[1] : `(${x[1]}) ${x[2]}${x[3] ? `-${x[3]}` : ''}`;
}
},
userStatus() {
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) { this.user = response.data.user; }
})
.catch(() => { this.user = null; });
},
getCustomerTypeList() { // Functions
const path = import.meta.env.VITE_BASE_URL + "/query/customertype"; const acceptNumber = () => {
axios.get(path, { withCredentials: true }) const x = CreateCustomerForm.value.customer_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
.then((response: any) => { this.custList = response.data; }); if (x) {
}, CreateCustomerForm.value.customer_phone_number = !x[2] ? x[1] : `(${x[1]}) ${x[2]}${x[3] ? `-${x[3]}` : ''}`;
getStatesList() { }
const path = import.meta.env.VITE_BASE_URL + "/query/states"; }
axios.get(path, { withCredentials: true })
.then((response: any) => { this.stateList = response.data; }); const userStatus = () => {
}, const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
CreateCustomer(payload: any) { axios.get(path, { withCredentials: true, headers: authHeader() })
const path = import.meta.env.VITE_BASE_URL + "/customer/create"; .then((response: any) => {
axios.post(path, payload, { withCredentials: true, headers: authHeader() }) if (response.data.ok) { user.value = response.data.user; }
.then((response: any) => { })
if (response.data.ok) { .catch(() => { user.value = null; });
const new_user_id = response.data.user.user_id; }
this.$router.push({ name: 'customerProfile', params: { id: new_user_id } });
} else { const getCustomerTypeList = () => {
notify({ title: "Error", text: response.data.error || "Failed to create customer.", type: "error" }); const path = import.meta.env.VITE_BASE_URL + "/query/customertype";
} axios.get(path, { withCredentials: true })
}); .then((response: any) => { custList.value = response.data; });
}, }
onSubmit() {
this.v$.$validate(); // Trigger validation const getStatesList = () => {
if (!this.v$.$error) { const path = import.meta.env.VITE_BASE_URL + "/query/states";
// If validation passes, submit the form axios.get(path, { withCredentials: true })
this.CreateCustomer(this.CreateCustomerForm); .then((response: any) => { stateList.value = response.data; });
}
const CreateCustomer = (payload: any) => {
const path = import.meta.env.VITE_BASE_URL + "/customer/create";
axios.post(path, payload, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data.ok) {
const new_user_id = response.data.user.user_id;
router.push({ name: 'customerProfile', params: { id: new_user_id } });
} else { } else {
// If validation fails, show a single notification notify({ title: "Error", text: response.data.error || "Failed to create customer.", type: "error" });
notify({ title: "Validation Error", text: "Please fill out all required fields correctly.", type: "error" });
console.log("Form validation failed.");
} }
}, });
}, }
});
</script> const onSubmit = () => {
v$.value.$validate(); // Trigger validation
if (!v$.value.$error) {
// If validation passes, submit the form
CreateCustomer(CreateCustomerForm.value);
} else {
// If validation fails, show a single notification
notify({ title: "Validation Error", text: "Please fill out all required fields correctly.", type: "error" });
console.log("Form validation failed.");
}
}
// Lifecycle
onMounted(() => {
userStatus();
getCustomerTypeList();
getStatesList();
})
</script>

View File

@@ -162,235 +162,227 @@
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { StateOption, HomeTypeOption } from '../../types/models'
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 { email, minLength, required } from "@vuelidate/validators"; import { email, minLength, required } from "@vuelidate/validators";
// --- NEW: Interface for select options for better type safety --- // Reactive data
interface SelectOption { const route = useRoute()
text: string; const router = useRouter()
value: number; const user = ref(null)
} const stateList = ref<StateOption[]>([])
const custList = ref<HomeTypeOption[]>([])
export default defineComponent({ const customer = ref({
name: 'CustomerEdit', id: 0,
user_id: 0,
components: { customer_first_name: '',
// Removed unused Header and SideBar customer_last_name: '',
Footer, customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_address: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
})
const customerDescription = ref({
id: 0,
customer_id: 0,
account_number: '',
company_id: '',
fill_location: 0,
description: '',
})
const CreateCustomerForm = ref({
basicInfo: {
customer_last_name: "",
customer_first_name: "",
customer_town: "",
customer_apt: "",
customer_home_type: 0,
customer_zip: "",
customer_automatic: false,
customer_email: "",
customer_phone_number: "",
customer_state: 0,
customer_address: "",
customer_description: "",
customer_fill_location: 0,
}, },
servicePlan: {
data() { contract_plan: 0,
return { contract_years: 1,
v$: useValidate(), contract_start_date: "",
user: null,
stateList: [] as SelectOption[],
custList: [] as SelectOption[],
customer: {
id: 0,
user_id: 0,
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_address: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
},
customerDescription: {
id: 0,
customer_id: 0,
account_number: '',
company_id: '',
fill_location: 0,
description: '',
},
CreateCustomerForm: {
basicInfo: {
customer_last_name: "",
customer_first_name: "",
customer_town: "",
customer_apt: "",
// --- FIX: Initialized as a number ---
customer_home_type: 0,
customer_zip: "",
customer_automatic: false,
customer_email: "",
customer_phone_number: "",
// --- FIX: Initialized as a number ---
customer_state: 0,
customer_address: "",
customer_description: "",
customer_fill_location: 0,
},
servicePlan: {
contract_plan: 0,
contract_years: 1,
contract_start_date: "",
},
},
renewalDate: "",
}
},
validations() {
return {
CreateCustomerForm: {
basicInfo: {
customer_last_name: { required, minLength: minLength(1) },
customer_first_name: { required, minLength: minLength(1) },
customer_town: { required, minLength: minLength(1) },
customer_home_type: { required },
customer_zip: { required, minLength: minLength(5) },
customer_email: { email }, // Removed required to match template label "Optional"
customer_phone_number: { required },
customer_state: { required },
customer_address: { required },
},
},
};
},
created() {
this.userStatus()
this.getCustomer(this.$route.params.id)
},
mounted() {
this.getCustomerTypeList();
this.getStatesList();
},
methods: {
acceptNumber() {
let x = this.CreateCustomerForm.basicInfo.customer_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
if (x) {
this.CreateCustomerForm.basicInfo.customer_phone_number = !x[2] ? x[1] : '(' + x[1] + ') ' + x[2] + (x[3] ? '-' + x[3] : '');
} else {
this.CreateCustomerForm.basicInfo.customer_phone_number = '';
}
},
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;
}
})
.catch(() => {
this.user = null
})
},
getCustomerDescription(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/description/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customerDescription = response.data
this.CreateCustomerForm.basicInfo.customer_description = this.customerDescription.description;
this.CreateCustomerForm.basicInfo.customer_fill_location = this.customerDescription.fill_location
})
},
getCustomer(userid: any) {
let path = import.meta.env.VITE_BASE_URL + "/customer/" + userid;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
this.customer = response.data;
this.getCustomerDescription(this.customer.id);
this.CreateCustomerForm.basicInfo.customer_last_name = response.data.customer_last_name;
this.CreateCustomerForm.basicInfo.customer_first_name = response.data.customer_first_name;
this.CreateCustomerForm.basicInfo.customer_town = response.data.customer_town;
this.CreateCustomerForm.basicInfo.customer_state = response.data.customer_state;
this.CreateCustomerForm.basicInfo.customer_zip = response.data.customer_zip;
this.CreateCustomerForm.basicInfo.customer_phone_number = response.data.customer_phone_number;
this.CreateCustomerForm.basicInfo.customer_home_type = response.data.customer_home_type;
this.CreateCustomerForm.basicInfo.customer_apt = response.data.customer_apt;
this.CreateCustomerForm.basicInfo.customer_email = response.data.customer_email;
this.CreateCustomerForm.basicInfo.customer_address = response.data.customer_address;
if (response.data.customer_automatic === 1) {
this.CreateCustomerForm.basicInfo.customer_automatic = true
}
if (response.data.customer_automatic === 0) {
this.CreateCustomerForm.basicInfo.customer_automatic = false
}
}
})
},
editItem(payload: any) { // Simplified payload type for brevity
let path = import.meta.env.VITE_BASE_URL + "/customer/edit/" + this.customer.id;
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id }, query: { success: 'true' } });
} else if (response.data.error) {
// Handle specific errors if needed
this.$router.push("/");
}
})
},
onSubmit() {
// Create payload with both basic info and service plan data
const payload = {
...this.CreateCustomerForm.basicInfo,
service_plan: this.CreateCustomerForm.servicePlan
};
this.editItem(payload);
},
getCustomerTypeList() {
let path = import.meta.env.VITE_BASE_URL + "/query/customertype";
axios.get(path, { withCredentials: true })
.then((response: any) => {
this.custList = response.data;
});
},
getStatesList() {
let path = import.meta.env.VITE_BASE_URL + "/query/states";
axios.get(path, { withCredentials: true })
.then((response: any) => {
this.stateList = response.data;
});
},
formatEndDate(startDate: string, years: number): string {
if (!startDate) return 'N/A';
const date = new Date(startDate);
date.setFullYear(date.getFullYear() + years);
return date.toISOString().split('T')[0];
},
renewContract() {
if (!this.renewalDate) {
alert('Please select a renewal date');
return;
}
this.CreateCustomerForm.servicePlan.contract_years += 1;
this.CreateCustomerForm.servicePlan.contract_start_date = this.renewalDate;
this.renewalDate = '';
alert('Contract renewed! Years increased by 1 and start date updated.');
},
}, },
}) })
const renewalDate = ref("")
// Validation rules
const rules = {
CreateCustomerForm: {
basicInfo: {
customer_last_name: { required, minLength: minLength(1) },
customer_first_name: { required, minLength: minLength(1) },
customer_town: { required, minLength: minLength(1) },
customer_home_type: { required },
customer_zip: { required, minLength: minLength(5) },
customer_email: { email },
customer_phone_number: { required },
customer_state: { required },
customer_address: { required },
},
},
}
// Vuelidate instance
const v$ = useValidate(rules, { CreateCustomerForm })
// Functions
const acceptNumber = () => {
let x = CreateCustomerForm.value.basicInfo.customer_phone_number.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/);
if (x) {
CreateCustomerForm.value.basicInfo.customer_phone_number = !x[2] ? x[1] : '(' + x[1] + ') ' + x[2] + (x[3] ? '-' + x[3] : '');
} else {
CreateCustomerForm.value.basicInfo.customer_phone_number = '';
}
}
const 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) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const getCustomerDescription = (userid: any) => {
let path = import.meta.env.VITE_BASE_URL + '/customer/description/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
customerDescription.value = response.data
CreateCustomerForm.value.basicInfo.customer_description = customerDescription.value.description;
CreateCustomerForm.value.basicInfo.customer_fill_location = customerDescription.value.fill_location
})
}
const getCustomer = (userid: any) => {
let path = import.meta.env.VITE_BASE_URL + "/customer/" + userid;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
customer.value = response.data;
getCustomerDescription(customer.value.id);
CreateCustomerForm.value.basicInfo.customer_last_name = response.data.customer_last_name;
CreateCustomerForm.value.basicInfo.customer_first_name = response.data.customer_first_name;
CreateCustomerForm.value.basicInfo.customer_town = response.data.customer_town;
CreateCustomerForm.value.basicInfo.customer_state = response.data.customer_state;
CreateCustomerForm.value.basicInfo.customer_zip = response.data.customer_zip;
CreateCustomerForm.value.basicInfo.customer_phone_number = response.data.customer_phone_number;
CreateCustomerForm.value.basicInfo.customer_home_type = response.data.customer_home_type;
CreateCustomerForm.value.basicInfo.customer_apt = response.data.customer_apt;
CreateCustomerForm.value.basicInfo.customer_email = response.data.customer_email;
CreateCustomerForm.value.basicInfo.customer_address = response.data.customer_address;
if (response.data.customer_automatic === 1) {
CreateCustomerForm.value.basicInfo.customer_automatic = true
}
if (response.data.customer_automatic === 0) {
CreateCustomerForm.value.basicInfo.customer_automatic = false
}
}
})
}
const editItem = (payload: any) => {
let path = import.meta.env.VITE_BASE_URL + "/customer/edit/" + customer.value.id;
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
router.push({ name: "customerProfile", params: { id: customer.value.id }, query: { success: 'true' } });
} else if (response.data.error) {
router.push("/");
}
})
}
const onSubmit = () => {
const payload = {
...CreateCustomerForm.value.basicInfo,
service_plan: CreateCustomerForm.value.servicePlan
};
editItem(payload);
}
const getCustomerTypeList = () => {
let path = import.meta.env.VITE_BASE_URL + "/query/customertype";
axios.get(path, { withCredentials: true })
.then((response: any) => {
custList.value = response.data;
});
}
const getStatesList = () => {
let path = import.meta.env.VITE_BASE_URL + "/query/states";
axios.get(path, { withCredentials: true })
.then((response: any) => {
stateList.value = response.data;
});
}
const formatEndDate = (startDate: string, years: number): string => {
if (!startDate) return 'N/A';
const date = new Date(startDate);
date.setFullYear(date.getFullYear() + years);
return date.toISOString().split('T')[0];
}
const renewContract = () => {
if (!renewalDate.value) {
alert('Please select a renewal date');
return;
}
CreateCustomerForm.value.servicePlan.contract_years += 1;
CreateCustomerForm.value.servicePlan.contract_start_date = renewalDate.value;
renewalDate.value = '';
alert('Contract renewed! Years increased by 1 and start date updated.');
}
// Lifecycle
onMounted(() => {
userStatus()
getCustomer(route.params.id)
getCustomerTypeList();
getStatesList();
})
</script> </script>

View File

@@ -105,107 +105,92 @@
</div> </div>
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { customerService } from '../../services/customerService'
import { Customer } from '../../types/models'
import Header from '../../layouts/headers/headerauth.vue' import Header from '../../layouts/headers/headerauth.vue'
import PaginationComp from '../../components/pagination.vue' import PaginationComp from '../../components/pagination.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'
export default defineComponent({ // Reactive data
name: 'CustomerHome', const token = ref(null)
const user = ref(null)
const customers = ref<Customer[]>([])
const customer_count = ref(0)
const page = ref(1)
const perPage = ref(50)
const recordsLength = ref(0)
const options = ref({
edgeNavigation: false,
format: false,
template: PaginationComp
})
components: { // Functions
Header, const getPage = (pageVal: any) => {
SideBar, customers.value = [];
Footer, get_customers(pageVal)
}, get_customer_count()
}
data() { const userStatus = () => {
return { let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
token: null, axios({
user: null, method: 'get',
customers: [] as any[], url: path,
customer_count: 0, withCredentials: true,
page: 1, headers: authHeader(),
perPage: 50, })
recordsLength: 0, .then((response: any) => {
if (response.data.ok) {
options: { user.value = response.data.user;
edgeNavigation: false,
format: false,
template: PaginationComp
} }
})
.catch(() => {
user.value = null
})
}
const get_customers = async (pageVal: number) => {
try {
const response = await customerService.getAll(pageVal)
customers.value = response.data || []
} catch (error) {
console.error('Error fetching customers:', error)
customers.value = []
}
}
const get_customer_count = async () => {
try {
const response = await customerService.getCount()
if (response.data) {
customer_count.value = response.data.count
} }
}, } catch (error) {
console.error('Error fetching customer count:', error)
}
}
created() { const deleteCustomer = (user_id: any) => {
this.userStatus() let path = import.meta.env.VITE_BASE_URL + '/customer/delete/' + user_id;
}, axios({
mounted() { method: 'delete',
this.getPage(this.page) url: path,
headers: authHeader(),
}).then(() => {
get_customers(1)
})
}
}, // Lifecycle
methods: { onMounted(() => {
getPage: function (page: any) { userStatus()
// we simulate an api call that fetch the records from a backend getPage(page.value)
this.customers = [];
this.get_customers(page)
this.get_customer_count()
},
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;
}
})
.catch(() => {
this.user = null
})
},
get_customers(page: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/all/' + page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customers = response.data
})
},
get_customer_count() {
let path = import.meta.env.VITE_BASE_URL + '/customer/count';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.customer_count = response.data.count
})
},
deleteCustomer(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/customer/delete/' + user_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then(() => {
this.get_customers(1)
})
},
},
}) })
</script> </script>

View File

@@ -16,11 +16,11 @@
<tbody> <tbody>
<tr v-for="customer in customers" :key="customer.account_number"> <tr v-for="customer in customers" :key="customer.account_number">
<td>{{ customer.account_number }}</td> <td>{{ customer.account_number }}</td>
<td>{{ customer.first_name }}</td> <td>{{ customer.customer_first_name }}</td>
<td>{{ customer.last_name }}</td> <td>{{ customer.customer_last_name }}</td>
<td>{{ customer.address }}</td> <td>{{ customer.customer_address }}</td>
<td>{{ customer.town }}</td> <td>{{ customer.customer_town }}</td>
<td>{{ customer.phone_number }}</td> <td>{{ customer.customer_phone_number }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -28,50 +28,38 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { ref, onMounted } from 'vue'
import axios from 'axios'; import axios from 'axios'
import authHeader from '../../services/auth.header'; import authHeader from '../../services/auth.header'
import { Customer } from '../../types/models'
interface Customer { // Reactive data
account_number: string; const customers = ref<Customer[]>([])
first_name: string;
last_name: string; // Functions
address: string; const fetchCustomers = () => {
town: string; let path = import.meta.env.VITE_BASE_URL + '/report/customers/list';
phone_number: string; axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
customers.value = response.data.customers;
}
})
.catch((error: unknown) => {
console.error('Error fetching customer data:', error);
});
} }
export default defineComponent({ // Lifecycle
name: 'CustomerList', onMounted(() => {
data() { fetchCustomers();
return { })
customers: [] as Customer[]
};
},
created() {
this.fetchCustomers();
},
methods: {
fetchCustomers() {
let path = import.meta.env.VITE_BASE_URL + '/report/customers/list';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
this.customers = response.data.customers;
}
})
.catch((error: unknown) => {
console.error('Error fetching customer data:', error);
});
}
}
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -76,8 +76,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted, watch } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -101,158 +101,150 @@ interface FuelEstimation {
last_fill?: string | null; last_fill?: string | null;
} }
export default defineComponent({ // Props
name: 'TankEstimation', const props = defineProps<{
props: { customerId: number
customerId: { }>()
type: Number,
required: true
}
},
data() {
return {
estimation: null as FuelEstimation | null,
loading: true,
error: null as string | null
}
},
mounted() {
this.fetchEstimation()
},
watch: {
customerId: {
handler(newId, oldId) {
if (newId !== oldId) {
console.log('Customer ID changed from', oldId, 'to', newId)
this.fetchEstimation()
}
},
immediate: false
}
},
methods: {
async fetchEstimation() {
this.loading = true
this.error = null
this.estimation = null // Clear previous data
try { // Reactive data
console.log('Fetching estimation for customer ID:', this.customerId) const estimation = ref<FuelEstimation | null>(null)
const loading = ref(true)
const error = ref<string | null>(null)
// First check if customer is automatic // Lifecycle
const customerPath = `${import.meta.env.VITE_BASE_URL}/customer/${this.customerId}` onMounted(() => {
console.log('Checking customer type:', customerPath) fetchEstimation()
const customerResponse = await axios.get(customerPath, { headers: authHeader() }) })
const isAutomatic = customerResponse.data.customer_automatic === 1
console.log('Customer automatic status:', isAutomatic, customerResponse.data) // Watchers
watch(() => props.customerId, (newId, oldId) => {
let path: string if (newId !== oldId) {
if (isAutomatic) { console.log('Customer ID changed from', oldId, 'to', newId)
// Fetch from automatic delivery API fetchEstimation()
path = `${import.meta.env.VITE_AUTO_URL}/delivery/auto/customer/${this.customerId}`
console.log('Fetching automatic data from:', path)
} else {
// Fetch from customer estimation API
path = `${import.meta.env.VITE_AUTO_URL}/fixstuff_customer/estimate_gallons/customer/${this.customerId}`
console.log('Fetching customer data from:', path)
}
const response = await axios.get(path, { headers: authHeader() })
console.log('API Response:', response.data)
if (response.data.error) {
this.error = response.data.error
console.error('API returned error:', response.data.error)
} else {
if (isAutomatic) {
// Transform automatic delivery data to match our interface
if (response.data && response.data.id) {
const autoData = response.data
console.log('Processing automatic data:', autoData)
this.estimation = {
id: autoData.id,
customer_id: autoData.customer_id,
total_deliveries: 0, // Not available in auto data
customer_full_name: autoData.customer_full_name,
account_number: autoData.account_number,
address: autoData.customer_address,
estimated_gallons: autoData.estimated_gallons_left,
tank_size: autoData.tank_size,
scaling_factor: autoData.house_factor,
last_5_deliveries: [],
last_fill: autoData.last_fill
}
console.log('Set automatic estimation:', this.estimation)
} else {
console.warn('No automatic delivery data found for customer', this.customerId)
this.error = 'No automatic delivery data available'
}
} else {
console.log('Setting customer estimation:', response.data)
this.estimation = response.data
}
}
} catch (error: any) {
console.error('Failed to fetch fuel estimation:', error)
if (error.response?.status === 404) {
this.error = 'Customer data not found'
} else if (error.response?.data?.error) {
this.error = error.response.data.error
} else {
this.error = 'Failed to load fuel estimation data'
}
} finally {
this.loading = false
}
},
getTankLevelPercentage(): number {
if (!this.estimation || !this.estimation.tank_size || this.estimation.tank_size === 0) {
return 0
}
return (this.estimation.estimated_gallons / this.estimation.tank_size) * 100
},
calculateDailyUsage(): string {
if (!this.estimation || !this.estimation.scaling_factor) {
return 'N/A'
}
// For a typical day with ~20 degree days (moderate winter day)
const typicalDegreeDays = 20
const dailyUsage = this.estimation.scaling_factor * typicalDegreeDays
return dailyUsage.toFixed(1)
},
formatScalingFactor(scalingFactor: number | null): string {
if (scalingFactor === null || scalingFactor === undefined) {
return 'N/A'
}
return scalingFactor.toFixed(2)
},
getScalingFactorCategory(scalingFactor: number | null): string {
if (scalingFactor === null || scalingFactor === undefined) {
return 'N/A'
}
if (scalingFactor < 0.05) {
return 'Very Low'
} else if (scalingFactor < 0.1) {
return 'Low'
} else if (scalingFactor < 0.2) {
return 'Normal'
} else if (scalingFactor < 0.5) {
return 'High'
} else {
return 'Very High'
}
},
formatDate(dateString: string): string {
if (!dateString) return 'N/A'
return dayjs(dateString).format('MMM D, YYYY')
}
} }
}) })
// Functions
const fetchEstimation = async () => {
loading.value = true
error.value = null
estimation.value = null // Clear previous data
try {
console.log('Fetching estimation for customer ID:', props.customerId)
// First check if customer is automatic
const customerPath = `${import.meta.env.VITE_BASE_URL}/customer/${props.customerId}`
console.log('Checking customer type:', customerPath)
const customerResponse = await axios.get(customerPath, { headers: authHeader() })
const isAutomatic = customerResponse.data.customer_automatic === 1
console.log('Customer automatic status:', isAutomatic, customerResponse.data)
let path: string
if (isAutomatic) {
// Fetch from automatic delivery API
path = `${import.meta.env.VITE_AUTO_URL}/delivery/auto/customer/${props.customerId}`
console.log('Fetching automatic data from:', path)
} else {
// Fetch from customer estimation API
path = `${import.meta.env.VITE_AUTO_URL}/fixstuff_customer/estimate_gallons/customer/${props.customerId}`
console.log('Fetching customer data from:', path)
}
const response = await axios.get(path, { headers: authHeader() })
console.log('API Response:', response.data)
if (response.data.error) {
error.value = response.data.error
console.error('API returned error:', response.data.error)
} else {
if (isAutomatic) {
// Transform automatic delivery data to match our interface
if (response.data && response.data.id) {
const autoData = response.data
console.log('Processing automatic data:', autoData)
estimation.value = {
id: autoData.id,
customer_id: autoData.customer_id,
total_deliveries: 0, // Not available in auto data
customer_full_name: autoData.customer_full_name,
account_number: autoData.account_number,
address: autoData.customer_address,
estimated_gallons: autoData.estimated_gallons_left,
tank_size: autoData.tank_size,
scaling_factor: autoData.house_factor,
last_5_deliveries: [],
last_fill: autoData.last_fill
}
console.log('Set automatic estimation:', estimation.value)
} else {
console.warn('No automatic delivery data found for customer', props.customerId)
error.value = 'No automatic delivery data available'
}
} else {
console.log('Setting customer estimation:', response.data)
estimation.value = response.data
}
}
} catch (error: any) {
console.error('Failed to fetch fuel estimation:', error)
if (error.response?.status === 404) {
error.value = 'Customer data not found'
} else if (error.response?.data?.error) {
error.value = error.response.data.error
} else {
error.value = 'Failed to load fuel estimation data'
}
} finally {
loading.value = false
}
}
const getTankLevelPercentage = (): number => {
if (!estimation.value || !estimation.value.tank_size || estimation.value.tank_size === 0) {
return 0
}
return (estimation.value.estimated_gallons / estimation.value.tank_size) * 100
}
const calculateDailyUsage = (): string => {
if (!estimation.value || !estimation.value.scaling_factor) {
return 'N/A'
}
// For a typical day with ~20 degree days (moderate winter day)
const typicalDegreeDays = 20
const dailyUsage = estimation.value.scaling_factor * typicalDegreeDays
return dailyUsage.toFixed(1)
}
const formatScalingFactor = (scalingFactor: number | null): string => {
if (scalingFactor === null || scalingFactor === undefined) {
return 'N/A'
}
return scalingFactor.toFixed(2)
}
const getScalingFactorCategory = (scalingFactor: number | null): string => {
if (scalingFactor === null || scalingFactor === undefined) {
return 'N/A'
}
if (scalingFactor < 0.05) {
return 'Very Low'
} else if (scalingFactor < 0.1) {
return 'Low'
} else if (scalingFactor < 0.2) {
return 'Normal'
} else if (scalingFactor < 0.5) {
return 'High'
} else {
return 'Very High'
}
}
const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A'
return dayjs(dateString).format('MMM D, YYYY')
}
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -33,6 +33,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { AuthorizeTransaction } from '../../../../types/models';
import DeliveriesTable from './DeliveriesTable.vue'; import DeliveriesTable from './DeliveriesTable.vue';
import ServiceCallsTable from './ServiceCallsTable.vue'; import ServiceCallsTable from './ServiceCallsTable.vue';
import TransactionsTable from './TransactionsTable.vue'; import TransactionsTable from './TransactionsTable.vue';
@@ -69,26 +70,12 @@ interface ServiceCall {
description: string; description: string;
} }
interface Transaction {
id: number;
preauthorize_amount: number | null;
charge_amount: number | null;
transaction_type: number;
status: number;
created_at: string;
auth_net_transaction_id: string | null;
rejection_reason: string | null;
delivery_id: number | null;
service_id: number | null;
auto_id: number | null;
}
// 2. Define the Props interface // 2. Define the Props interface
interface Props { interface Props {
deliveries: Delivery[]; deliveries: Delivery[];
autodeliveries: AutomaticDelivery[]; autodeliveries: AutomaticDelivery[];
serviceCalls: ServiceCall[]; serviceCalls: ServiceCall[];
transactions: Transaction[]; transactions: AuthorizeTransaction[];
} }
// 3. Use the typed defineProps // 3. Use the typed defineProps

View File

@@ -30,7 +30,7 @@
<div><strong>Pre-Auth:</strong> ${{ transaction.preauthorize_amount || '0.00' }}</div> <div><strong>Pre-Auth:</strong> ${{ transaction.preauthorize_amount || '0.00' }}</div>
<div><strong>Charge:</strong> ${{ transaction.charge_amount || '0.00' }}</div> <div><strong>Charge:</strong> ${{ transaction.charge_amount || '0.00' }}</div>
</div> </div>
<div><strong>Type:</strong> <span :class="getTypeColor(transaction.transaction_type)">{{ transaction.transaction_type === 0 ? 'Charge' : transaction.transaction_type === 1 ? 'Auth' : 'Capture' }}</span></div> <div><strong>Type:</strong> <span :class="getTypeColor(transaction.transaction_type)">{{ transaction.transaction_type === 0 ? 'Charge' : transaction.transaction_type === 1 ? 'Auth' : transaction.transaction_type === 2 ? 'Capture' : 'Unknown' }}</span></div>
</div> </div>
</td> </td>
<td> <td>
@@ -78,7 +78,7 @@
<p><strong>Transaction ID:</strong> {{ transaction.auth_net_transaction_id || 'N/A' }}</p> <p><strong>Transaction ID:</strong> {{ transaction.auth_net_transaction_id || 'N/A' }}</p>
<p><strong>Pre-Auth:</strong> ${{ transaction.preauthorize_amount || '0.00' }}</p> <p><strong>Pre-Auth:</strong> ${{ transaction.preauthorize_amount || '0.00' }}</p>
<p><strong>Charge:</strong> ${{ transaction.charge_amount || '0.00' }}</p> <p><strong>Charge:</strong> ${{ transaction.charge_amount || '0.00' }}</p>
<p><strong>Type:</strong> <span :class="getTypeColor(transaction.transaction_type)">{{ transaction.transaction_type === 0 ? 'Charge' : transaction.transaction_type === 1 ? 'Auth' : 'Capture' }}</span></p> <p><strong>Type:</strong> <span :class="getTypeColor(transaction.transaction_type)">{{ transaction.transaction_type === 0 ? 'Charge' : transaction.transaction_type === 1 ? 'Auth' : transaction.transaction_type === 2 ? 'Capture' : 'Unknown' }}</span></p>
<p><strong>Source:</strong> {{ getSourceText(transaction) }}</p> <p><strong>Source:</strong> {{ getSourceText(transaction) }}</p>
<!-- Rejection Reason in Mobile View --> <!-- 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 v-if="transaction.rejection_reason && transaction.rejection_reason.trim()" class="bg-transparent border border-gray-300 rounded-md p-3 mt-2">
@@ -103,29 +103,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
interface Transaction { import { AuthorizeTransaction } from '../../../../types/models';
id: number;
preauthorize_amount: number | null;
charge_amount: number | null;
transaction_type: number;
status: number;
created_at: string;
auth_net_transaction_id: string | null;
rejection_reason: string | null;
delivery_id: number | null;
service_id: number | null;
auto_id: number | null;
}
interface Props { interface Props {
transactions: Transaction[]; transactions: AuthorizeTransaction[];
} }
defineProps<Props>(); defineProps<Props>();
const getStatusClass = (status: number) => status === 0 ? 'badge-success' : 'badge-error'; const getStatusClass = (status: number) => status === 0 ? 'badge-success' : 'badge-error';
const getStatusText = (status: number) => status === 0 ? 'Approved' : 'Declined'; const getStatusText = (status: number) => status === 0 ? 'Approved' : 'Declined';
const getSourceText = (transaction: Transaction) => { const getSourceText = (transaction: AuthorizeTransaction) => {
if (transaction.auto_id) { if (transaction.auto_id) {
return 'Automatic'; return 'Automatic';
} else if (transaction.delivery_id) { } else if (transaction.delivery_id) {

View File

@@ -1,10 +1,10 @@
import CustomerHome from '../customer/home.vue'; const CustomerHome = () => import('../customer/home.vue');
import CustomerCreate from '../customer/create.vue'; const CustomerCreate = () => import('../customer/create.vue');
import CustomerEdit from "../customer/edit.vue"; const CustomerEdit = () => import("../customer/edit.vue");
import CustomerProfile from "./profile/profile.vue" const CustomerProfile = () => import("./profile/profile.vue")
import TankEdit from "./tank/edit.vue" const TankEdit = () => import("./tank/edit.vue")
const customerRoutes = [ const customerRoutes = [

View File

@@ -96,8 +96,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, PropType } from 'vue'; import { ref, watch } from 'vue'
// Interfaces remain the same // Interfaces remain the same
interface ServiceParts { interface ServiceParts {
@@ -116,92 +116,78 @@ interface NozzleParts {
part3: string; part3: string;
} }
export default defineComponent({ // Props
name: 'PartsEditModal', const props = defineProps<{
props: { existingParts: ServiceParts
existingParts: { customerId: number
type: Object as PropType<ServiceParts>, }>()
required: true
},
customerId: {
type: Number,
required: true
}
},
emits: ['close-modal', 'save-parts'],
data() { // Emits
return { const emit = defineEmits<{
// All reactive state goes inside the data object 'close-modal': []
editableParts: {} as Partial<ServiceParts>, 'save-parts': [parts: Partial<ServiceParts>]
nozzle1: { part1: '', part2: '', part3: '' } as NozzleParts, }>()
nozzle2: { part1: '', part2: '', part3: '' } as NozzleParts,
// ======================================================
// ============== THE UPDATED OIL FILTER LIST ==============
// ======================================================
oilFilterOptions: [
'RF-1',
'RF-4',
'88CR',
'Big Guy f80-24',
'Garber Model R',
'Garber Model M',
'PurePro f100-10W'
],
nozzlePart1Options: ['a', 'b', 'w'],
nozzlePart2Options: Array.from({ length: (200 - 45) / 5 + 1 }, (_, i) => (45 + i * 5).toString()),
nozzlePart3Options: Array.from({ length: (100 - 45) / 5 + 1 }, (_, i) => (45 + i * 5).toString()),
};
},
watch: { // Reactive data
// Watch for changes to the `existingParts` prop const editableParts = ref({} as Partial<ServiceParts>)
existingParts: { const nozzle1 = ref({ part1: '', part2: '', part3: '' } as NozzleParts)
handler(newVal) { const nozzle2 = ref({ part1: '', part2: '', part3: '' } as NozzleParts)
if (!newVal) return;
// Update the component's internal data when the prop changes
this.editableParts = { ...newVal, customer_id: this.customerId };
this.nozzle1 = this.splitNozzle(newVal.oil_nozzle);
this.nozzle2 = this.splitNozzle(newVal.oil_nozzle_2);
},
immediate: true, // Run the handler immediately when the component is created
deep: true, // Watch for nested changes within the object
},
},
methods: { // ======================================================
// Function to split a combined nozzle string (e.g., "a 55 75") into its parts // ============== THE UPDATED OIL FILTER LIST ==============
splitNozzle(nozzleStr: string | undefined): NozzleParts { // ======================================================
if (!nozzleStr || typeof nozzleStr !== 'string' || nozzleStr.trim() === '') { const oilFilterOptions = ref([
return { part1: '', part2: '', part3: '' }; 'RF-1',
} 'RF-4',
const parts = nozzleStr.split(' '); '88CR',
return { 'Big Guy f80-24',
part1: parts[0] || '', 'Garber Model R',
part2: parts[1] || '', 'Garber Model M',
part3: parts[2] || '', 'PurePro f100-10W'
}; ])
},
// Function to combine nozzle parts into a single string const nozzlePart1Options = ref(['a', 'b', 'w'])
combineNozzle(parts: NozzleParts): string { const nozzlePart2Options = ref(Array.from({ length: (200 - 45) / 5 + 1 }, (_, i) => (45 + i * 5).toString()))
if (parts.part1 && parts.part2 && parts.part3) { const nozzlePart3Options = ref(Array.from({ length: (100 - 45) / 5 + 1 }, (_, i) => (45 + i * 5).toString()))
return `${parts.part1} ${parts.part2} ${parts.part3}`;
}
return ''; // Return empty if any part is missing
},
// Form submission handler // Watchers
saveChanges() { watch(() => props.existingParts, (newVal) => {
// Before emitting, combine the nozzle parts back into a single string if (!newVal) return;
this.editableParts.oil_nozzle = this.combineNozzle(this.nozzle1); // Update the component's internal data when the prop changes
this.editableParts.oil_nozzle_2 = this.combineNozzle(this.nozzle2); editableParts.value = { ...newVal, customer_id: props.customerId };
nozzle1.value = splitNozzle(newVal.oil_nozzle);
// Emit the save event with the final payload nozzle2.value = splitNozzle(newVal.oil_nozzle_2);
this.$emit('save-parts', this.editableParts); }, { immediate: true, deep: true })
},
}, // Functions
}); // Function to split a combined nozzle string (e.g., "a 55 75") into its parts
const splitNozzle = (nozzleStr: string | undefined): NozzleParts => {
if (!nozzleStr || typeof nozzleStr !== 'string' || nozzleStr.trim() === '') {
return { part1: '', part2: '', part3: '' };
}
const parts = nozzleStr.split(' ');
return {
part1: parts[0] || '',
part2: parts[1] || '',
part3: parts[2] || '',
};
}
// Function to combine nozzle parts into a single string
const combineNozzle = (parts: NozzleParts): string => {
if (parts.part1 && parts.part2 && parts.part3) {
return `${parts.part1} ${parts.part2} ${parts.part3}`;
}
return ''; // Return empty if any part is missing
}
// Form submission handler
const saveChanges = () => {
// Before emitting, combine the nozzle parts back into a single string
editableParts.value.oil_nozzle = combineNozzle(nozzle1.value);
editableParts.value.oil_nozzle_2 = combineNozzle(nozzle2.value);
// Emit the save event with the final payload
emit('save-parts', editableParts.value);
}
</script>``` </script>```

View File

@@ -85,8 +85,9 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import Footer from '../../../layouts/footers/footer.vue' import Footer from '../../../layouts/footers/footer.vue'
@@ -100,88 +101,89 @@ interface TankFormData {
fill_location: string; fill_location: string;
} }
export default defineComponent({ const route = useRoute()
name: 'TankEdit', const router = useRouter()
components: {
Footer, // Reactive data
}, const user = ref(null as any)
data() { const customer = ref({} as any)
return { // --- REFACTORED: Simplified, flat form object ---
user: null as any, const TankForm = ref({
customer: {} as any, last_tank_inspection: null,
// --- REFACTORED: Simplified, flat form object --- tank_status: true,
TankForm: { outside_or_inside: true,
last_tank_inspection: null, tank_size: 0,
tank_status: true, fill_location: '',
outside_or_inside: true, } as TankFormData)
tank_size: 0,
fill_location: '', // Functions
} as TankFormData, const userStatus = () => {
} const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
}, axios.get(path, { withCredentials: true, headers: authHeader() })
created() { .then((response: any) => {
this.userStatus(); if (response.data.ok) {
const customerId = this.$route.params.id; user.value = response.data.user;
this.getCustomer(customerId); }
this.getCustomerDescription(customerId); })
this.getTank(customerId); .catch(() => { user.value = null; });
}, }
methods: {
userStatus() { const getCustomer = (userid: any) => {
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami'; const path = `${import.meta.env.VITE_BASE_URL}/customer/${userid}`;
axios.get(path, { withCredentials: true, headers: authHeader() }) axios.get(path, { headers: authHeader() })
.then((response: any) => { .then((response: any) => {
if (response.data.ok) { customer.value = response.data;
this.user = response.data.user; });
} }
})
.catch(() => { this.user = null; }); const getCustomerDescription = (userid: any) => {
}, const path = `${import.meta.env.VITE_BASE_URL}/customer/description/${userid}`;
getCustomer(userid: any) { axios.get(path, { headers: authHeader() })
const path = `${import.meta.env.VITE_BASE_URL}/customer/${userid}`; .then((response: any) => {
axios.get(path, { headers: authHeader() }) // Only update fill_location if the response has it
.then((response: any) => { if (response.data && response.data.fill_location) {
this.customer = response.data; TankForm.value.fill_location = response.data.fill_location;
}); }
}, });
getCustomerDescription(userid: any) { }
const path = `${import.meta.env.VITE_BASE_URL}/customer/description/${userid}`;
axios.get(path, { headers: authHeader() }) const getTank = (customer_id: any) => {
.then((response: any) => { const path = `${import.meta.env.VITE_BASE_URL}/customer/tank/${customer_id}`;
// Only update fill_location if the response has it axios.get(path, { withCredentials: true, headers: authHeader() })
if (response.data && response.data.fill_location) { .then((response: any) => {
this.TankForm.fill_location = response.data.fill_location; if (response.data) {
} // Update the form model with data from the tank endpoint
}); TankForm.value.last_tank_inspection = response.data.last_tank_inspection;
}, TankForm.value.tank_status = response.data.tank_status;
getTank(customer_id: any) { TankForm.value.outside_or_inside = response.data.outside_or_inside;
const path = `${import.meta.env.VITE_BASE_URL}/customer/tank/${customer_id}`; TankForm.value.tank_size = response.data.tank_size;
axios.get(path, { withCredentials: true, headers: authHeader() }) }
.then((response: any) => { });
if (response.data) { }
// Update the form model with data from the tank endpoint
this.TankForm.last_tank_inspection = response.data.last_tank_inspection; const editTank = (payload: TankFormData) => {
this.TankForm.tank_status = response.data.tank_status; const path = `${import.meta.env.VITE_BASE_URL}/customer/edit/tank/${route.params.id}`;
this.TankForm.outside_or_inside = response.data.outside_or_inside; axios.put(path, payload, { withCredentials: true, headers: authHeader() })
this.TankForm.tank_size = response.data.tank_size; .then((response: any) => {
} if (response.data.ok) {
}); router.push({ name: "customerProfile", params: { id: customer.value.id } });
}, } else {
editTank(payload: TankFormData) { console.error("Failed to edit tank:", response.data.error);
const path = `${import.meta.env.VITE_BASE_URL}/customer/edit/tank/${this.$route.params.id}`; }
axios.put(path, payload, { withCredentials: true, headers: authHeader() }) });
.then((response: any) => { }
if (response.data.ok) {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } }); const onSubmit = () => {
} else { // The payload is simply the entire form object now
console.error("Failed to edit tank:", response.data.error); editTank(TankForm.value);
} }
});
}, // Lifecycle
onSubmit() { onMounted(() => {
// The payload is simply the entire form object now userStatus();
this.editTank(this.TankForm); const customerId = route.params.id;
}, getCustomer(customerId);
}, getCustomerDescription(customerId);
getTank(customerId);
}) })
</script> </script>

View File

@@ -229,7 +229,7 @@
<div class="font-bold text-sm">{{ card.name_on_card }}</div> <div class="font-bold text-sm">{{ card.name_on_card }}</div>
<div class="text-sm">{{ card.type_of_card }}</div> <div class="text-sm">{{ card.type_of_card }}</div>
<div class="text-sm">{{ card.card_number }}</div> <div class="text-sm">{{ card.card_number }}</div>
<p class="text-sm">Exp: <span v-if="card.expiration_month < 10">0</span>{{ card.expiration_month }} / {{ card.expiration_year }}</p> <p class="text-sm">Exp: <span v-if="Number(card.expiration_month) < 10">0</span>{{ card.expiration_month }} / {{ card.expiration_year }}</p>
<p class="text-sm">CVV: {{ card.security_number }}</p> <p class="text-sm">CVV: {{ card.security_number }}</p>
</div> </div>
<div class="flex justify-end gap-2 mt-2"> <div class="flex justify-end gap-2 mt-2">
@@ -305,43 +305,21 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { Customer, CreditCard, CreateCardRequest } from '../../types/models'
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 useValidate from "@vuelidate/core"; import { useVuelidate } from "@vuelidate/core";
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import { minLength, required, requiredIf } from "@vuelidate/validators"; import { minLength, required, requiredIf } from "@vuelidate/validators";
// --- TYPE DEFINITIONS (MODIFIED) --- // --- TYPE DEFINITIONS (MODIFIED) ---
interface SimpleResponse<T> { data: T; } interface SimpleResponse<T> { data: T; }
interface Customer {
id: number;
customer_last_name: string;
customer_first_name: string;
customer_town: string;
customer_state: number;
customer_zip: string;
customer_phone_number: string;
customer_home_type: number;
customer_apt: string;
customer_address: string;
account_number: string;
}
// This interface now reflects the full card data from the backend
interface UserCard {
id: number;
last_four_digits?: number;
type_of_card: string;
expiration_month: number;
expiration_year: number;
name_on_card: string;
card_number: string;
security_number: string;
}
interface Promo { id: number; name_of_promotion: string; money_off_delivery: number; } interface Promo { id: number; name_of_promotion: string; money_off_delivery: number; }
interface Driver { id: number; employee_first_name: string; employee_last_name: string; } interface Driver { id: number; employee_first_name: string; employee_last_name: string; }
interface PricingTier { gallons: number | string; price: number | string; } interface PricingTier { gallons: number | string; price: number | string; }
@@ -370,363 +348,384 @@ interface CardFormData {
type_of_card: string; type_of_card: string;
} }
export default defineComponent({ const router = useRouter()
name: 'deliveryCreate', const route = useRoute()
components: { Header, SideBar, Footer },
data() {
return {
v$: useValidate(),
user: null as any,
customer: {} as any,
isLoading: false, // For main form submission
isCardSaving: false, // For quick add card form
quickGallonAmounts: [100, 125, 150, 175, 200, 220],
userCards: [] as UserCard[],
promos: [] as Promo[],
truckDriversList: [] as Driver[],
pricingTiers: [] as PricingTier[],
isLoadingAuthorize: true,
authorizeCheck: { profile_exists: false, has_payment_methods: false, missing_components: [] as string[], valid_for_charging: false },
isConfirmationModalVisible: false,
formDelivery: {
gallons_ordered: '',
customer_asked_for_fill: false,
expected_delivery_date: '',
dispatcher_notes_taken: '',
prime: false,
emergency: false,
same_day: false,
credit: false,
cash: false,
check: false,
other: false,
credit_card_id: 0,
promo_id: 0,
driver_employee_id: 0,
} as DeliveryFormData,
// Simplified formCard data
formCard: {
card_number: '',
expiration_month: '',
expiration_year: '',
security_number: '',
card_name: '',
type_of_card: '',
} as CardFormData,
} // Reactive data
const user = ref(null as any)
const customer = ref({} as any)
const isLoading = ref(false) // For main form submission
const isCardSaving = ref(false) // For quick add card form
const quickGallonAmounts = ref([100, 125, 150, 175, 200, 220])
const userCards = ref<CreditCard[]>([])
const promos = ref([] as Promo[])
const truckDriversList = ref([] as Driver[])
const pricingTiers = ref([] as PricingTier[])
const isLoadingAuthorize = ref(true)
const authorizeCheck = ref({ profile_exists: false, has_payment_methods: false, missing_components: [] as string[], valid_for_charging: false })
const isConfirmationModalVisible = ref(false)
const formDelivery = ref({
gallons_ordered: '',
customer_asked_for_fill: false,
expected_delivery_date: '',
dispatcher_notes_taken: '',
prime: false,
emergency: false,
same_day: false,
credit: false,
cash: false,
check: false,
other: false,
credit_card_id: 0,
promo_id: 0,
driver_employee_id: 0,
} as DeliveryFormData)
// Simplified formCard data
const formCard = ref({
card_number: '',
expiration_month: '',
expiration_year: '',
security_number: '',
card_name: '',
type_of_card: '',
} as CardFormData)
// Computed
const preAuthAmount = computed((): number => {
if (!formDelivery.value.credit || formDelivery.value.customer_asked_for_fill) return 0;
const gallons = Number(formDelivery.value.gallons_ordered);
if (isNaN(gallons) || gallons <= 0 || pricingTiers.value.length === 0) return 0;
// Find the correct price tier. Assumes tiers are for total price, not price/gallon.
let priceForGallons = 0;
const sortedTiers = [...pricingTiers.value].sort((a, b) => Number(a.gallons) - Number(b.gallons));
// Find the highest tier that is less than or equal to the gallons ordered
let applicableTier = sortedTiers.filter(t => gallons >= Number(t.gallons)).pop();
if (applicableTier) {
const pricePerGallon = Number(applicableTier.price) / Number(applicableTier.gallons);
priceForGallons = gallons * pricePerGallon;
} else if (sortedTiers.length > 0) {
// Fallback to the lowest tier's price/gallon if no tier is met (e.g., ordering 50 gallons when lowest tier is 100)
const lowestTier = sortedTiers[0];
const pricePerGallon = Number(lowestTier.price) / Number(lowestTier.gallons);
priceForGallons = gallons * pricePerGallon;
}
return priceForGallons;
})
const customerStateName = computed((): string => {
const states: Record<number, string> = { 0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York' };
return states[customer.value.customer_state] || 'Unknown state';
})
const customerHomeTypeName = computed((): string => {
const types: Record<number, string> = { 0: 'Residential', 1: 'apartment', 2: 'condo', 3: 'commercial', 4: 'business', 5: 'construction', 6: 'container' };
return types[customer.value.customer_home_type] || 'Unknown type';
})
const isAnyPaymentMethodSelected = computed((): boolean => {
return !!(formDelivery.value?.credit || formDelivery.value?.cash || formDelivery.value?.check || formDelivery.value?.other);
})
const selectedGallonsAmount = computed((): number => {
const value = formDelivery.value.gallons_ordered ?? '';
return Number(value);
})
// Validations
const validations = {
formDelivery: {
gallons_ordered: {
required: requiredIf(() => !formDelivery.value.customer_asked_for_fill),
minValue: (value: string) => {
if (formDelivery.value.customer_asked_for_fill) return true;
if (!value) return true;
const num = parseInt(value, 10);
return num >= 1;
}
},
expected_delivery_date: { required },
driver_employee_id: { required },
credit_card_id: {
creditCardRequired: (value: number) => {
if (formDelivery.value.credit) { return value !== 0; }
return true;
}
},
}, },
validations() { isAnyPaymentMethodSelected: { mustBeTrue: (value: boolean) => value === true, },
return { // Simplified validations for quick add card
formDelivery: { formCard: {
gallons_ordered: { card_name: { required, minLength: minLength(1) },
required: requiredIf(function(this: any) { return !this.formDelivery.customer_asked_for_fill; }), security_number: { required, minLength: minLength(1) },
minValue: function(this: any, value: string) { type_of_card: { required },
if (this.formDelivery.customer_asked_for_fill) return true; card_number: { required, minLength: minLength(1) },
if (!value) return true; expiration_month: { required },
const num = parseInt(value, 10); expiration_year: { required },
return num >= 1; },
} }
},
expected_delivery_date: { required }, const v$ = useVuelidate(validations, { formDelivery, formCard, isAnyPaymentMethodSelected })
driver_employee_id: { required },
credit_card_id: { // Functions
creditCardRequired: function(this: any, value: number) { const setGallons = (amount: number) => {
if (this.formDelivery.credit) { return value !== 0; } formDelivery.value.gallons_ordered = String(amount);
return true; formDelivery.value.customer_asked_for_fill = false;
} }
},
}, const selectCreditCard = (cardId: number) => {
isAnyPaymentMethodSelected: { mustBeTrue: (value: boolean) => value === true, }, formDelivery.value.credit_card_id = cardId;
// Simplified validations for quick add card }
formCard: {
card_name: { required, minLength: minLength(1) }, const setDeliveryDate = (days: number) => {
security_number: { required, minLength: minLength(1) }, const date = new Date();
type_of_card: { required }, date.setDate(date.getDate() + days);
card_number: { required, minLength: minLength(1) }, formDelivery.value.expected_delivery_date = date.toISOString().split('T')[0];
expiration_month: { required }, }
expiration_year: { required },
}, const isDeliveryDateSelected = (days: number): boolean => {
const date = new Date();
date.setDate(date.getDate() + days);
return formDelivery.value.expected_delivery_date === date.toISOString().split('T')[0];
}
const isPricingTierSelected = (tierGallons: number | string): boolean => {
if (!formDelivery.value.gallons_ordered) return false;
const selectedGallons = Number(formDelivery.value.gallons_ordered);
if (isNaN(selectedGallons)) return false;
const tierNum = Number(tierGallons);
if (isNaN(tierNum)) return false;
return selectedGallons === tierNum;
}
const getPricingTiers = () => {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
.then((response: SimpleResponse<{ [key: string]: string }>) => {
pricingTiers.value = Object.entries(response.data).map(([gallons, price]) => ({ gallons: parseInt(gallons, 10), price: price }));
})
.catch(() => notify({ title: "Pricing Error", text: "Could not retrieve today's pricing.", type: "error" }));
}
const getCustomer = (user_id: string | number | string[]) => {
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
axios({ method: "get", url: path, withCredentials: true })
.then((response: SimpleResponse<Customer>) => { customer.value = response.data; })
.catch(() => notify({ title: "Error", text: "Could not find customer", type: "error" }));
}
const getPaymentCards = (user_id: string | number | string[]) => {
// IMPORTANT: This endpoint points to the Flask API that returns the secure card list.
let path = `${import.meta.env.VITE_BASE_URL}/payment/cards/${user_id}`;
return axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: SimpleResponse<CreditCard[]>) => { userCards.value = response.data; })
.catch(() => { userCards.value = []; }); // Clear cards on error
}
const getPromos = () => {
let path = import.meta.env.VITE_BASE_URL + "/promo/all";
axios({ method: "get", url: path, withCredentials: true })
.then((response: SimpleResponse<Promo[]>) => {
promos.value = response.data;
})
.catch(() => { /* empty */ });
}
const getDriversList = () => {
let path = import.meta.env.VITE_BASE_URL + "/employee/drivers";
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
.then((response: SimpleResponse<Driver[]>) => {
truckDriversList.value = response.data;
})
.catch(() => { /* empty */ });
}
const editCard = (card_id: number) => {
router.push({ name: "cardedit", params: { id: card_id } });
}
const removeCard = (card_id: number) => {
if (window.confirm("Are you sure you want to remove this card?")) {
// You will need a new backend endpoint for this: DELETE /payments/customers/{customer_id}/cards/{card_id}
let path = `${import.meta.env.VITE_BASE_URL}/payment/card/remove/${card_id}`; // Keep old path or update to new
axios.delete(path, { headers: authHeader() })
.then(() => {
notify({ title: "Card Removed", type: "success" });
getPaymentCards(customer.value.id);
})
.catch(() => {
notify({ title: "Error", text: "Could not remove card.", type: "error" });
});
}
}
const onDeliverySubmit = async () => {
const isFormValid = await v$.value.formDelivery.$validate();
if (!isFormValid) {
notify({ title: "Form Incomplete", text: "Please review the fields marked in red.", type: "warn" });
return;
}
if (formDelivery.value.cash || formDelivery.value.check) {
isConfirmationModalVisible.value = true;
} else {
proceedWithSubmission();
}
}
const proceedWithSubmission = async () => {
isConfirmationModalVisible.value = false;
isLoading.value = true;
// Step 1: Create the delivery order record
const createDeliveryPath = `${import.meta.env.VITE_BASE_URL}/delivery/create/${customer.value.id}`;
try {
const deliveryResponse = await axios.post(createDeliveryPath, formDelivery.value, { withCredentials: true, headers: authHeader() });
const deliveryData = deliveryResponse.data;
if (!deliveryData.ok || !deliveryData.delivery_id) {
throw new Error(deliveryData.error || "Failed to create delivery record.");
}
// Delivery created successfully - redirect to payment page
notify({
title: "Success!",
text: `Delivery #${deliveryData.delivery_id} created. ${
formDelivery.value.credit
? "Redirecting to payment page."
: ""
}`,
type: "success"
});
router.push({ name: "payOil", params: { id: deliveryData.delivery_id } });
} catch (error: any) {
const errorMessage = error.response?.data?.detail || "An error occurred during submission.";
notify({ title: "Submission Error", text: errorMessage, type: "error" });
} finally {
isLoading.value = false;
}
}
const checkAuthorizeAccount = async () => {
if (!customer.value.id) return;
isLoadingAuthorize.value = true;
try {
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/user/check-authorize-account/${customer.value.id}`;
const response = await axios.get(path, { headers: authHeader() });
authorizeCheck.value = response.data;
} catch (error) {
console.error("Failed to check authorize account:", error);
notify({ title: "Error", text: "Could not check payment account status.", type: "error" });
// Set default error state
authorizeCheck.value = {
profile_exists: false,
has_payment_methods: false,
missing_components: ['api_error'],
valid_for_charging: false
}; };
}, } finally {
computed: { isLoadingAuthorize.value = false;
// New computed property to estimate pre-auth amount }
preAuthAmount(): number { }
if (!this.formDelivery.credit || this.formDelivery.customer_asked_for_fill) return 0;
const gallons = Number(this.formDelivery.gallons_ordered);
if (isNaN(gallons) || gallons <= 0 || this.pricingTiers.length === 0) return 0;
// Find the correct price tier. Assumes tiers are for total price, not price/gallon. const onCardSubmit = async () => {
let priceForGallons = 0; v$.value.formCard.$validate();
const sortedTiers = [...this.pricingTiers].sort((a, b) => Number(a.gallons) - Number(b.gallons)); if (v$.value.formCard.$error) {
notify({ title: "Validation Error", text: "Please fill out all required fields.", type: "error" });
// Find the highest tier that is less than or equal to the gallons ordered return;
let applicableTier = sortedTiers.filter(t => gallons >= Number(t.gallons)).pop(); }
isCardSaving.value = true;
if (applicableTier) {
const pricePerGallon = Number(applicableTier.price) / Number(applicableTier.gallons);
priceForGallons = gallons * pricePerGallon;
} else if (sortedTiers.length > 0) {
// Fallback to the lowest tier's price/gallon if no tier is met (e.g., ordering 50 gallons when lowest tier is 100)
const lowestTier = sortedTiers[0];
const pricePerGallon = Number(lowestTier.price) / Number(lowestTier.gallons);
priceForGallons = gallons * pricePerGallon;
}
return priceForGallons; // --- STEP 1: PREPARE PAYLOADS FOR BOTH SERVICES ---
}, // Payload for Flask backend (it takes all the raw details for your DB)
customerStateName(): string { const flaskPayload = {
const states: Record<number, string> = { 0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York' }; card_number: formCard.value.card_number,
return states[this.customer.customer_state] || 'Unknown state'; expiration_month: formCard.value.expiration_month,
}, expiration_year: formCard.value.expiration_year,
customerHomeTypeName(): string { type_of_card: formCard.value.type_of_card,
const types: Record<number, string> = { 0: 'Residential', 1: 'apartment', 2: 'condo', 3: 'commercial', 4: 'business', 5: 'construction', 6: 'container' }; security_number: formCard.value.security_number, // Flask expects 'security_number'
return types[this.customer.customer_home_type] || 'Unknown type'; main_card: false,
}, name_on_card: formCard.value.card_name, // Map card_name to name_on_card for Flask
isAnyPaymentMethodSelected(): boolean { };
return !!(this.formDelivery?.credit || this.formDelivery?.cash || this.formDelivery?.check || this.formDelivery?.other);
}, // --- STEP 2: CRITICAL CALL - SAVE CARD TO LOCAL DATABASE VIA FLASK ---
selectedGallonsAmount(): number { try {
const value = this.formDelivery.gallons_ordered ?? ''; const flaskPath = `${import.meta.env.VITE_BASE_URL}/payment/card/create/${customer.value.id}`;
return Number(value); console.log("Attempting to save card to local DB via Flask:", flaskPath);
const flaskResponse = await axios.post(flaskPath, flaskPayload, { withCredentials: true, headers: authHeader() });
if (!flaskResponse.data.ok) {
// If the primary save fails, stop everything and show an error.
throw new Error(flaskResponse.data.error || "Failed to save card.");
} }
}, console.log("Card successfully saved to local database via Flask.");
created() {
this.getDriversList()
this.getPromos()
this.getPricingTiers()
},
watch: {
$route() {
const customerId = this.$route.params.id;
this.getCustomer(customerId);
this.getPaymentCards(customerId);
},
},
mounted() {
const customerId = this.$route.params.id;
this.getCustomer(customerId)
this.getPaymentCards(customerId);
},
methods: {
setGallons(amount: number) { this.formDelivery.gallons_ordered = String(amount); this.formDelivery.customer_asked_for_fill = false; },
selectCreditCard(cardId: number) { this.formDelivery.credit_card_id = cardId; },
setDeliveryDate(days: number) { const date = new Date(); date.setDate(date.getDate() + days); 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;
return selectedGallons === tierNum;
},
getPricingTiers() {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
.then((response: SimpleResponse<{ [key: string]: string }>) => {
this.pricingTiers = Object.entries(response.data).map(([gallons, price]) => ({ gallons: parseInt(gallons, 10), price: price }));
})
.catch(() => notify({ title: "Pricing Error", text: "Could not retrieve today's pricing.", type: "error" }));
},
getCustomer(user_id: string | number | string[]) {
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
axios({ method: "get", url: path, withCredentials: true })
.then((response: SimpleResponse<Customer>) => { this.customer = response.data; })
.catch(() => notify({ title: "Error", text: "Could not find customer", type: "error" }));
},
getPaymentCards(user_id: string | number | string[]) {
// IMPORTANT: This endpoint points to the Flask API that returns the secure card list.
let path = `${import.meta.env.VITE_BASE_URL}/payment/cards/${user_id}`;
return axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: SimpleResponse<UserCard[]>) => { this.userCards = response.data; })
.catch(() => { this.userCards = []; }); // Clear cards on error
},
getPromos() {
let path = import.meta.env.VITE_BASE_URL + "/promo/all";
axios({ method: "get", url: path, withCredentials: true })
.then((response: SimpleResponse<Promo[]>) => {
this.promos = response.data;
})
.catch(() => { /* empty */ });
},
getDriversList() {
let path = import.meta.env.VITE_BASE_URL + "/employee/drivers";
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
.then((response: SimpleResponse<Driver[]>) => {
this.truckDriversList = response.data;
})
.catch(() => { /* empty */ });
},
editCard(card_id: number) { } catch (error: any) {
this.$router.push({ name: "cardedit", params: { id: card_id } }); const errorMessage = error.response?.data?.error || "A critical error occurred while saving the card.";
}, notify({ title: "Error", text: errorMessage, type: "error" });
isCardSaving.value = false; // Stop loading spinner
return; // End the function here
}
removeCard(card_id: number) { // --- STEP 3: BEST-EFFORT CALL - TOKENIZE CARD VIA FASTAPI ---
if (window.confirm("Are you sure you want to remove this card?")) { if (authorizeCheck.value.profile_exists) {
// You will need a new backend endpoint for this: DELETE /payments/customers/{customer_id}/cards/{card_id} // Payload for FastAPI backend (it only needs the essentials for Authorize.Net)
let path = `${import.meta.env.VITE_BASE_URL}/payment/card/remove/${card_id}`; // Keep old path or update to new const fastapiPayload = {
axios.delete(path, { headers: authHeader() }) card_number: formCard.value.card_number.replace(/\s/g, ''),
.then(() => { expiration_date: `${formCard.value.expiration_year}-${formCard.value.expiration_month}`,
notify({ title: "Card Removed", type: "success" }); cvv: formCard.value.security_number, // Map security_number to cvv for FastAPI
this.getPaymentCards(this.customer.id); main_card: false, // Send this to FastAPI as well
}) };
.catch(() => {
notify({ title: "Error", text: "Could not remove card.", type: "error" });
});
}
},
async onDeliverySubmit() { try {
const isFormValid = await this.v$.formDelivery.$validate(); const fastapiPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/customers/${customer.value.id}/cards`;
if (!isFormValid) { console.log("Attempting to tokenize card with Authorize.Net via FastAPI:", fastapiPath);
notify({ title: "Form Incomplete", text: "Please review the fields marked in red.", type: "warn" }); await axios.post(fastapiPath, fastapiPayload, { withCredentials: true, headers: authHeader() });
return; console.log("Card successfully tokenized with Authorize.Net via FastAPI.");
} } catch (error: any) {
if (this.formDelivery.cash || this.formDelivery.check) { // If this fails, we just log it for the developers. We DON'T show an error to the user.
this.isConfirmationModalVisible = true; console.warn("NON-CRITICAL-ERROR: Tokenization with Authorize.Net failed, but the card was saved locally.", error.response?.data || error.message);
} else { }
this.proceedWithSubmission(); }else{
} console.log("Skipping Authorize.Net tokenization as no profile exists for customer.");
}, }
// --- STEP 4: ALWAYS SHOW SUCCESS, REFRESH CARDS, STAY ON PAGE ---
async proceedWithSubmission() { // This code runs as long as the first (Flask) call was successful.
this.isConfirmationModalVisible = false; notify({ type: 'success', title: 'Card Saved!' });
this.isLoading = true;
// Step 1: Create the delivery order record // Refresh the card list and try to auto-select if possible
const createDeliveryPath = `${import.meta.env.VITE_BASE_URL}/delivery/create/${this.customer.id}`; await getPaymentCards(customer.value.id);
if (userCards.value.length > 0) {
try { formDelivery.value.credit = true;
const deliveryResponse = await axios.post(createDeliveryPath, this.formDelivery, { withCredentials: true, headers: authHeader() }); formDelivery.value.credit_card_id = userCards.value[userCards.value.length - 1].id;
const deliveryData = deliveryResponse.data; }
if (!deliveryData.ok || !deliveryData.delivery_id) { // Reset the quick add form
throw new Error(deliveryData.error || "Failed to create delivery record."); Object.assign(formCard.value, { card_number: '', expiration_month: '', expiration_year: '', security_number: '', card_name: '', type_of_card: '' });
} v$.value.formCard.$reset();
isCardSaving.value = false;
// Delivery created successfully - redirect to payment page }
notify({
title: "Success!",
text: `Delivery #${deliveryData.delivery_id} created. ${
this.formDelivery.credit
? "Redirecting to payment page."
: ""
}`,
type: "success"
});
this.$router.push({ name: "payOil", params: { id: deliveryData.delivery_id } });
} catch (error: any) { // Watchers
const errorMessage = error.response?.data?.detail || "An error occurred during submission."; watch(route, () => {
notify({ title: "Submission Error", text: errorMessage, type: "error" }); const customerId = route.params.id;
} finally { getCustomer(customerId);
this.isLoading = false; getPaymentCards(customerId);
} })
},
async checkAuthorizeAccount() {
if (!this.customer.id) return;
this.isLoadingAuthorize = true; // Lifecycle
onMounted(() => {
try { getDriversList()
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/user/check-authorize-account/${this.customer.id}`; getPromos()
const response = await axios.get(path, { headers: authHeader() }); getPricingTiers()
this.authorizeCheck = response.data; const customerId = route.params.id;
} catch (error) { getCustomer(customerId)
console.error("Failed to check authorize account:", error); getPaymentCards(customerId);
notify({ title: "Error", text: "Could not check payment account status.", type: "error" });
// Set default error state
this.authorizeCheck = {
profile_exists: false,
has_payment_methods: false,
missing_components: ['api_error'],
valid_for_charging: false
};
} finally {
this.isLoadingAuthorize = false;
}
},
async onCardSubmit() {
this.v$.formCard.$validate();
if (this.v$.formCard.$error) {
notify({ title: "Validation Error", text: "Please fill out all required fields.", type: "error" });
return;
}
this.isCardSaving = true;
// --- STEP 1: PREPARE PAYLOADS FOR BOTH SERVICES ---
// Payload for Flask backend (it takes all the raw details for your DB)
const flaskPayload = {
card_number: this.formCard.card_number,
expiration_month: this.formCard.expiration_month,
expiration_year: this.formCard.expiration_year,
type_of_card: this.formCard.type_of_card,
security_number: this.formCard.security_number, // Flask expects 'security_number'
main_card: false,
name_on_card: this.formCard.card_name, // Map card_name to name_on_card for Flask
};
// --- STEP 2: CRITICAL CALL - SAVE CARD TO LOCAL DATABASE VIA FLASK ---
try {
const flaskPath = `${import.meta.env.VITE_BASE_URL}/payment/card/create/${this.customer.id}`;
console.log("Attempting to save card to local DB via Flask:", flaskPath);
const flaskResponse = await axios.post(flaskPath, flaskPayload, { withCredentials: true, headers: authHeader() });
if (!flaskResponse.data.ok) {
// If the primary save fails, stop everything and show an error.
throw new Error(flaskResponse.data.error || "Failed to save card.");
}
console.log("Card successfully saved to local database via Flask.");
} catch (error: any) {
const errorMessage = error.response?.data?.error || "A critical error occurred while saving the card.";
notify({ title: "Error", text: errorMessage, type: "error" });
this.isCardSaving = false; // Stop loading spinner
return; // End the function here
}
// --- STEP 3: BEST-EFFORT CALL - TOKENIZE CARD VIA FASTAPI ---
if (this.authorizeCheck.profile_exists) {
// Payload for FastAPI backend (it only needs the essentials for Authorize.Net)
const fastapiPayload = {
card_number: this.formCard.card_number.replace(/\s/g, ''),
expiration_date: `${this.formCard.expiration_year}-${this.formCard.expiration_month}`,
cvv: this.formCard.security_number, // Map security_number to cvv for FastAPI
main_card: false, // Send this to FastAPI as well
};
try {
const fastapiPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/customers/${this.customer.id}/cards`;
console.log("Attempting to tokenize card with Authorize.Net via FastAPI:", fastapiPath);
await axios.post(fastapiPath, fastapiPayload, { withCredentials: true, headers: authHeader() });
console.log("Card successfully tokenized with Authorize.Net via FastAPI.");
} catch (error: any) {
// If this fails, we just log it for the developers. We DON'T show an error to the user.
console.warn("NON-CRITICAL-ERROR: Tokenization with Authorize.Net failed, but the card was saved locally.", error.response?.data || error.message);
}
}else{
console.log("Skipping Authorize.Net tokenization as no profile exists for customer.");
}
// --- STEP 4: ALWAYS SHOW SUCCESS, REFRESH CARDS, STAY ON PAGE ---
// This code runs as long as the first (Flask) call was successful.
notify({ type: 'success', title: 'Card Saved!' });
// Refresh the card list and try to auto-select if possible
await this.getPaymentCards(this.customer.id);
if (this.userCards.length > 0) {
this.formDelivery.credit = true;
this.formDelivery.credit_card_id = this.userCards[this.userCards.length - 1].id;
}
// Reset the quick add form
Object.assign(this.formCard, { card_number: '', expiration_month: '', expiration_year: '', security_number: '', card_name: '', type_of_card: '' });
this.v$.formCard.$reset();
this.isCardSaving = false;
},
},
}) })
</script> </script>

View File

@@ -223,7 +223,7 @@
</div> </div>
<div class="mt-2 text-sm font-mono tracking-wider"> <div class="mt-2 text-sm font-mono tracking-wider">
<p>{{ card.card_number }}</p> <p>{{ card.card_number }}</p>
<p>Exp: <span v-if="card.expiration_month < 10">0</span>{{ card.expiration_month }} / {{ card.expiration_year }}</p> <p>Exp: <span v-if="Number(card.expiration_month) < 10">0</span>{{ card.expiration_month }} / {{ card.expiration_year }}</p>
<p>CVV: {{ card.security_number }}</p> <p>CVV: {{ card.security_number }}</p>
</div> </div>
<div class="flex justify-end gap-2 mt-2"> <div class="flex justify-end gap-2 mt-2">
@@ -240,302 +240,308 @@
</div> </div>
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { Customer, CreditCard } from '../../types/models'
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 useValidate from "@vuelidate/core"; import { useVuelidate } from "@vuelidate/core";
import { required, requiredIf } from "@vuelidate/validators"; import { required, 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 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: number; expiration_year: number; last_four_digits: string; security_number: string; main_card: boolean; }
interface PricingTier { gallons: number; price: string | number; } interface PricingTier { gallons: number; price: string | number; }
const router = useRouter()
const route = useRoute()
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' };
export default defineComponent({ // Reactive data
name: 'deliveryEdit', const quickGallonAmounts = ref([100, 125, 150, 175, 200, 220])
components: { Header, SideBar, Footer }, const deliveryStatus = ref([] as any[])
data() { const truckDriversList = ref([] as any[])
return { const userCards = ref<CreditCard[]>([])
v$: useValidate(), const promos = ref([] as any[])
quickGallonAmounts: [100, 125, 150, 175, 200, 220], const pricingTiers = ref([] as PricingTier[])
deliveryStatus: [] as any[], const customer = ref({} as Customer)
truckDriversList: [] as any[], const deliveryOrder = ref({} as DeliveryOrder)
userCards: [] as UserCard[], const userCard = ref({} as CreditCard)
promos: [] as any[], // RESTORED: Add all form fields to the data model
pricingTiers: [] as PricingTier[], const CreateOilOrderForm = ref({
customer: {} as Customer, basicInfo: {
deliveryOrder: {} as DeliveryOrder, gallons_ordered: '',
userCard: {} as UserCard, customer_asked_for_fill: false,
// RESTORED: Add all form fields to the data model created_delivery_date: '',
CreateOilOrderForm: { expected_delivery_date: '',
basicInfo: { prime: false,
gallons_ordered: '', emergency: false,
customer_asked_for_fill: false, same_day: false,
created_delivery_date: '', delivery_status: 0,
expected_delivery_date: '', driver_employee_id: 0,
prime: false, dispatcher_notes_taken: '',
emergency: false, promo_id: 0,
same_day: false, payment_type: 0,
delivery_status: 0, credit_card_id: 0,
driver_employee_id: 0, credit: false,
dispatcher_notes_taken: '', cash: false,
promo_id: 0, check: false,
payment_type: 0, other: false,
credit_card_id: 0,
credit: false,
cash: false,
check: false,
other: false,
},
},
}
},
validations() {
return {
CreateOilOrderForm: {
basicInfo: {
gallons_ordered: {
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 },
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,
},
};
},
computed: {
stateName(): string {
if (this.customer && this.customer.customer_state !== undefined) {
return STATE_MAP[this.customer.customer_state];
}
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() {
this.fetchInitialData();
},
methods: {
fetchInitialData() {
const deliveryId = this.$route.params.id as string;
this.getPromos();
this.getDriversList();
this.getDeliveryStatusList();
this.getPricingTiers();
this.getDeliveryOrder(deliveryId);
},
getDeliveryOrder(deliveryId: string) {
axios.get(`${import.meta.env.VITE_BASE_URL}/delivery/order/${deliveryId}`, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
// FIX: Check for the 'ok' flag and access the nested 'delivery' object
if (response.data && response.data.ok) {
this.deliveryOrder = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE
// RESTORED: Populate all form fields from the API response
const paymentType = this.deliveryOrder.payment_type;
this.CreateOilOrderForm.basicInfo = {
gallons_ordered: String(this.deliveryOrder.gallons_ordered),
customer_asked_for_fill: !!this.deliveryOrder.customer_asked_for_fill,
created_delivery_date: this.deliveryOrder.when_ordered,
expected_delivery_date: this.deliveryOrder.expected_delivery_date,
prime: !!this.deliveryOrder.prime,
emergency: !!this.deliveryOrder.emergency,
same_day: !!this.deliveryOrder.same_day,
delivery_status: this.deliveryOrder.delivery_status,
driver_employee_id: this.deliveryOrder.driver_employee_id || 0,
dispatcher_notes_taken: this.deliveryOrder.dispatcher_notes,
promo_id: this.deliveryOrder.promo_id || 0,
payment_type: paymentType,
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);
} else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
}
})
.catch((error: any) => console.error("Error fetching delivery order:", error));
},
getCustomer(customerId: number) {
axios.get(`${import.meta.env.VITE_BASE_URL}/customer/${customerId}`, { withCredentials: true })
.then((response: any) => {
this.customer = response.data;
this.getPaymentCards(customerId);
if (this.deliveryOrder.payment_type === 1 && this.deliveryOrder.payment_card_id) {
this.getPaymentCard(this.deliveryOrder.payment_card_id);
}
})
.catch((error: any) => console.error("Error fetching customer:", error));
},
getPaymentCards(customerId: number) {
axios.get(`${import.meta.env.VITE_BASE_URL}/payment/cards/${customerId}`, { withCredentials: true })
.then((response: any) => { this.userCards = response.data; })
.catch((error: any) => console.error("Error fetching payment cards:", error));
},
getPaymentCard(cardId: number) {
axios.get(`${import.meta.env.VITE_BASE_URL}/payment/card/${cardId}`, { withCredentials: true })
.then((response: any) => { this.userCard = response.data; })
.catch((error: any) => console.error("Error fetching specific payment card:", error));
},
getPromos() {
axios.get(`${import.meta.env.VITE_BASE_URL}/promo/all`, { withCredentials: true })
.then((response: any) => { this.promos = response.data; });
},
getDriversList() {
axios.get(`${import.meta.env.VITE_BASE_URL}/employee/drivers`, { headers: authHeader(), withCredentials: true })
.then((response: any) => { this.truckDriversList = response.data; });
},
getDeliveryStatusList() {
axios.get(`${import.meta.env.VITE_BASE_URL}/query/deliverystatus`, { withCredentials: true })
.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();
if (!isFormValid || !isPaymentValid) {
notify({ title: "Form Incomplete", text: "Please review the fields marked in red.", type: "warn" });
return;
}
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
const payload = {
...formInfo,
payment_type: paymentType,
cash: formInfo.cash,
credit: formInfo.credit,
check: formInfo.check,
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() })
.then(() => {
notify({ type: 'success', title: 'Success!', text: 'Delivery updated.' });
if (paymentType === 1) {
this.$router.push({ name: 'payOil', params: { id: this.deliveryOrder.id } });
} else {
this.$router.push({ name: 'deliveryOrder', params: { id: this.deliveryOrder.id } });
}
})
.catch((error: any) => {
console.error("Error submitting form:", error);
notify({ type: 'error', title: 'Update Failed', text: 'Could not save changes.' });
});
},
}, },
}) })
// Computed
const stateName = computed((): string => {
if (customer.value && customer.value.customer_state !== undefined) {
return STATE_MAP[customer.value.customer_state];
}
return '';
})
const isPricingTierSelected = computed(() => {
return (tierGallons: number | string): boolean => {
if (!CreateOilOrderForm.value.basicInfo.gallons_ordered) return false;
const selectedGallons = Number(CreateOilOrderForm.value.basicInfo.gallons_ordered);
if (isNaN(selectedGallons)) return false;
const tierNum = Number(tierGallons);
if (isNaN(tierNum)) return false;
return selectedGallons === tierNum;
};
})
const isAnyPaymentMethodSelected = computed((): boolean => {
return !!(CreateOilOrderForm.value.basicInfo?.credit || CreateOilOrderForm.value.basicInfo?.cash || CreateOilOrderForm.value.basicInfo?.check || CreateOilOrderForm.value.basicInfo?.other);
})
const selectedGallonsAmount = computed((): number => {
const value = CreateOilOrderForm.value.basicInfo.gallons_ordered ?? '';
return Number(value);
})
// Validations
const validations = {
CreateOilOrderForm: {
basicInfo: {
gallons_ordered: {
required: requiredIf(() => !CreateOilOrderForm.value.basicInfo.customer_asked_for_fill),
minValue: (value: string) => {
if (CreateOilOrderForm.value.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 },
credit_card_id: {
creditCardRequired: (value: number) => {
if (CreateOilOrderForm.value.basicInfo.credit) {
return value !== 0;
}
return true;
}
},
},
},
isAnyPaymentMethodSelected: {
mustBeTrue: (value: boolean) => value === true,
},
}
const v$ = useVuelidate(validations, { CreateOilOrderForm, isAnyPaymentMethodSelected })
// Functions
const fetchInitialData = () => {
const deliveryId = route.params.id as string;
getPromos();
getDriversList();
getDeliveryStatusList();
getPricingTiers();
getDeliveryOrder(deliveryId);
}
const getDeliveryOrder = (deliveryId: string) => {
axios.get(`${import.meta.env.VITE_BASE_URL}/delivery/order/${deliveryId}`, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
// FIX: Check for the 'ok' flag and access the nested 'delivery' object
if (response.data && response.data.ok) {
deliveryOrder.value = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE
// RESTORED: Populate all form fields from the API response
const paymentType = deliveryOrder.value.payment_type;
CreateOilOrderForm.value.basicInfo = {
gallons_ordered: String(deliveryOrder.value.gallons_ordered),
customer_asked_for_fill: !!deliveryOrder.value.customer_asked_for_fill,
created_delivery_date: deliveryOrder.value.when_ordered,
expected_delivery_date: deliveryOrder.value.expected_delivery_date,
prime: !!deliveryOrder.value.prime,
emergency: !!deliveryOrder.value.emergency,
same_day: !!deliveryOrder.value.same_day,
delivery_status: deliveryOrder.value.delivery_status,
driver_employee_id: deliveryOrder.value.driver_employee_id || 0,
dispatcher_notes_taken: deliveryOrder.value.dispatcher_notes,
promo_id: deliveryOrder.value.promo_id || 0,
payment_type: paymentType,
credit_card_id: deliveryOrder.value.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,
};
getCustomer(deliveryOrder.value.customer_id);
} else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
}
})
.catch((error: any) => console.error("Error fetching delivery order:", error));
}
const getCustomer = (customerId: number) => {
axios.get(`${import.meta.env.VITE_BASE_URL}/customer/${customerId}`, { withCredentials: true })
.then((response: any) => {
customer.value = response.data;
getPaymentCards(customerId);
if (deliveryOrder.value.payment_type === 1 && deliveryOrder.value.payment_card_id) {
getPaymentCard(deliveryOrder.value.payment_card_id);
}
})
.catch((error: any) => console.error("Error fetching customer:", error));
}
const getPaymentCards = (customerId: number) => {
axios.get(`${import.meta.env.VITE_BASE_URL}/payment/cards/${customerId}`, { withCredentials: true })
.then((response: any) => { userCards.value = response.data; })
.catch((error: any) => console.error("Error fetching payment cards:", error));
}
const getPaymentCard = (cardId: number) => {
axios.get(`${import.meta.env.VITE_BASE_URL}/payment/card/${cardId}`, { withCredentials: true })
.then((response: any) => { userCard.value = response.data; })
.catch((error: any) => console.error("Error fetching specific payment card:", error));
}
const getPromos = () => {
axios.get(`${import.meta.env.VITE_BASE_URL}/promo/all`, { withCredentials: true })
.then((response: any) => { promos.value = response.data; });
}
const getDriversList = () => {
axios.get(`${import.meta.env.VITE_BASE_URL}/employee/drivers`, { headers: authHeader(), withCredentials: true })
.then((response: any) => { truckDriversList.value = response.data; });
}
const getDeliveryStatusList = () => {
axios.get(`${import.meta.env.VITE_BASE_URL}/query/deliverystatus`, { withCredentials: true })
.then((response: any) => { deliveryStatus.value = response.data; });
}
const 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;
pricingTiers.value = 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" });
});
}
const editCard = (card_id: number) => {
router.push({ name: "cardedit", params: { id: card_id } });
}
const 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" });
getPaymentCards(customer.value.id);
})
.catch(() => {
notify({ title: "Error", text: "Could not remove card.", type: "error" });
});
}
}
const selectCreditCard = (cardId: number) => {
CreateOilOrderForm.value.basicInfo.credit_card_id = cardId;
}
const setGallons = (amount: number) => {
CreateOilOrderForm.value.basicInfo.gallons_ordered = String(amount);
CreateOilOrderForm.value.basicInfo.customer_asked_for_fill = false;
}
const setDeliveryDate = (days: number) => {
const date = new Date();
date.setDate(date.getDate() + days);
CreateOilOrderForm.value.basicInfo.expected_delivery_date = date.toISOString().split('T')[0];
}
const isDeliveryDateSelected = (days: number): boolean => {
const date = new Date();
date.setDate(date.getDate() + days);
return CreateOilOrderForm.value.basicInfo.expected_delivery_date === date.toISOString().split('T')[0];
}
const onSubmit = async () => {
const isFormValid = await v$.value.CreateOilOrderForm.$validate();
const isPaymentValid = await v$.value.isAnyPaymentMethodSelected.$validate();
if (!isFormValid || !isPaymentValid) {
notify({ title: "Form Incomplete", text: "Please review the fields marked in red.", type: "warn" });
return;
}
const formInfo = CreateOilOrderForm.value.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
const payload = {
...formInfo,
payment_type: paymentType,
cash: formInfo.cash,
credit: formInfo.credit,
check: formInfo.check,
other: formInfo.other,
credit_card_id: formInfo.credit ? formInfo.credit_card_id : null,
};
axios.post(`${import.meta.env.VITE_BASE_URL}/delivery/edit/${deliveryOrder.value.id}`, payload, { withCredentials: true, headers: authHeader() })
.then(() => {
notify({ type: 'success', title: 'Success!', text: 'Delivery updated.' });
if (paymentType === 1) {
router.push({ name: 'payOil', params: { id: deliveryOrder.value.id } });
} else {
router.push({ name: 'deliveryOrder', params: { id: deliveryOrder.value.id } });
}
})
.catch((error: any) => {
console.error("Error submitting form:", error);
notify({ type: 'error', title: 'Update Failed', text: 'Could not save changes.' });
});
}
// Lifecycle
onMounted(() => {
fetchInitialData();
})
</script> </script>

View File

@@ -55,18 +55,18 @@
</td> </td>
<td> <td>
<span class="badge badge-sm" :class="{ <span class="badge badge-sm" :class="{
'badge-warning': oil.delivery_status == 0, 'badge-warning': oil.delivery_status === computedDELIVERY_STATUS.WAITING,
'badge-success': [1, 10].includes(oil.delivery_status), 'badge-success': [computedDELIVERY_STATUS.DELIVERED, computedDELIVERY_STATUS.FINALIZED].includes(oil.delivery_status as any),
'badge-info': oil.delivery_status == 2, 'badge-info': oil.delivery_status === computedDELIVERY_STATUS.OUT_FOR_DELIVERY,
'badge-error': [3, 5].includes(oil.delivery_status), 'badge-error': [computedDELIVERY_STATUS.TOMORROW, computedDELIVERY_STATUS.ISSUE].includes(oil.delivery_status as any),
}"> }">
<span v-if="oil.delivery_status == 0">Waiting</span> <span v-if="oil.delivery_status === computedDELIVERY_STATUS.WAITING">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Cancelled</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.CANCELLED">Cancelled</span>
<span v-else-if="oil.delivery_status == 2">Today</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.OUT_FOR_DELIVERY">Today</span>
<span v-else-if="oil.delivery_status == 3">Tomorrow_Delivery</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.TOMORROW">Tomorrow_Delivery</span>
<span v-else-if="oil.delivery_status == 4">Partial_Delivery</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.PARTIAL_DELIVERY">Partial_Delivery</span>
<span v-else-if="oil.delivery_status == 5">Issue</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.ISSUE">Issue</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.FINALIZED">Finalized</span>
</span> </span>
</td> </td>
<td> <td>
@@ -96,7 +96,7 @@
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link> <router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link> <router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent">Finalize</router-link> <router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="!isFinalizedStatus(oil.delivery_status)" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link> <router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div> </div>
</td> </td>
@@ -115,18 +115,18 @@
<p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p> <p class="text-xs text-gray-400">Delivery #{{ oil.id }}</p>
</div> </div>
<div class="badge" :class="{ <div class="badge" :class="{
'badge-warning': oil.delivery_status == 0, 'badge-warning': oil.delivery_status === computedDELIVERY_STATUS.WAITING,
'badge-success': [1, 10].includes(oil.delivery_status), 'badge-success': [computedDELIVERY_STATUS.DELIVERED, computedDELIVERY_STATUS.FINALIZED].includes(oil.delivery_status as any),
'badge-info': oil.delivery_status == 2, 'badge-info': oil.delivery_status === computedDELIVERY_STATUS.OUT_FOR_DELIVERY,
'badge-error': [3, 5].includes(oil.delivery_status), 'badge-error': [computedDELIVERY_STATUS.TOMORROW, computedDELIVERY_STATUS.ISSUE].includes(oil.delivery_status as any),
}"> }">
<span v-if="oil.delivery_status == 0">Waiting</span> <span v-if="oil.delivery_status === computedDELIVERY_STATUS.WAITING">Waiting</span>
<span v-else-if="oil.delivery_status == 1">Delivered</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.DELIVERED">Delivered</span>
<span v-else-if="oil.delivery_status == 2">Today_Delivery</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.OUT_FOR_DELIVERY">Today_Delivery</span>
<span v-else-if="oil.delivery_status == 3">Tommorrow_Delivery</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.TOMORROW">Tommorrow_Delivery</span>
<span v-else-if="oil.delivery_status == 4">Partial Delivery</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.PARTIAL_DELIVERY">Partial Delivery</span>
<span v-else-if="oil.delivery_status == 5">Issue</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.ISSUE">Issue</span>
<span v-else-if="oil.delivery_status == 10">Finalized</span> <span v-else-if="oil.delivery_status === computedDELIVERY_STATUS.FINALIZED">Finalized</span>
</div> </div>
</div> </div>
@@ -149,7 +149,7 @@
<div class="card-actions justify-end flex-wrap gap-2 mt-2"> <div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link> <router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link> <router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent">Finalize</router-link> <router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="!isFinalizedStatus(oil.delivery_status)" class="btn btn-sm btn-accent">Finalize</router-link>
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link> <router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success">Print</router-link>
</div> </div>
</div> </div>
@@ -169,136 +169,135 @@
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted, computed } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { deliveryService } from '../../services/deliveryService'
import { Delivery } from '../../types/models'
import { DELIVERY_STATUS, DeliveryStatusType, getDeliveryStatusLabel } from '../../constants/status';
import Header from '../../layouts/headers/headerauth.vue' import Header from '../../layouts/headers/headerauth.vue'
import PaginationComp from '../../components/pagination.vue' import PaginationComp from '../../components/pagination.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({ // Reactive data
name: 'deliveryHome', const delivery_count = ref(0)
const delivery_count_delivered = ref(0)
const token = ref(null)
const user = ref(null)
const deliveries = ref<Delivery[]>([])
const page = ref(1)
const perPage = ref(50)
const recordsLength = ref(0)
const options = ref({
edgeNavigation: false,
format: false,
template: PaginationComp
})
components: { // Computed
Header, const computedDELIVERY_STATUS = computed(() => DELIVERY_STATUS)
SideBar,
Footer,
},
data() { // Functions
return { const isDeliveredStatus = (status: DeliveryStatusType): boolean => {
delivery_count: 0, return status === DELIVERY_STATUS.DELIVERED || status === 1; // Support both old (1) and new (11) values
delivery_count_delivered: 0, }
token: null,
user: null, const isFinalizedStatus = (status: DeliveryStatusType): boolean => {
deliveries: [] as any[], return status === DELIVERY_STATUS.FINALIZED;
page: 1, }
perPage: 50,
recordsLength: 0, const getPage = (pageVal: any) => {
options: { deliveries.value = [];
edgeNavigation: false, get_oil_orders(pageVal)
format: false, }
template: PaginationComp
const 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) {
user.value = response.data.user;
} }
})
.catch(() => {
user.value = null
})
}
const get_oil_orders = async (pageVal: number) => {
try {
const response = await deliveryService.getAll(pageVal)
deliveries.value = response.data || []
} catch (error) {
console.error('Error fetching deliveries:', error)
deliveries.value = []
}
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
} }
}, })
}
created() { const today_delivery_count = () => {
this.userStatus() let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/today'
}, axios({
mounted() { method: "get",
this.getPage(this.page); url: path,
this.today_delivery_count(); withCredentials: true,
this.today_delivery_delivered(); headers: authHeader(),
}, })
methods: { .then((response: any) => {
getPage: function (page: any) { delivery_count.value = response.data.data;
// we simulate an api call that fetch the records from a backend })
this.deliveries = []; }
this.get_oil_orders(page)
},
userStatus() { const today_delivery_delivered = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami'; let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/delivered/today'
axios({ axios({
method: 'get', method: "get",
url: path, url: path,
withCredentials: true, withCredentials: true,
headers: authHeader(), headers: authHeader(),
}) })
.then((response: any) => { .then((response: any) => {
if (response.data.ok) { delivery_count_delivered.value = response.data.data;
this.user = response.data.user; })
} }
})
.catch(() => {
this.user = null
})
},
get_oil_orders(page: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/all/' + page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
}) // Lifecycle
}, onMounted(() => {
userStatus()
getPage(page.value);
deleteCall(delivery_id: any) { today_delivery_count();
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id; today_delivery_delivered();
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
},
today_delivery_count() {
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/today'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.delivery_count = response.data.data;
})
},
today_delivery_delivered() {
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/delivered/today'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.delivery_count_delivered = response.data.data;
})
},
},
}) })
</script> </script>

View File

@@ -1,20 +1,20 @@
import DeliveryHome from './home.vue'; const DeliveryHome = () => import('./home.vue');
import DeliveryCreate from "./create.vue"; const DeliveryCreate = () => import("./create.vue");
import DeliveryEdit from './edit.vue'; const DeliveryEdit = () => import('./edit.vue');
import DeliveryOrder from './view.vue'; const DeliveryOrder = () => import('./view.vue');
import deliveryTicketsMissing from './update_tickets/missing_data_home.vue'; const deliveryTicketsMissing = () => import('./update_tickets/missing_data_home.vue');
import deliveryPending from './viewstatus/pending.vue'; const deliveryPending = () => import('./viewstatus/pending.vue');
import deliveryCancelled from './viewstatus/cancelled.vue'; const deliveryCancelled = () => import('./viewstatus/cancelled.vue');
import deliveryIssue from './viewstatus/issue.vue'; const deliveryIssue = () => import('./viewstatus/issue.vue');
import deliveryDelivered from './viewstatus/delivered.vue'; const deliveryDelivered = () => import('./viewstatus/delivered.vue');
import deliveryOutForDelivery from './viewstatus/todaysdeliveries.vue'; const deliveryOutForDelivery = () => import('./viewstatus/todaysdeliveries.vue');
import deliveryWaiting from './viewstatus/waiting.vue'; const deliveryWaiting = () => import('./viewstatus/waiting.vue');
import deliveryFinalized from './viewstatus/finalized.vue' const deliveryFinalized = () => import('./viewstatus/finalized.vue')
import deliveryTommorrow from './viewstatus/tommorrow.vue' const deliveryTommorrow = () => import('./viewstatus/tommorrow.vue')
import finalizeTicket from './update_tickets/finalize_ticket.vue'; const finalizeTicket = () => import('./update_tickets/finalize_ticket.vue');
import finalizeTicketAuto from './update_tickets/finalize_ticket_auto.vue'; const finalizeTicketAuto = () => import('./update_tickets/finalize_ticket_auto.vue');
import finalizeTicketAutoNocc from './update_tickets/finalize_ticket_auto_nocc.vue'; const finalizeTicketAutoNocc = () => import('./update_tickets/finalize_ticket_auto_nocc.vue');
const deliveryRoutes = [ const deliveryRoutes = [
{ {

View File

@@ -252,8 +252,9 @@
</div> </div>
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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'
@@ -272,389 +273,395 @@ interface UserCard {
security_number: string; security_number: string;
} }
// Route and router
const route = useRoute()
const router = useRouter()
export default defineComponent({ // Reactive data
name: 'finalizeTicket', const isLoading = ref(false)
components: { Header, SideBar, Footer }, const user = ref({ id: 0 })
data() { const userCardfound = ref(false)
return { const preChargeTotal = ref(0)
isLoading: false, const FinalizeOilOrderForm = ref({
user: { id: 0 }, cash_recieved: '',
userCardfound: false, fill_location: '',
preChargeTotal: 0, check_number: '',
FinalizeOilOrderForm: { gallons_delivered: '',
cash_recieved: '', })
fill_location: '', const userCard = ref({} as UserCard)
check_number: '', const customer = ref({
gallons_delivered: '', id: 0,
}, customer_address: '',
userCard: {} as UserCard, customer_first_name: '',
customer: { customer_last_name: '',
id: 0, customer_town: '',
customer_address: '', customer_state: 0,
customer_first_name: '', customer_zip: '',
customer_last_name: '', customer_apt: '',
customer_town: '', customer_home_type: 0,
customer_state: 0, customer_phone_number: '',
customer_zip: '', })
customer_apt: '', const customerDescription = ref({ fill_location: '' })
customer_home_type: 0, const deliveryOrder = ref({
customer_phone_number: '', id: '',
}, customer_id: 0,
customerDescription: { fill_location: '' }, gallons_ordered: 0,
deliveryOrder: { customer_asked_for_fill: 0,
id: '', gallons_delivered: '',
customer_id: 0, delivery_status: 0,
gallons_ordered: 0, when_ordered: '',
customer_asked_for_fill: 0, when_delivered: '',
gallons_delivered: '', expected_delivery_date: '',
delivery_status: 0, customer_price: '',
when_ordered: '', prime: 0,
when_delivered: '', same_day: 0,
expected_delivery_date: '', payment_type: 0,
customer_price: '', payment_card_id: '',
prime: 0, promo_id: null,
same_day: 0, })
payment_type: 0, const pricing = ref({
payment_card_id: '', price_prime: 0,
promo_id: null, price_same_day: 0,
}, })
pricing: { const promo_active = ref(false)
price_prime: 0, const promo = ref({
price_same_day: 0, name_of_promotion: '',
}, description: '',
promo_active: false, money_off_delivery: 0,
promo: { text_on_ticket: ''
name_of_promotion: '', })
description: '', const total_amount = ref(0)
money_off_delivery: 0, const discount = ref(0)
text_on_ticket: '' const total_amount_after_discount = ref(0)
}, const transaction = ref(null as any)
total_amount: 0,
discount: 0, // Computed properties
total_amount_after_discount: 0, const finalChargeAmount = computed((): number => {
transaction: null as any, // If promo is active, use server-calculated totals with fees added
if (promo_active.value && total_amount_after_discount.value > 0) {
let total = total_amount_after_discount.value;
if (deliveryOrder.value.prime === 1) total += Number(pricing.value.price_prime);
if (deliveryOrder.value.same_day === 1) total += Number(pricing.value.price_same_day);
return total;
}
// Otherwise, calculate locally
const gallons = Number(FinalizeOilOrderForm.value.gallons_delivered);
const pricePerGallon = Number(deliveryOrder.value.customer_price);
if (isNaN(gallons) || isNaN(pricePerGallon) || gallons <= 0) {
return 0;
}
let total = gallons * pricePerGallon;
if (deliveryOrder.value.prime === 1) total += Number(pricing.value.price_prime);
if (deliveryOrder.value.same_day === 1) total += Number(pricing.value.price_same_day);
return total;
})
// Lifecycle
onMounted(() => {
const deliveryId = route.params.id;
// --- DEBUGGING STEP 1 ---
console.log(`[DEBUG] Component Mounted. Fetching data for delivery ID: ${deliveryId}`);
getOilOrder(deliveryId);
getOilPricing();
})
// Functions
const getOilOrder = async (delivery_id: any) => {
const path = `${import.meta.env.VITE_BASE_URL}/delivery/order/${delivery_id}`;
// --- DEBUGGING STEP 2 ---
console.log(`[DEBUG] Calling getOilOrder API at: ${path}`);
try {
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
// --- DEBUGGING STEP 3 ---
console.log('[DEBUG] Received RAW response from getOilOrder:', response.data);
if (response.data && response.data.ok) {
console.log('[DEBUG] Response is OK. Processing data...');
deliveryOrder.value = response.data.delivery;
// --- DEBUGGING STEP 4 ---
console.log(`[DEBUG] Value of response.data.total_amount is:`, response.data.total_amount);
total_amount.value = response.data.delivery.total_amount || 0;
preChargeTotal.value = response.data.delivery.total_amount || 0;
await getCustomer(deliveryOrder.value.customer_id);
if ([1, 2, 11].includes(deliveryOrder.value.payment_type) && deliveryOrder.value.payment_card_id) {
getPaymentCard(deliveryOrder.value.payment_card_id);
}
if (deliveryOrder.value.promo_id != null) {
getPromo(deliveryOrder.value.promo_id);
}
// Fetch calculated totals including discounts
sumdelivery(delivery_id);
// Call transaction fetch after customer is loaded
setTimeout(() => getTransaction(delivery_id), 500);
} else {
console.error('[DEBUG] getOilOrder response was not OK or data is missing.');
notify({ title: "Data Error", text: "Could not retrieve complete delivery details.", type: "error" });
} }
}, } catch (error) {
computed: { // --- DEBUGGING STEP 5 ---
finalChargeAmount(): number { console.error("[DEBUG] The getOilOrder API call FAILED. Error object:", error);
// If promo is active, use server-calculated totals with fees added notify({ title: "Network Error", text: "Could not fetch delivery order.", type: "error" });
if (this.promo_active && this.total_amount_after_discount > 0) { }
let total = this.total_amount_after_discount; }
if (this.deliveryOrder.prime === 1) total += Number(this.pricing.price_prime);
if (this.deliveryOrder.same_day === 1) total += Number(this.pricing.price_same_day); const getPaymentCard = async (card_id: any) => {
return total; const path = `${import.meta.env.VITE_BASE_URL}/payment/card/${card_id}`;
try {
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
userCard.value = response.data;
userCardfound.value = true;
} catch (error) {
userCardfound.value = false;
console.error(`[DEBUG] Error fetching payment card ${card_id}:`, error);
}
}
const getCustomer = async (user_id: any) => {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
try {
const response = await axios.get(path, { withCredentials: true });
customer.value = response.data;
await getCustomerDescription(deliveryOrder.value.customer_id);
} catch (error) { console.error("[DEBUG] Error fetching customer:", error); }
}
const getCustomerDescription = async (user_id: any) => {
const path = `${import.meta.env.VITE_BASE_URL}/customer/description/${user_id}`;
try {
const response = await axios.get(path, { withCredentials: true });
customerDescription.value = response.data;
FinalizeOilOrderForm.value.fill_location = customerDescription.value.fill_location;
} catch (error) { console.error("[DEBUG] Error fetching customer description:", error); }
}
const getOilPricing = () => {
const path = `${import.meta.env.VITE_BASE_URL}/info/price/oil/table`;
axios.get(path, { withCredentials: true })
.then((response: any) => { pricing.value = response.data; })
.catch((error: any) => { console.error("[DEBUG] Error fetching oil pricing:", error); });
}
const 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) {
promo.value = response.data
promo_active.value = true
} }
})
}
// Otherwise, calculate locally const sumdelivery = (delivery_id: any) => {
const gallons = Number(this.FinalizeOilOrderForm.gallons_delivered); let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
const pricePerGallon = Number(this.deliveryOrder.customer_price); axios({
if (isNaN(gallons) || isNaN(pricePerGallon) || gallons <= 0) { method: "get",
return 0; url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data.ok) {
total_amount.value = parseFloat(response.data.total_amount) || 0;
discount.value = parseFloat(response.data.discount) || 0;
total_amount_after_discount.value = parseFloat(response.data.total_amount_after_discount) || 0;
} }
let total = gallons * pricePerGallon; })
if (this.deliveryOrder.prime === 1) total += Number(this.pricing.price_prime); .catch(() => {
if (this.deliveryOrder.same_day === 1) total += Number(this.pricing.price_same_day); notify({
return total; title: "Error",
} text: "Could not get oil pricing",
}, type: "error",
mounted() {
const deliveryId = this.$route.params.id;
// --- DEBUGGING STEP 1 ---
console.log(`[DEBUG] Component Mounted. Fetching data for delivery ID: ${deliveryId}`);
this.getOilOrder(deliveryId);
this.getOilPricing();
},
methods: {
async getOilOrder(delivery_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/delivery/order/${delivery_id}`;
// --- DEBUGGING STEP 2 ---
console.log(`[DEBUG] Calling getOilOrder API at: ${path}`);
try {
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
// --- DEBUGGING STEP 3 ---
console.log('[DEBUG] Received RAW response from getOilOrder:', response.data);
if (response.data && response.data.ok) {
console.log('[DEBUG] Response is OK. Processing data...');
this.deliveryOrder = response.data.delivery;
// --- DEBUGGING STEP 4 ---
console.log(`[DEBUG] Value of response.data.total_amount is:`, response.data.total_amount);
this.total_amount = response.data.delivery.total_amount || 0;
this.preChargeTotal = response.data.delivery.total_amount || 0;
await this.getCustomer(this.deliveryOrder.customer_id);
if ([1, 2, 11].includes(this.deliveryOrder.payment_type) && this.deliveryOrder.payment_card_id) {
this.getPaymentCard(this.deliveryOrder.payment_card_id);
}
if (this.deliveryOrder.promo_id != null) {
this.getPromo(this.deliveryOrder.promo_id);
}
// Fetch calculated totals including discounts
this.sumdelivery(delivery_id);
// Call transaction fetch after customer is loaded
setTimeout(() => this.getTransaction(delivery_id), 500);
} else {
console.error('[DEBUG] getOilOrder response was not OK or data is missing.');
notify({ title: "Data Error", text: "Could not retrieve complete delivery details.", type: "error" });
}
} catch (error) {
// --- DEBUGGING STEP 5 ---
console.error("[DEBUG] The getOilOrder API call FAILED. Error object:", error);
notify({ title: "Network Error", text: "Could not fetch delivery order.", type: "error" });
}
},
async getPaymentCard(card_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/${card_id}`;
try {
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
this.userCard = response.data;
this.userCardfound = true;
} catch (error) {
this.userCardfound = false;
console.error(`[DEBUG] Error fetching payment card ${card_id}:`, error);
}
},
async getCustomer(user_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
try {
const response = await axios.get(path, { withCredentials: true });
this.customer = response.data;
await this.getCustomerDescription(this.deliveryOrder.customer_id);
} catch (error) { console.error("[DEBUG] Error fetching customer:", error); }
},
async getCustomerDescription(user_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/description/${user_id}`;
try {
const response = await axios.get(path, { withCredentials: true });
this.customerDescription = response.data;
this.FinalizeOilOrderForm.fill_location = this.customerDescription.fill_location;
} catch (error) { console.error("[DEBUG] Error fetching customer description:", error); }
},
getOilPricing() {
const path = `${import.meta.env.VITE_BASE_URL}/info/price/oil/table`;
axios.get(path, { withCredentials: true })
.then((response: any) => { this.pricing = response.data; })
.catch((error: any) => { console.error("[DEBUG] Error fetching oil pricing:", 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
}
})
},
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 = parseFloat(response.data.total_amount) || 0;
this.discount = parseFloat(response.data.discount) || 0;
this.total_amount_after_discount = parseFloat(response.data.total_amount_after_discount) || 0;
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
},
getTransaction(delivery_id: any) {
// Add guard to prevent undefined customer ID API calls
if (!delivery_id || !this.customer || !this.customer.id) {
console.log("Skipping transaction fetch - delivery or customer data not available");
return;
}
// Consistent with delivery/view.vue - use customer transaction endpoint
const path = `${import.meta.env.VITE_BASE_URL}/payment/transactions/customer/${this.customer.id}/1`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
console.log("Transaction API response:", response.data);
// Handle both single transaction object and array responses
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
// Find the transaction for this specific delivery
const deliveryTransaction = response.data.find((txn: any) =>
txn.delivery_id === parseInt(delivery_id) ||
txn.transaction_id === delivery_id ||
txn.delivery_number === delivery_id
);
this.transaction = deliveryTransaction || null;
} else if (response.data && !Array.isArray(response.data)) {
// If single transaction, check if it's for this delivery
const txn = response.data;
if (txn.delivery_id === parseInt(delivery_id) ||
txn.transaction_id === delivery_id ||
txn.delivery_number === delivery_id) {
this.transaction = txn;
} else {
this.transaction = null;
}
} else {
this.transaction = null;
}
if (!this.transaction) {
console.log(`No transaction found for delivery ${delivery_id} among customer transactions`);
}
})
.catch((error: any) => {
// Handle various error responses gracefully
if (error.response && error.response.status === 404) {
console.log(`No transactions found for customer ${this.customer.id}`);
this.transaction = null;
} else if (error.response && error.response.status === 400) {
console.log(`Bad request for customer transactions: ${error.response.data?.detail || error.message}`);
this.transaction = null;
} else {
console.error("Error fetching transaction:", error);
this.transaction = null;
}
});
},
getTypeColor(transactionType: number) {
switch (transactionType) {
case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge
case 2: return 'text-purple-600'; // Capture
case 3: return 'text-green-600'; // Delivery/Other
default: return 'text-gray-600';
}
},
CreateTransaction() {
const path = `${import.meta.env.VITE_MONEY_URL}/delivery/add/${this.deliveryOrder.id}`;
axios.post(path, {}, { withCredentials: true, headers: authHeader() })
.then(() => notify({ title: "Success", text: "Accounting record created.", type: "success" }))
.catch(() => notify({ title: "Warning", text: "Could not create accounting record.", type: "warn" }));
},
async onSubmit() {
if (Number(this.FinalizeOilOrderForm.gallons_delivered) <= 0) {
notify({ title: "Validation Error", text: "Gallons delivered must be greater than zero.", type: "error" });
return;
}
this.isLoading = true;
const finalizePayload = {
gallons_delivered: this.FinalizeOilOrderForm.gallons_delivered,
fill_location: this.FinalizeOilOrderForm.fill_location,
cash_recieved: this.FinalizeOilOrderForm.cash_recieved,
check_number: this.FinalizeOilOrderForm.check_number,
};
const finalizePath = `${import.meta.env.VITE_BASE_URL}/deliverydata/finalize/${this.deliveryOrder.id}`;
try {
const finalizeResponse = await axios.put(finalizePath, finalizePayload, { withCredentials: true, headers: authHeader() });
if (!finalizeResponse.data.ok) {
throw new Error(finalizeResponse.data.error || "Failed to update delivery details.");
}
this.CreateTransaction();
notify({ title: "Success", text: "Ticket has been finalized.", type: "success" });
// FIX: Wait for customer data to be loaded before redirecting
await this.waitForCustomerData(this.deliveryOrder.customer_id);
// Updated redirect logic based on your requirements
await this.handleRedirect();
} catch (error: any) {
const errorMessage = error.response?.data?.detail || "An error occurred during finalization.";
notify({ title: "Error", text: errorMessage, type: "error" });
} finally {
this.isLoading = false;
}
},
// NEW: Wait for customer data to be loaded before redirecting
async waitForCustomerData(customerId: number): Promise<void> {
return new Promise((resolve) => {
const checkCustomer = () => {
if (this.customer && this.customer.id && this.customer.id === customerId) {
resolve();
} else {
setTimeout(checkCustomer, 100);
}
};
checkCustomer();
}); });
}, });
}
// NEW: Updated redirect logic based on payment type and transaction status const getTransaction = (delivery_id: any) => {
async handleRedirect() { // Add guard to prevent undefined customer ID API calls
console.log('[DEBUG] Starting redirect logic...'); if (!delivery_id || !customer.value || !customer.value.id) {
console.log('[DEBUG] payment_type:', this.deliveryOrder.payment_type); console.log("Skipping transaction fetch - delivery or customer data not available");
console.log('[DEBUG] transaction:', this.transaction); return;
console.log('[DEBUG] customer:', this.customer); }
if (this.deliveryOrder.payment_type === 1) {
// payment_type 1: Manual charging - already charged, just redirect to profile
console.log('[DEBUG] payment_type 1 - redirecting to customer profile');
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
} else if (this.deliveryOrder.payment_type === 11) {
// payment_type 11: API charging - check transaction type
console.log('[DEBUG] payment_type 11 - checking transaction type');
if (this.transaction) {
console.log('[DEBUG] Transaction found, type:', this.transaction.transaction_type);
if (this.transaction.transaction_type === 0) {
// Already charged (transaction_type = 0) - redirect to profile
console.log('[DEBUG] Already charged - redirecting to customer profile');
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
} else if (this.transaction.transaction_type === 1) {
// Auth only (transaction_type = 1) - redirect to capture page
console.log('[DEBUG] Auth only - redirecting to capture page');
this.$router.push({ name: "captureAuthorize", params: { id: this.deliveryOrder.id } });
} else {
// Unknown transaction type - default to customer profile
console.log('[DEBUG] Unknown transaction type - redirecting to customer profile');
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
}
// Consistent with delivery/view.vue - use customer transaction endpoint
const path = `${import.meta.env.VITE_BASE_URL}/payment/transactions/customer/${customer.value.id}/1`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
console.log("Transaction API response:", response.data);
// Handle both single transaction object and array responses
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
// Find the transaction for this specific delivery
const deliveryTransaction = response.data.find((txn: any) =>
txn.delivery_id === parseInt(delivery_id) ||
txn.transaction_id === delivery_id ||
txn.delivery_number === delivery_id
);
transaction.value = deliveryTransaction || null;
} else if (response.data && !Array.isArray(response.data)) {
// If single transaction, check if it's for this delivery
const txn = response.data;
if (txn.delivery_id === parseInt(delivery_id) ||
txn.transaction_id === delivery_id ||
txn.delivery_number === delivery_id) {
transaction.value = txn;
} else { } else {
// No transaction found for payment_type 11 - redirect to customer profile transaction.value = null;
console.log('[DEBUG] No transaction found for payment_type 11 - redirecting to customer profile');
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
} }
} else {
transaction.value = null;
}
} else if ([2].includes(this.deliveryOrder.payment_type)) { if (!transaction.value) {
// payment_type 2: Credit Card & Cash - go to capture page for any remaining payment console.log(`No transaction found for delivery ${delivery_id} among customer transactions`);
console.log('[DEBUG] payment_type 2 - redirecting to capture page'); }
this.$router.push({ name: "captureAuthorize", params: { id: this.deliveryOrder.id } }); })
.catch((error: any) => {
// Handle various error responses gracefully
if (error.response && error.response.status === 404) {
console.log(`No transactions found for customer ${customer.value.id}`);
transaction.value = null;
} else if (error.response && error.response.status === 400) {
console.log(`Bad request for customer transactions: ${error.response.data?.detail || error.message}`);
transaction.value = null;
} else {
console.error("Error fetching transaction:", error);
transaction.value = null;
}
});
}
const getTypeColor = (transactionType: number) => {
switch (transactionType) {
case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge
case 2: return 'text-purple-600'; // Capture
case 3: return 'text-green-600'; // Delivery/Other
default: return 'text-gray-600';
}
}
const CreateTransaction = () => {
const path = `${import.meta.env.VITE_MONEY_URL}/delivery/add/${deliveryOrder.value.id}`;
axios.post(path, {}, { withCredentials: true, headers: authHeader() })
.then(() => notify({ title: "Success", text: "Accounting record created.", type: "success" }))
.catch(() => notify({ title: "Warning", text: "Could not create accounting record.", type: "warn" }));
}
const onSubmit = async () => {
if (Number(FinalizeOilOrderForm.value.gallons_delivered) <= 0) {
notify({ title: "Validation Error", text: "Gallons delivered must be greater than zero.", type: "error" });
return;
}
isLoading.value = true;
const finalizePayload = {
gallons_delivered: FinalizeOilOrderForm.value.gallons_delivered,
fill_location: FinalizeOilOrderForm.value.fill_location,
cash_recieved: FinalizeOilOrderForm.value.cash_recieved,
check_number: FinalizeOilOrderForm.value.check_number,
};
const finalizePath = `${import.meta.env.VITE_BASE_URL}/deliverydata/finalize/${deliveryOrder.value.id}`;
try {
const finalizeResponse = await axios.put(finalizePath, finalizePayload, { withCredentials: true, headers: authHeader() });
if (!finalizeResponse.data.ok) {
throw new Error(finalizeResponse.data.error || "Failed to update delivery details.");
}
CreateTransaction();
notify({ title: "Success", text: "Ticket has been finalized.", type: "success" });
// FIX: Wait for customer data to be loaded before redirecting
await waitForCustomerData(deliveryOrder.value.customer_id);
// Updated redirect logic based on your requirements
await handleRedirect();
} catch (error: any) {
const errorMessage = error.response?.data?.detail || "An error occurred during finalization.";
notify({ title: "Error", text: errorMessage, type: "error" });
} finally {
isLoading.value = false;
}
}
// NEW: Wait for customer data to be loaded before redirecting
const waitForCustomerData = async (customerId: number): Promise<void> => {
return new Promise((resolve) => {
const checkCustomer = () => {
if (customer.value && customer.value.id && customer.value.id === customerId) {
resolve();
} else {
setTimeout(checkCustomer, 100);
}
};
checkCustomer();
});
}
// NEW: Updated redirect logic based on payment type and transaction status
const handleRedirect = async () => {
console.log('[DEBUG] Starting redirect logic...');
console.log('[DEBUG] payment_type:', deliveryOrder.value.payment_type);
console.log('[DEBUG] transaction:', transaction.value);
console.log('[DEBUG] customer:', customer.value);
if (deliveryOrder.value.payment_type === 1) {
// payment_type 1: Manual charging - already charged, just redirect to profile
console.log('[DEBUG] payment_type 1 - redirecting to customer profile');
router.push({ name: "customerProfile", params: { id: customer.value.id } });
} else if (deliveryOrder.value.payment_type === 11) {
// payment_type 11: API charging - check transaction type
console.log('[DEBUG] payment_type 11 - checking transaction type');
if (transaction.value) {
console.log('[DEBUG] Transaction found, type:', transaction.value.transaction_type);
if (transaction.value.transaction_type === 0) {
// Already charged (transaction_type = 0) - redirect to profile
console.log('[DEBUG] Already charged - redirecting to customer profile');
router.push({ name: "customerProfile", params: { id: customer.value.id } });
} else if (transaction.value.transaction_type === 1) {
// Auth only (transaction_type = 1) - redirect to capture page
console.log('[DEBUG] Auth only - redirecting to capture page');
router.push({ name: "captureAuthorize", params: { id: deliveryOrder.value.id } });
} else { } else {
// Default case (cash, check, etc.) - redirect to customer profile // Unknown transaction type - default to customer profile
console.log('[DEBUG] Default payment type - redirecting to customer profile'); console.log('[DEBUG] Unknown transaction type - redirecting to customer profile');
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } }); router.push({ name: "customerProfile", params: { id: customer.value.id } });
} }
},
}, } else {
}); // No transaction found for payment_type 11 - redirect to customer profile
console.log('[DEBUG] No transaction found for payment_type 11 - redirecting to customer profile');
router.push({ name: "customerProfile", params: { id: customer.value.id } });
}
} else if ([2].includes(deliveryOrder.value.payment_type)) {
// payment_type 2: Credit Card & Cash - go to capture page for any remaining payment
console.log('[DEBUG] payment_type 2 - redirecting to capture page');
router.push({ name: "captureAuthorize", params: { id: deliveryOrder.value.id } });
} else {
// Default case (cash, check, etc.) - redirect to customer profile
console.log('[DEBUG] Default payment type - redirecting to customer profile');
router.push({ name: "customerProfile", params: { id: customer.value.id } });
}
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -78,7 +78,7 @@
<div> <div>
<div class="font-bold text-sm">Payment Method</div> <div class="font-bold text-sm">Payment Method</div>
<div v-if="!userCardfound" class="text-warning text-sm">No card on file for this customer.</div> <div v-if="!userCardfound" class="text-warning text-sm">No card on file for this customer.</div>
<div v-if="userCardfound" class="mt-2 p-3 rounded-lg border bg-primary/10 border-primary"> <div v-if="userCardfound && userCard" class="mt-2 p-3 rounded-lg border bg-primary/10 border-primary">
<div class="font-bold text-sm">{{ userCard.name_on_card }}</div> <div class="font-bold text-sm">{{ 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 class="mt-1 text-sm font-mono tracking-wider"> <div class="mt-1 text-sm font-mono tracking-wider">
@@ -119,471 +119,458 @@
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted, watch } from 'vue'
import axios from 'axios' import { useRoute, useRouter } from 'vue-router'
import authHeader from '../../../services/auth.header' import axios from 'axios'
import Header from '../../../layouts/headers/headerauth.vue' import authHeader from '../../../services/auth.header'
import SideBar from '../../../layouts/sidebar/sidebar.vue' import Header from '../../../layouts/headers/headerauth.vue'
import Footer from '../../../layouts/footers/footer.vue' import SideBar from '../../../layouts/sidebar/sidebar.vue'
import useValidate from "@vuelidate/core"; import Footer from '../../../layouts/footers/footer.vue'
import { notify } from "@kyvg/vue3-notification" import useValidate from "@vuelidate/core";
import { notify } from "@kyvg/vue3-notification"
export default defineComponent({
name: 'finalizeTicketAuto',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
v$: useValidate(),
loaded: false,
user: {
id: 0
},
userCardfound: false,
deliveryStatus: [],
userCards: [],
deliveryNotesDriver: [],
today_oil_price: 0,
FinalizeOilOrderForm: {
fill_location: 0,
check_number: 0,
delivery_status: 10,
userCards: [],
credit_card_id: 0,
driver: 0,
gallons_delivered: '',
customer_filled: false,
prime: false,
same_day: false,
emergency: false,
},
CreateOilOrderForm: {
basicInfo: {
gallons_delivered: '',
userCards: []
},
},
userCard: {
date_added: '',
user_id: '',
card_number: '',
last_four_digits: '',
name_on_card: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
accepted_or_declined: '',
main_card: '',
},
customer: {
id: 0,
user_id: 0,
customer_address: '',
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
},
customerDescription: {
customer_id: 0,
account_number: '',
company_id: 0,
fill_location: 0,
description: '',
},
// Route and router
const route = useRoute()
const router = useRouter()
autoTicket: { // Reactive data
id: 0, const v$ = useValidate()
customer_id: '', const loaded = ref(false)
account_number: '', const user = ref({
id: 0
})
const userCardfound = ref(false)
const deliveryStatus = ref([])
const userCards = ref([])
const deliveryNotesDriver = ref([])
const today_oil_price = ref(0)
customer_town : '', const FinalizeOilOrderForm = ref({
customer_state : '', fill_location: 0,
customer_address : '', check_number: 0,
customer_zip: '', delivery_status: 10,
customer_full_name : '', userCards: [],
credit_card_id: 0,
driver: 0,
gallons_delivered: '',
customer_filled: false,
prime: false,
same_day: false,
emergency: false,
})
const CreateOilOrderForm = ref({
basicInfo: {
gallons_delivered: '',
userCards: []
},
})
const userCard = ref<{
date_added: string;
user_id: string;
card_number: string;
last_four_digits: string;
name_on_card: string;
expiration_month: string;
expiration_year: string;
type_of_card: string;
security_number: string;
accepted_or_declined: string;
main_card: string;
} | null>(null)
const customer = ref({
id: 0,
user_id: 0,
customer_address: '',
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
})
const customerDescription = ref({
customer_id: 0,
account_number: '',
company_id: 0,
fill_location: 0,
description: '',
})
oil_prices_id : '', const autoTicket = ref({
fill_date : '', id: 0,
gallons_delivered : '', customer_id: '',
price_per_gallon : '', account_number: '',
total_amount_customer : '', customer_town : '',
customer_state : '',
customer_address : '',
customer_zip: '',
customer_full_name : '',
payment_type : '', oil_prices_id : '',
payment_card_id : '', fill_date : '',
payment_status : '', gallons_delivered : '',
open_ticket_id: 0 price_per_gallon : '',
}, total_amount_customer : '',
autoDelivery: { payment_type : '',
id: 0, payment_card_id : '',
customer_id: 0, payment_status : '',
account_number: '', open_ticket_id: 0
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_full_name: '',
last_fill: '',
days_since_last_fill: 0,
last_updated: '',
estimated_gallons_left: 0,
estimated_gallons_left_prev_day: 0,
tank_height: '',
tank_size: '',
house_factor: 0,
auto_status: 0,
open_ticket_id: null,
},
}
},
created() {
this.userStatus()
},
watch: {
$route() {
this.today_price_oil();
this.getAutoTicket(this.$route.params.id);
},
},
mounted() {
this.today_price_oil();
this.getAutoTicket(this.$route.params.id);
})
},
methods: {
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;
this.user.id = response.data.user_id;
}
})
},
getPaymentCard(card_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data.userCard.card_number === ''){
this.userCard === null;
this.userCardfound = false;
}
else{
this.userCard = response.data;
this.userCardfound = true;
}
this.FinalizeOilOrderForm.userCards = response.data.id
})
.catch(() => {
});
},
getPaymentCards(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/payment/cards/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.userCards = response.data;
if (this.userCards && this.userCards.length > 0) {
this.userCardfound = true;
this.userCard = this.userCards.find((card: any) => card.main_card) || this.userCards[0];
}
})
.catch(() => {
});
},
getCustomer(user_id: any) {
if (!user_id || user_id === 'undefined') return;
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.customer = response.data;
if (this.customer.id > 0) {
this.getPaymentCards(this.customer.user_id || this.customer.id);
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
this.customer = { id: 0, user_id: 0, customer_address: '', customer_first_name: '', customer_last_name: '', customer_town: '', customer_state: 0, customer_zip: '', customer_apt: '', customer_home_type: 0, customer_phone_number: '' };
});
},
getCustomerDescription(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/customer/description/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.customerDescription = response.data;
this.loaded = true
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
});
},
getAutoTicket(delivery_id: any) {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/autoticket/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.autoTicket = response.data;
this.getCustomer(this.autoTicket.customer_id)
this.getAutoDelivery(this.autoTicket.id) const autoDelivery = ref({
this.getCustomerDescription(this.autoTicket.customer_id) id: 0,
customer_id: 0,
}) account_number: '',
.catch(() => { customer_town: '',
notify({ customer_state: 0,
title: "Error", customer_address: '',
text: "Could not get automatic", customer_zip: '',
type: "error", customer_full_name: '',
}); last_fill: '',
}); days_since_last_fill: 0,
}, last_updated: '',
estimated_gallons_left: 0,
getAutoDelivery(delivery_id: any) { estimated_gallons_left_prev_day: 0,
let path = import.meta.env.VITE_AUTO_URL + "/delivery/finddelivery/" + delivery_id; tank_height: '',
axios({ tank_size: '',
method: "get", house_factor: 0,
url: path, auto_status: 0,
withCredentials: true, open_ticket_id: null,
}) })
.then((response: any) => {
this.autoDelivery = response.data; // Watchers
this.getCustomer(this.autoDelivery.customer_id) watch(() => route.params, () => {
this.getCustomerDescription(this.autoDelivery.customer_id) today_price_oil()
getAutoTicket(route.params.id)
}) }, { immediate: false })
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
},
today_price_oil() {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.today_oil_price = response.data.price_for_customer;
})
},
UpdateAuto(payload: {
gallons: string,
delivery_id: string,
}) {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/delivery"
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
notify({
text: 'Update',
type: 'postive',
title: 'top'
})
this.$router.push({ name: "auto" });
}
else {
notify({
text: 'Auto Failure',
type: 'negative',
title: 'Update'
})
}
})
},
// Lifecycle
ConfirmAuto(payload: { onMounted(() => {
gallons_delivered: string, userStatus()
}) { today_price_oil()
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/create/" + this.autoDelivery.id; getAutoTicket(route.params.id)
axios({ })
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
notify({ // Functions
title: "Success", const userStatus = () => {
text: "Auto Delivered", let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
type: "success", axios({
}); method: 'get',
this.CreateTransaction(response.data['0']['auto_ticket_id']) url: path,
this.updateTransactionDelivery(this.autoDelivery.id, response.data['0']['auto_ticket_id']) withCredentials: true,
this.$router.push({ name: "payAutoCapture", params: { id: response.data['0']['auto_ticket_id'] } }); headers: authHeader(),
}
if (response.data.error) {
notify({
title: "Error",
text: "Could not finalize auto",
type: "error",
});
this.$router.push("auto");
}
})
},
closeTicket(ticketId: number) {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/close_ticket/" + ticketId;
axios({
method: "put",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then(() => {
// Ticket closed successfully
})
},
UpdateDeliveredAuto(payload: {
gallons_delivered: string,
}) {
console.log(this.autoDelivery)
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/update/" + this.autoDelivery.id;
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
notify({
title: "Success",
text: "Auto Updated",
type: "success",
});
// Removed redirect from here, will handle in onSubmit
}
})
},
updateTransactionDelivery(current_delivery_id: any, new_delivery_id: any) {
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/auto/transaction/delivery/${current_delivery_id}/update/${new_delivery_id}`;
axios.put(path, {}, { withCredentials: true, headers: authHeader() })
.then(() => console.log("Transaction auto_id updated"))
.catch(() => console.error("Error updating transaction auto_id"));
},
CreateTransaction(auto_ticket_id: string,) {
let path = import.meta.env.VITE_MONEY_URL + "/delivery/add/auto/" + auto_ticket_id;
axios({
method: "post",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.status == 201) {
notify({
message: 'Confirmed Transaction',
type: 'positive',
position: 'top'
})
}
else {
notify({
message: 'Form Error',
type: 'negative',
position: 'top'
})
}
})
},
onSubmit() {
let payload = {
gallons_delivered: this.FinalizeOilOrderForm.gallons_delivered,
};
this.UpdateDeliveredAuto(payload);
if (this.autoTicket.payment_status == '1') {
// Pre-authorized: redirect to capture page
this.$router.push({ name: "payAutoCapture", params: { id: this.autoTicket.id } });
} else {
// Fully charged: close ticket
if (this.autoDelivery.open_ticket_id) {
this.closeTicket(this.autoDelivery.open_ticket_id);
}
this.$router.push({ name: "auto" });
}
},
},
}) })
</script> .then((response: any) => {
if (response.data.ok) {
<style scoped></style> user.value = response.data.user;
user.value.id = response.data.user_id;
}
})
}
const getPaymentCard = (card_id: any) => {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data.userCard.card_number === ''){
userCard.value = null;
userCardfound.value = false;
}
else{
userCard.value = response.data;
userCardfound.value = true;
}
FinalizeOilOrderForm.value.userCards = response.data.id
})
.catch(() => {
});
}
const getPaymentCards = (user_id: any) => {
let path = import.meta.env.VITE_BASE_URL + "/payment/cards/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
userCards.value = response.data;
if (userCards.value && userCards.value.length > 0) {
userCardfound.value = true;
userCard.value = userCards.value.find((card: any) => card.main_card) || userCards.value[0];
}
})
.catch(() => {
});
}
const getCustomer = (user_id: any) => {
if (!user_id || user_id === 'undefined') return;
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
customer.value = response.data;
if (customer.value.id > 0) {
getPaymentCards(customer.value.user_id || customer.value.id);
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
customer.value = { id: 0, user_id: 0, customer_address: '', customer_first_name: '', customer_last_name: '', customer_town: '', customer_state: 0, customer_zip: '', customer_apt: '', customer_home_type: 0, customer_phone_number: '' };
});
}
const getCustomerDescription = (user_id: any) => {
let path = import.meta.env.VITE_BASE_URL + "/customer/description/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
customerDescription.value = response.data;
loaded.value = true
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
});
}
const getAutoTicket = (delivery_id: any) => {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/autoticket/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
autoTicket.value = response.data;
getCustomer(autoTicket.value.customer_id)
getAutoDelivery(autoTicket.value.id)
getCustomerDescription(autoTicket.value.customer_id)
})
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
}
const getAutoDelivery = (delivery_id: any) => {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/finddelivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
autoDelivery.value = response.data;
getCustomer(autoDelivery.value.customer_id)
getCustomerDescription(autoDelivery.value.customer_id)
})
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
}
const today_price_oil = () => {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
today_oil_price.value = response.data.price_for_customer;
})
}
const UpdateAuto = (payload: {
gallons: string,
delivery_id: string,
}) => {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/delivery"
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
notify({
text: 'Update',
type: 'postive',
title: 'top'
})
router.push({ name: "auto" });
}
else {
notify({
text: 'Auto Failure',
type: 'negative',
title: 'Update'
})
}
})
}
const ConfirmAuto = (payload: {
gallons_delivered: string,
}) => {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/create/" + autoDelivery.value.id;
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
notify({
title: "Success",
text: "Auto Delivered",
type: "success",
});
CreateTransaction(response.data['0']['auto_ticket_id'])
updateTransactionDelivery(autoDelivery.value.id, response.data['0']['auto_ticket_id'])
router.push({ name: "payAutoCapture", params: { id: response.data['0']['auto_ticket_id'] } });
}
if (response.data.error) {
notify({
title: "Error",
text: "Could not finalize auto",
type: "error",
});
router.push("auto");
}
})
}
const closeTicket = (ticketId: number) => {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/close_ticket/" + ticketId;
axios({
method: "put",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then(() => {
// Ticket closed successfully
})
}
const UpdateDeliveredAuto = (payload: {
gallons_delivered: string,
}) => {
console.log(autoDelivery.value)
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/update/" + autoDelivery.value.id;
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
notify({
title: "Success",
text: "Auto Updated",
type: "success",
});
// Removed redirect from here, will handle in onSubmit
}
})
}
const updateTransactionDelivery = (current_delivery_id: any, new_delivery_id: any) => {
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/auto/transaction/delivery/${current_delivery_id}/update/${new_delivery_id}`;
axios.put(path, {}, { withCredentials: true, headers: authHeader() })
.then(() => console.log("Transaction auto_id updated"))
.catch(() => console.error("Error updating transaction auto_id"));
}
const CreateTransaction = (auto_ticket_id: string) => {
let path = import.meta.env.VITE_MONEY_URL + "/delivery/add/auto/" + auto_ticket_id;
axios({
method: "post",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.status == 201) {
notify({
message: 'Confirmed Transaction',
type: 'positive',
position: 'top'
})
}
else {
notify({
message: 'Form Error',
type: 'negative',
position: 'top'
})
}
})
}
const onSubmit = () => {
let payload = {
gallons_delivered: FinalizeOilOrderForm.value.gallons_delivered,
};
UpdateDeliveredAuto(payload);
if (autoTicket.value.payment_status == '1') {
// Pre-authorized: redirect to capture page
router.push({ name: "payAutoCapture", params: { id: autoTicket.value.id } });
} else {
// Fully charged: close ticket
if (autoDelivery.value.open_ticket_id) {
closeTicket(autoDelivery.value.open_ticket_id);
}
router.push({ name: "auto" });
}
</script>
<style scoped></style>

View File

@@ -78,7 +78,7 @@
<div> <div>
<div class="font-bold text-sm">Payment Method</div> <div class="font-bold text-sm">Payment Method</div>
<div v-if="!userCardfound" class="text-warning text-sm">No card on file for this customer.</div> <div v-if="!userCardfound" class="text-warning text-sm">No card on file for this customer.</div>
<div v-if="userCardfound" class="mt-2 p-3 rounded-lg border bg-primary/10 border-primary"> <div v-if="userCardfound && userCard" class="mt-2 p-3 rounded-lg border bg-primary/10 border-primary">
<div class="font-bold text-sm">{{ userCard.name_on_card }}</div> <div class="font-bold text-sm">{{ 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 class="mt-1 text-sm font-mono tracking-wider"> <div class="mt-1 text-sm font-mono tracking-wider">
@@ -121,8 +121,9 @@
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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'
@@ -131,408 +132,397 @@ import Footer from '../../../layouts/footers/footer.vue'
import useValidate from "@vuelidate/core"; import useValidate from "@vuelidate/core";
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
// Route and router
const route = useRoute()
const router = useRouter()
export default defineComponent({ // Reactive data
name: 'finalizeTicketAuto', const v$ = useValidate()
const loaded = ref(false)
components: { const user = ref({
Header, id: 0
SideBar, })
Footer, const userCardfound = ref(false)
}, const deliveryStatus = ref([])
const userCards = ref([])
data() { const deliveryNotesDriver = ref([])
return { const today_oil_price = ref(0)
v$: useValidate(),
loaded: false,
user: {
id: 0
},
userCardfound: false,
deliveryStatus: [],
userCards: [],
deliveryNotesDriver: [],
today_oil_price: 0,
FinalizeOilOrderForm: {
fill_location: 0,
check_number: 0,
delivery_status: 10,
userCards: [],
credit_card_id: 0,
driver: 0,
gallons_delivered: '',
customer_filled: false,
prime: false,
same_day: false,
emergency: false,
},
CreateOilOrderForm: {
basicInfo: {
gallons_delivered: '',
userCards: []
},
},
userCard: {
date_added: '',
user_id: '',
card_number: '',
last_four_digits: '',
name_on_card: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
accepted_or_declined: '',
main_card: '',
},
customer: {
id: 0,
user_id: 0,
customer_address: '',
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
},
customerDescription: {
customer_id: 0,
account_number: '',
company_id: 0,
fill_location: 0,
description: '',
},
autoTicket: {
id: 0,
customer_id: '',
account_number: '',
customer_town: '',
customer_state: '',
customer_address: '',
customer_zip: '',
customer_full_name: '',
oil_prices_id: '',
fill_date: '',
gallons_delivered: '',
price_per_gallon: '',
total_amount_customer: '',
payment_type: '',
payment_card_id: '',
payment_status: '',
open_ticket_id: 0
},
autoDelivery: {
id: 0,
customer_id: 0,
account_number: '',
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_full_name: '',
last_fill: '',
days_since_last_fill: 0,
last_updated: '',
estimated_gallons_left: 0,
estimated_gallons_left_prev_day: 0,
tank_height: '',
tank_size: '',
house_factor: 0,
auto_status: 0,
open_ticket_id: null,
},
}
},
created() {
this.userStatus()
},
watch: {
$route() {
this.today_price_oil();
this.getAutoTicket(this.$route.params.id);
},
},
mounted() {
this.today_price_oil();
this.getAutoDelivery(this.$route.params.id);
},
methods: {
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;
this.user.id = response.data.user_id;
}
})
},
getPaymentCard(card_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data.userCard.card_number === '') {
this.userCard === null;
this.userCardfound = false;
}
else {
this.userCard = response.data;
this.userCardfound = true;
}
this.FinalizeOilOrderForm.userCards = response.data.id
})
.catch(() => {
});
},
getPaymentCards(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/payment/cards/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.userCards = response.data;
if (this.userCards && this.userCards.length > 0) {
this.userCardfound = true;
this.userCard = this.userCards.find((card: any) => card.main_card) || this.userCards[0];
}
})
.catch(() => {
});
},
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
})
},
getCreditCards(userid: any) {
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.userCards = response.data;
if (this.userCards && this.userCards.length > 0) {
this.userCardfound = true;
this.userCard = this.userCards.find((card: any) => card.main_card) || this.userCards[0];
}
})
},
getCustomerDescription(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/customer/description/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.customerDescription = response.data;
this.loaded = true
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
});
},
getAutoTicket(delivery_id: any) {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/autoticket/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.autoTicket = response.data;
this.getCustomer(this.autoTicket.customer_id)
this.getAutoDelivery(this.autoTicket.id)
this.getCustomerDescription(this.autoTicket.customer_id)
})
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
},
getAutoDelivery(delivery_id: any) {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/delivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data && response.data.customer_id) {
this.autoDelivery = response.data;
this.getCustomer(this.autoDelivery.customer_id)
this.getCreditCards(this.autoDelivery.customer_id)
} else {
console.error("API Error:", response.data.error || "Failed to fetch auto delivery data.");
}
})
.catch((error: any) => {
console.error("API Error in getAutoDelivery:", error);
notify({
title: "Error",
text: "Could not get automatic delivery",
type: "error",
});
});
},
today_price_oil() {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.today_oil_price = response.data.price_for_customer;
})
},
UpdateAuto(payload: {
gallons: string,
delivery_id: string,
}) {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/delivery"
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
notify({
text: 'Update',
type: 'postive',
title: 'top'
})
this.$router.push({ name: "auto" });
}
else {
notify({
text: 'Auto Failure',
type: 'negative',
title: 'Update'
})
}
})
},
CreateTransaction(auto_ticket_id: string,) {
let path = import.meta.env.VITE_MONEY_URL + "/delivery/add/auto/" + auto_ticket_id;
axios({
method: "post",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.status == 201) {
notify({
message: 'Confirmed Transaction',
type: 'positive',
position: 'top'
})
}
else {
notify({
message: 'Form Error',
type: 'negative',
position: 'top'
})
}
})
},
ConfirmAuto(payload: {
gallons_delivered: string,
}) {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/create/nopreauth/" + this.autoDelivery.id;
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
notify({
title: "Success",
text: "Auto Delivered",
type: "success",
});
this.CreateTransaction(response.data['0']['auto_ticket_id'])
}
if (response.data.error) {
notify({
title: "Error",
text: "Could not finalize auto",
type: "error",
});
this.$router.push("auto");
}
})
},
onSubmit() {
let payload = {
gallons_delivered: this.FinalizeOilOrderForm.gallons_delivered,
};
this.ConfirmAuto(payload)
this.$router.push({ name: "auto" });
},
const FinalizeOilOrderForm = ref({
fill_location: 0,
check_number: 0,
delivery_status: 10,
userCards: [],
credit_card_id: 0,
driver: 0,
gallons_delivered: '',
customer_filled: false,
prime: false,
same_day: false,
emergency: false,
})
const CreateOilOrderForm = ref({
basicInfo: {
gallons_delivered: '',
userCards: []
}, },
}) })
const userCard = ref<{
date_added: string;
user_id: string;
card_number: string;
last_four_digits: string;
name_on_card: string;
expiration_month: string;
expiration_year: string;
type_of_card: string;
security_number: string;
accepted_or_declined: string;
main_card: string;
} | null>(null)
const customer = ref({
id: 0,
user_id: 0,
customer_address: '',
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
})
const customerDescription = ref({
customer_id: 0,
account_number: '',
company_id: 0,
fill_location: 0,
description: '',
})
const autoTicket = ref({
id: 0,
customer_id: '',
account_number: '',
customer_town: '',
customer_state: '',
customer_address: '',
customer_zip: '',
customer_full_name: '',
oil_prices_id: '',
fill_date: '',
gallons_delivered: '',
price_per_gallon: '',
total_amount_customer: '',
payment_type: '',
payment_card_id: '',
payment_status: '',
open_ticket_id: 0
})
const autoDelivery = ref({
id: 0,
customer_id: 0,
account_number: '',
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_full_name: '',
last_fill: '',
days_since_last_fill: 0,
last_updated: '',
estimated_gallons_left: 0,
estimated_gallons_left_prev_day: 0,
tank_height: '',
tank_size: '',
house_factor: 0,
auto_status: 0,
open_ticket_id: null,
})
// Watchers
watch(() => route.params, () => {
today_price_oil()
getAutoTicket(route.params.id)
}, { immediate: false })
// Lifecycle
onMounted(() => {
userStatus()
today_price_oil()
getAutoDelivery(route.params.id)
})
// Functions
const 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) {
user.value = response.data.user;
user.value.id = response.data.user_id;
}
})
}
const getPaymentCard = (card_id: any) => {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data.userCard.card_number === '') {
userCard.value = null;
userCardfound.value = false;
}
else {
userCard.value = response.data;
userCardfound.value = true;
}
FinalizeOilOrderForm.value.userCards = response.data.id
})
.catch(() => {
});
}
const getPaymentCards = (user_id: any) => {
let path = import.meta.env.VITE_BASE_URL + "/payment/cards/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
userCards.value = response.data;
if (userCards.value && userCards.value.length > 0) {
userCardfound.value = true;
userCard.value = userCards.value.find((card: any) => card.main_card) || userCards.value[0];
}
})
.catch(() => {
});
}
const getCustomer = (userid: any) => {
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
customer.value = response.data
})
}
const getCreditCards = (userid: any) => {
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
userCards.value = response.data;
if (userCards.value && userCards.value.length > 0) {
userCardfound.value = true;
userCard.value = userCards.value.find((card: any) => card.main_card) || userCards.value[0];
}
})
}
const getCustomerDescription = (user_id: any) => {
let path = import.meta.env.VITE_BASE_URL + "/customer/description/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
customerDescription.value = response.data;
loaded.value = true
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
});
}
const getAutoTicket = (delivery_id: any) => {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/autoticket/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
autoTicket.value = response.data;
getCustomer(autoTicket.value.customer_id)
getAutoDelivery(autoTicket.value.id)
getCustomerDescription(autoTicket.value.customer_id)
})
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
}
const getAutoDelivery = (delivery_id: any) => {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/delivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data && response.data.customer_id) {
autoDelivery.value = response.data;
getCustomer(autoDelivery.value.customer_id)
getCreditCards(autoDelivery.value.customer_id)
} else {
console.error("API Error:", response.data.error || "Failed to fetch auto delivery data.");
}
})
.catch((error: any) => {
console.error("API Error in getAutoDelivery:", error);
notify({
title: "Error",
text: "Could not get automatic delivery",
type: "error",
});
});
}
const today_price_oil = () => {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
today_oil_price.value = response.data.price_for_customer;
})
}
const UpdateAuto = (payload: {
gallons: string,
delivery_id: string,
}) => {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/delivery"
axios({
method: "put",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data.ok) {
notify({
text: 'Update',
type: 'postive',
title: 'top'
})
router.push({ name: "auto" });
}
else {
notify({
text: 'Auto Failure',
type: 'negative',
title: 'Update'
})
}
})
}
const CreateTransaction = (auto_ticket_id: string) => {
let path = import.meta.env.VITE_MONEY_URL + "/delivery/add/auto/" + auto_ticket_id;
axios({
method: "post",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.status == 201) {
notify({
message: 'Confirmed Transaction',
type: 'positive',
position: 'top'
})
}
else {
notify({
message: 'Form Error',
type: 'negative',
position: 'top'
})
}
})
}
const ConfirmAuto = (payload: {
gallons_delivered: string,
}) => {
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/create/nopreauth/" + autoDelivery.value.id;
axios({
method: "post",
url: path,
data: payload,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
notify({
title: "Success",
text: "Auto Delivered",
type: "success",
});
CreateTransaction(response.data['0']['auto_ticket_id'])
}
if (response.data.error) {
notify({
title: "Error",
text: "Could not finalize auto",
type: "error",
});
router.push("auto");
}
})
}
const onSubmit = () => {
let payload = {
gallons_delivered: FinalizeOilOrderForm.value.gallons_delivered,
};
ConfirmAuto(payload)
router.push({ name: "auto" });
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -71,72 +71,54 @@
<Footer/> <Footer/>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import { ref, onMounted } 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'
// Reactive data
const token = ref(null)
const user = ref(null)
const deliveries = ref([])
export default defineComponent({ // Lifecycle
name: 'deliveryTicketsMissing', onMounted(() => {
userStatus()
components: { get_oil_orders()
Header,
SideBar,
Footer,
},
data() {
return {
token: null,
user: null,
deliveries: [
],
}
},
created() {
this.userStatus()
},
mounted() {
this.get_oil_orders()
},
methods: {
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;
}
})
.catch(() => {
this.user = null
})
},
get_oil_orders() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/pending';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
})
},
},
}) })
// Functions
const 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) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const get_oil_orders = () => {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/pending';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
deliveries.value = response.data
})
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -67,20 +67,20 @@
<div class="font-bold text-sm">Current Status</div> <div class="font-bold text-sm">Current Status</div>
<div class="badge badge-lg" <div class="badge badge-lg"
:class="{ :class="{
'badge-success': [1, 10].includes(deliveryOrder.delivery_status), 'badge-success': [DELIVERY_STATUS.DELIVERED, DELIVERY_STATUS.FINALIZED].includes(deliveryOrder.delivery_status as any),
'badge-info': deliveryOrder.delivery_status == 2, 'badge-info': deliveryOrder.delivery_status == DELIVERY_STATUS.OUT_FOR_DELIVERY,
'badge-error': deliveryOrder.delivery_status == 5, 'badge-error': deliveryOrder.delivery_status == DELIVERY_STATUS.ISSUE,
'badge-warning': ![1, 10, 2, 5].includes(deliveryOrder.delivery_status) 'badge-warning': ![DELIVERY_STATUS.DELIVERED, DELIVERY_STATUS.FINALIZED, DELIVERY_STATUS.OUT_FOR_DELIVERY, DELIVERY_STATUS.ISSUE].includes(deliveryOrder.delivery_status as any)
}"> }">
<span v-if="deliveryOrder.delivery_status == 0">Waiting</span> <span v-if="deliveryOrder.delivery_status == DELIVERY_STATUS.WAITING">Waiting</span>
<span v-else-if="deliveryOrder.delivery_status == 1">Delivered</span> <span v-else-if="deliveryOrder.delivery_status == DELIVERY_STATUS.DELIVERED">Delivered</span>
<span v-else-if="deliveryOrder.delivery_status == 2">Out_for_Delivery</span> <span v-else-if="deliveryOrder.delivery_status == DELIVERY_STATUS.OUT_FOR_DELIVERY">Out_for_Delivery</span>
<span v-else-if="deliveryOrder.delivery_status == 3">Tomorrow</span> <span v-else-if="deliveryOrder.delivery_status == DELIVERY_STATUS.TOMORROW">Tomorrow</span>
<span v-else-if="deliveryOrder.delivery_status == 4">Partial Delivery</span> <span v-else-if="deliveryOrder.delivery_status == DELIVERY_STATUS.PARTIAL_DELIVERY">Partial Delivery</span>
<span v-else-if="deliveryOrder.delivery_status == 5">Misdelivery</span> <span v-else-if="deliveryOrder.delivery_status == DELIVERY_STATUS.ISSUE">Misdelivery</span>
<span v-else-if="deliveryOrder.delivery_status == 6">Unknown</span> <span v-else-if="deliveryOrder.delivery_status == DELIVERY_STATUS.UNKNOWN">Unknown</span>
<span v-else-if="deliveryOrder.delivery_status == 9">Pending</span> <span v-else-if="deliveryOrder.delivery_status == DELIVERY_STATUS.PENDING_PAYMENT">Pending</span>
<span v-else-if="deliveryOrder.delivery_status == 10">Finalized</span> <span v-else-if="deliveryOrder.delivery_status == DELIVERY_STATUS.FINALIZED">Finalized</span>
</div> </div>
</div> </div>
<div> <div>
@@ -354,488 +354,480 @@
</div> </div>
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { DELIVERY_STATUS, PAYMENT_STATUS, TRANSACTION_STATUS } from '../../constants/status'
interface UserCard { import { CreditCard } from '../../types/models'
id: number;
last_four: string;
type_of_card: string;
expiration_month: number;
expiration_year: number;
name_on_card: string;
card_number: string;
security_number: string;
main_card?: boolean;
}
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 useValidate from "@vuelidate/core"; import useValidate from "@vuelidate/core";
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import moment from 'moment'; import dayjs from 'dayjs';
export default defineComponent({ const route = useRoute()
name: 'deliveryOrder',
components: { // Reactive data
Header, const v$ = useValidate()
SideBar, const user = ref({
Footer, user_id: 0
}, })
const priceprime = ref(0)
const pricesameday = ref(0)
const priceemergency = ref(0)
const total_amount = ref(0)
const discount = ref(0)
const total_amount_after_discount = ref(0)
const deliveryNotesDriver = ref([])
const userCardfound = ref(false)
const userCard = ref({} as CreditCard)
const customer = ref({
account_number: '',
id: 0,
user_id: 0,
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
})
data() { const deliveryMoney = ref({
return { time_added: '',
v$: useValidate(), total_amount_oil: '',
user: { total_amount_emergency: '',
user_id: 0 total_amount_same_day: '',
}, total_amount_prime: '',
priceprime: 0, total_amount_fee: '',
pricesameday: 0, total_discount_amount: '',
priceemergency: 0, total_discount_total: '',
total_amount: 0, total_amount: '',
discount: 0, })
total_amount_after_discount: 0, const promo = ref({
deliveryNotesDriver: [], id: 0,
userCardfound: false, name_of_promotion: '',
userCard: {} as UserCard, description: '',
customer: { money_off_delivery: '',
account_number: '', text_on_ticket: ''
id: 0, })
user_id: 0, const pricing = ref({
customer_first_name: '', price_from_supplier: 0,
customer_last_name: '', price_for_customer: 0,
customer_town: '', price_for_employee: 0,
customer_state: 0, price_same_day: 0,
customer_address: '', price_prime: 0,
customer_zip: '', price_emergency: 0,
customer_apt: '', date: "",
customer_home_type: 0, })
customer_phone_number: '', const deliveryOrder = ref({
}, id: '',
customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
gallons_ordered: 0,
customer_asked_for_fill: 0,
gallons_delivered: '',
customer_filled: 0,
delivery_status: 0,
when_ordered: '',
when_delivered: '',
expected_delivery_date: '',
automatic: 0,
oil_id: 0,
supplier_price: '',
customer_price: '',
customer_temperature: '',
dispatcher_notes: '',
prime: 0,
same_day: 0,
emergency: 0,
payment_type: 0,
payment_card_id: '',
driver_employee_id: 0,
driver_first_name: '',
driver_last_name: '',
promo_id: 0,
})
const transaction = ref(null as any)
deliveryMoney: { // Computed
time_added: '', const computedDELIVERY_STATUS = computed(() => DELIVERY_STATUS)
total_amount_oil: '', const computedPAYMENT_STATUS = computed(() => PAYMENT_STATUS)
total_amount_emergency: '', const computedTRANSACTION_STATUS = computed(() => TRANSACTION_STATUS)
total_amount_same_day: '',
total_amount_prime: '',
total_amount_fee: '',
total_discount_amount: '',
total_discount_total: '',
total_amount: '',
},
promo: {
id: 0,
name_of_promotion: '',
description: '',
money_off_delivery: '',
text_on_ticket: ''
},
pricing: {
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
date: "",
},
deliveryOrder: {
id: '',
customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
gallons_ordered: 0,
customer_asked_for_fill: 0,
gallons_delivered: '',
customer_filled: 0,
delivery_status: 0,
when_ordered: '',
when_delivered: '',
expected_delivery_date: '',
automatic: 0,
oil_id: 0,
supplier_price: '',
customer_price: '',
customer_temperature: '',
dispatcher_notes: '',
prime: 0,
same_day: 0,
emergency: 0,
payment_type: 0,
payment_card_id: '',
driver_employee_id: 0,
driver_first_name: '',
driver_last_name: '',
promo_id: 0,
},
transaction: null as any,
}
},
created() { // Watchers
this.userStatus() watch(route, () => {
}, getOilOrder(route.params.id);
watch: { getOilOrderMoney(route.params.id);
$route() { sumdelivery(route.params.id);
this.getOilOrder(this.$route.params.id); })
this.getOilOrderMoney(this.$route.params.id);
this.sumdelivery(this.$route.params.id);
},
},
mounted() {
this.getOilOrder(this.$route.params.id);
this.getOilOrderMoney(this.$route.params.id);
this.sumdelivery(this.$route.params.id);
this.getOilPricing();
},
methods: { // Lifecycle
format_date(value: string) { onMounted(() => {
if (value) { userStatus()
return moment(String(value)).format('LLLL') getOilOrder(route.params.id);
} getOilOrderMoney(route.params.id);
}, sumdelivery(route.params.id);
getTypeColor(transactionType: number) { getOilPricing();
switch (transactionType) { })
case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge // Functions
case 2: return 'text-green-600'; // Capture const format_date = (value: string) => {
case 3: return 'text-purple-600'; // Delivery/Other if (value) {
default: return 'text-gray-600'; return dayjs(String(value)).format('LLLL')
} }
}, }
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id; const getTypeColor = (transactionType: number) => {
axios({ switch (transactionType) {
method: 'delete', case 1: return 'text-blue-600'; // Auth
url: path, case 0: return 'text-orange-600'; // Charge
headers: authHeader(), case 2: return 'text-green-600'; // Capture
}).then((response: any) => { case 3: return 'text-purple-600'; // Delivery/Other
if (response.data.ok) { default: return 'text-gray-600';
notify({ }
title: "Success", }
text: "deleted delivery",
type: "success", const deleteCall = (delivery_id: any) => {
}); let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
this.$router.push({ name: "customerProfile", params: { id: this.customer.user_id } }); axios({
} else { method: 'delete',
notify({ url: path,
title: "Failure", headers: authHeader(),
text: "error deleting delivery", }).then((response: any) => {
type: "success", if (response.data.ok) {
}); notify({
} title: "Success",
}) text: "deleted delivery",
}, type: "success",
cancelDelivery() {
let path = import.meta.env.VITE_BASE_URL + '/delivery/cancel/' + this.deliveryOrder.id;
axios({
method: 'post',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "Delivery cancelled",
type: "success",
});
// Refresh the delivery data
this.getOilOrder(this.deliveryOrder.id);
} else {
notify({
title: "Failure",
text: "Failed to cancel delivery",
type: "error",
});
}
}).catch(() => {
notify({
title: "Error",
text: "Error cancelling delivery",
type: "error",
});
}); });
}, // Note: router.push would need to be imported and used
userStatus() { } else {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami'; notify({
axios({ title: "Failure",
method: 'get', text: "error deleting delivery",
url: path, type: "success",
withCredentials: true, });
headers: authHeader(), }
}) })
.then((response: any) => { }
if (response.data.ok) {
this.user = response.data.user; const cancelDelivery = () => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/cancel/' + deliveryOrder.value.id;
} axios({
}) method: 'post',
}, url: path,
getOilPricing() { headers: authHeader(),
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/table"; }).then((response: any) => {
axios({ if (response.data.ok) {
method: "get", notify({
url: path, title: "Success",
withCredentials: true, text: "Delivery cancelled",
}) type: "success",
.then((response: any) => { });
this.pricing = response.data; // Refresh the delivery data
}) getOilOrder(deliveryOrder.value.id);
.catch((_error: any) => { } else {
notify({ notify({
title: "Error", title: "Failure",
text: "Could not get oil pricing", text: "Failed to cancel delivery",
type: "error", type: "error",
}); });
}); }
}, }).catch(() => {
getCustomer(user_id: any) { notify({
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id; title: "Error",
axios({ text: "Error cancelling delivery",
method: "get", type: "error",
url: path, });
withCredentials: true, });
}) }
.then((response: any) => {
this.customer = response.data; const userStatus = () => {
}) let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
.catch((_error: any) => { axios({
notify({ method: 'get',
title: "Error", url: path,
text: "Could not find customer", withCredentials: true,
type: "error", headers: authHeader(),
}); })
}); .then((response: any) => {
}, if (response.data.ok) {
user.value = response.data.user;
getPaymentCard(card_id: any) {
if (card_id) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
// Check if we have valid card data
if (response.data && response.data.card_number && response.data.card_number !== '') {
this.userCard = response.data;
this.userCardfound = true;
} else {
this.userCard = {} as UserCard;
this.userCardfound = false;
}
})
.catch((error: any) => {
console.error("Error fetching payment card:", error);
this.userCard = {} as UserCard;
this.userCardfound = false;
});
} else {
this.userCardfound = false;
}
},
getOilOrder(delivery_id: any) {
if (!delivery_id) { // Add a guard to prevent calls with an undefined ID
console.error("getOilOrder called with no ID.");
return;
} }
let path = import.meta.env.VITE_BASE_URL + "/delivery/" + delivery_id; })
axios({ }
method: "get",
url: path, const getOilPricing = () => {
withCredentials: true, let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/table";
headers: authHeader(), axios({
}) method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
pricing.value = response.data;
})
.catch((_error: any) => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
}
const getCustomer = (user_id: any) => {
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
customer.value = response.data;
})
.catch((_error: any) => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
});
}
const getPaymentCard = (card_id: any) => {
if (card_id) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => { .then((response: any) => {
// FIX: Check for the 'ok' flag and access the nested 'delivery' object // Check if we have valid card data
if (response.data && response.data.ok) { if (response.data && response.data.card_number && response.data.card_number !== '') {
this.deliveryOrder = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE userCard.value = response.data;
userCardfound.value = true;
// Now that this.deliveryOrder is the correct object, the rest of the logic will work.
this.getCustomer(this.deliveryOrder.customer_id);
if ([1, 2, 3].includes(this.deliveryOrder.payment_type)) {
this.getPaymentCard(this.deliveryOrder.payment_card_id);
}
if (this.deliveryOrder.promo_id != null) {
this.getPromo(this.deliveryOrder.promo_id);
}
// Only fetch transactions for Authorize.net payments
if (this.deliveryOrder.payment_type == 11) {
this.getTransaction(delivery_id);
}
} else { } else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data."); userCard.value = {} as CreditCard;
notify({ title: "Error", text: "Could not load delivery details.", type: "error" }); userCardfound.value = false;
} }
}) })
.catch((error: any) => { .catch((error: any) => {
console.error("Error fetching delivery order:", error); console.error("Error fetching payment card:", error);
userCard.value = {} as CreditCard;
userCardfound.value = false;
}); });
}, } else {
getOilOrderMoney(delivery_id: any) { userCardfound.value = false;
let path = import.meta.env.VITE_MONEY_URL + "/delivery/order/money/" + delivery_id; }
axios({ }
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
if (response.data) {
this.deliveryMoney = response.data
}
})
},
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 && response.data.ok) {
this.priceprime = response.data.priceprime || 0;
this.pricesameday = response.data.pricesameday || 0;
this.priceemergency = response.data.priceemergency || 0;
this.total_amount = parseFloat(response.data.total_amount) || 0;
this.discount = parseFloat(response.data.discount) || 0;
this.total_amount_after_discount = parseFloat(response.data.total_amount_after_discount) || 0;
} else {
// Fallback calculation if API doesn't return expected data
this.calculateFallbackTotal();
}
})
.catch((error: any) => {
console.error("Error fetching delivery totals:", error);
// Fallback calculation on error
this.calculateFallbackTotal();
notify({
title: "Warning",
text: "Could not get delivery totals, using estimated calculation",
type: "warn",
});
});
},
calculateFallbackTotal() { const getOilOrder = (delivery_id: any) => {
// Fallback calculation using available data if (!delivery_id) { // Add a guard to prevent calls with an undefined ID
if (this.deliveryOrder.gallons_ordered && this.pricing.price_for_customer) { console.error("getOilOrder called with no ID.");
const gallons = Number(this.deliveryOrder.gallons_ordered); return;
const pricePerGallon = Number(this.pricing.price_for_customer); }
let total = gallons * pricePerGallon; let path = import.meta.env.VITE_BASE_URL + "/delivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
// FIX: Check for the 'ok' flag and access the nested 'delivery' object
if (response.data && response.data.ok) {
deliveryOrder.value = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE
if (this.deliveryOrder.prime == 1) { // Now that deliveryOrder is the correct object, the rest of the logic will work.
total += Number(this.pricing.price_prime) || 0; getCustomer(deliveryOrder.value.customer_id);
}
if (this.deliveryOrder.same_day == 1) {
total += Number(this.pricing.price_same_day) || 0;
}
if (this.deliveryOrder.emergency == 1) {
total += Number(this.pricing.price_emergency) || 0;
}
this.total_amount = total; if ([1, 2, 3].includes(deliveryOrder.value.payment_type)) {
this.total_amount_after_discount = total; // No discount info available getPaymentCard(deliveryOrder.value.payment_card_id);
this.discount = 0;
} }
}, if (deliveryOrder.value.promo_id != null) {
getPromo(deliveryOrder.value.promo_id);
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;
}
})
.catch((error: any) => {
console.error('Error fetching promo:', error);
})
},
calculateDeliveryTotal() {
if (!this.deliveryOrder.gallons_delivered || !this.pricing.price_for_customer) {
return '0.00';
} }
const gallons = Number(this.deliveryOrder.gallons_delivered); // Only fetch transactions for Authorize.net payments
const pricePerGallon = Number(this.pricing.price_for_customer); if (deliveryOrder.value.payment_type == 11) {
let total = gallons * pricePerGallon; getTransaction(delivery_id);
if (this.deliveryOrder.prime == 1) {
total += Number(this.pricing.price_prime) || 0;
}
if (this.deliveryOrder.same_day == 1) {
total += Number(this.pricing.price_same_day) || 0;
}
if (this.deliveryOrder.emergency == 1) {
total += Number(this.pricing.price_emergency) || 0;
} }
return total.toFixed(2); } else {
}, console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
calculateEstimatedTotal() { notify({ title: "Error", text: "Could not load delivery details.", type: "error" });
if (!this.deliveryOrder.gallons_ordered || !this.pricing.price_for_customer) { }
return 0; })
} .catch((error: any) => {
console.error("Error fetching delivery order:", error);
});
}
const gallons = Number(this.deliveryOrder.gallons_ordered); const getOilOrderMoney = (delivery_id: any) => {
const pricePerGallon = Number(this.pricing.price_for_customer); let path = import.meta.env.VITE_MONEY_URL + "/delivery/order/money/" + delivery_id;
let total = gallons * pricePerGallon; axios({
method: "get",
if (this.deliveryOrder.prime == 1) { url: path,
total += Number(this.pricing.price_prime) || 0; withCredentials: true,
} headers: authHeader(),
if (this.deliveryOrder.same_day == 1) { })
total += Number(this.pricing.price_same_day) || 0; .then((response: any) => {
} if (response.data) {
if (this.deliveryOrder.emergency == 1) { deliveryMoney.value = response.data
total += Number(this.pricing.price_emergency) || 0;
} }
})
}
return total; const sumdelivery = (delivery_id: any) => {
}, let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
axios({
getTransaction(delivery_id: any) { method: "get",
// Simple endpoint to get transaction directly by delivery_id url: path,
const path = `${import.meta.env.VITE_BASE_URL}/payment/transaction/delivery/${delivery_id}`; withCredentials: true,
axios.get(path, { })
withCredentials: true, .then((response: any) => {
headers: authHeader() if (response.data && response.data.ok) {
}).then((response: any) => { priceprime.value = response.data.priceprime || 0;
if (response.data.ok) { pricesameday.value = response.data.pricesameday || 0;
this.transaction = response.data.transaction; priceemergency.value = response.data.priceemergency || 0;
console.log("Transaction loaded:", this.transaction); total_amount.value = parseFloat(response.data.total_amount) || 0;
} else { discount.value = parseFloat(response.data.discount) || 0;
console.log("No transaction found for delivery:", delivery_id); total_amount_after_discount.value = parseFloat(response.data.total_amount_after_discount) || 0;
this.transaction = null; } else {
} // Fallback calculation if API doesn't return expected data
}).catch((error: any) => { calculateFallbackTotal();
console.error("Error fetching transaction:", error); }
this.transaction = null; })
.catch((error: any) => {
console.error("Error fetching delivery totals:", error);
// Fallback calculation on error
calculateFallbackTotal();
notify({
title: "Warning",
text: "Could not get delivery totals, using estimated calculation",
type: "warn",
}); });
}, });
}, }
})
const calculateFallbackTotal = () => {
// Fallback calculation using available data
if (deliveryOrder.value.gallons_ordered && pricing.value.price_for_customer) {
const gallons = Number(deliveryOrder.value.gallons_ordered);
const pricePerGallon = Number(pricing.value.price_for_customer);
let total = gallons * pricePerGallon;
if (deliveryOrder.value.prime == 1) {
total += Number(pricing.value.price_prime) || 0;
}
if (deliveryOrder.value.same_day == 1) {
total += Number(pricing.value.price_same_day) || 0;
}
if (deliveryOrder.value.emergency == 1) {
total += Number(pricing.value.price_emergency) || 0;
}
total_amount.value = total;
total_amount_after_discount.value = total; // No discount info available
discount.value = 0;
}
}
const 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) {
promo.value = response.data;
}
})
.catch((error: any) => {
console.error('Error fetching promo:', error);
})
}
const calculateDeliveryTotal = () => {
if (!deliveryOrder.value.gallons_delivered || !pricing.value.price_for_customer) {
return '0.00';
}
const gallons = Number(deliveryOrder.value.gallons_delivered);
const pricePerGallon = Number(pricing.value.price_for_customer);
let total = gallons * pricePerGallon;
if (deliveryOrder.value.prime == 1) {
total += Number(pricing.value.price_prime) || 0;
}
if (deliveryOrder.value.same_day == 1) {
total += Number(pricing.value.price_same_day) || 0;
}
if (deliveryOrder.value.emergency == 1) {
total += Number(pricing.value.price_emergency) || 0;
}
return total.toFixed(2);
}
const calculateEstimatedTotal = () => {
if (!deliveryOrder.value.gallons_ordered || !pricing.value.price_for_customer) {
return 0;
}
const gallons = Number(deliveryOrder.value.gallons_ordered);
const pricePerGallon = Number(pricing.value.price_for_customer);
let total = gallons * pricePerGallon;
if (deliveryOrder.value.prime == 1) {
total += Number(pricing.value.price_prime) || 0;
}
if (deliveryOrder.value.same_day == 1) {
total += Number(pricing.value.price_same_day) || 0;
}
if (deliveryOrder.value.emergency == 1) {
total += Number(pricing.value.price_emergency) || 0;
}
return total;
}
const getTransaction = (delivery_id: any) => {
// Simple endpoint to get transaction directly by delivery_id
const path = `${import.meta.env.VITE_BASE_URL}/payment/transaction/delivery/${delivery_id}`;
axios.get(path, {
withCredentials: true,
headers: authHeader()
}).then((response: any) => {
if (response.data.ok) {
transaction.value = response.data.transaction;
console.log("Transaction loaded:", transaction.value);
} else {
console.log("No transaction found for delivery:", delivery_id);
transaction.value = null;
}
}).catch((error: any) => {
console.error("Error fetching transaction:", error);
transaction.value = null;
});
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -121,104 +121,93 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { deliveryService } from '../../../services/deliveryService'
import { Delivery } from '../../../types/models'
import Header from '../../../layouts/headers/headerauth.vue' import Header from '../../../layouts/headers/headerauth.vue'
import PaginationComp from '../../../components/pagination.vue' import PaginationComp from '../../../components/pagination.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({ // Reactive data
name: 'deliveryCancelled', const token = ref(null)
const user = ref(null)
const deliveries = ref<Delivery[]>([])
const page = ref(1)
const perPage = ref(50)
const recordsLength = ref(0)
const options = ref({
edgeNavigation: false,
format: false,
template: PaginationComp
})
components: { // Functions
Header, const getPage = (pageVal: any) => {
SideBar, deliveries.value = [];
Footer, get_oil_orders(pageVal)
}, }
data() { const userStatus = () => {
return { let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
token: null, axios({
user: null, method: 'get',
deliveries: [] as any[], url: path,
page: 1, withCredentials: true,
perPage: 50, headers: authHeader(),
recordsLength: 0, })
options: { .then((response: any) => {
edgeNavigation: false,
format: false,
template: PaginationComp
}
}
},
created() {
this.userStatus()
},
mounted() {
this.getPage(this.page)
},
methods: {
getPage: function (page: any) {
// we simulate an api call that fetch the records from a backend
this.deliveries = [];
this.get_oil_orders(page)
},
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;
}
})
.catch(() => {
this.user = null
})
},
get_oil_orders(page: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/issue/' + page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
})
},
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/cancelled/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) { if (response.data.ok) {
notify({ user.value = response.data.user;
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
} }
}) })
}, .catch(() => {
}, user.value = null
})
}
const get_oil_orders = async (pageVal: number) => {
try {
const response = await deliveryService.getIssues(pageVal)
deliveries.value = response.data || []
} catch (error) {
console.error('Error fetching issue deliveries:', error)
deliveries.value = []
}
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/cancelled/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
}
// Lifecycle
onMounted(() => {
userStatus()
getPage(page.value)
}) })
</script> </script>

View File

@@ -121,105 +121,93 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { deliveryService } from '../../../services/deliveryService'
import { Delivery } from '../../../types/models'
import Header from '../../../layouts/headers/headerauth.vue' import Header from '../../../layouts/headers/headerauth.vue'
import PaginationComp from '../../../components/pagination.vue' import PaginationComp from '../../../components/pagination.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({ // Reactive data
name: 'deliveryDelivered', const token = ref(null)
const user = ref(null)
const deliveries = ref<Delivery[]>([])
const page = ref(1)
const perPage = ref(50)
const recordsLength = ref(0)
const options = ref({
edgeNavigation: false,
format: false,
template: PaginationComp
})
components: { // Functions
Header, const getPage = (pageVal: any) => {
SideBar, deliveries.value = [];
Footer, get_oil_orders(pageVal)
}, }
data() { const userStatus = () => {
return { let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
token: null, axios({
user: null, method: 'get',
deliveries: [] as any[], url: path,
page: 1, withCredentials: true,
perPage: 50, headers: authHeader(),
recordsLength: 0, })
options: { .then((response: any) => {
edgeNavigation: false,
format: false,
template: PaginationComp
}
}
},
created() {
this.userStatus()
},
mounted() {
this.getPage(this.page)
},
methods: {
getPage: function (page: any) {
// we simulate an api call that fetch the records from a backend
this.deliveries = [];
this.get_oil_orders(page)
},
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;
}
})
.catch(() => {
this.user = null
})
},
get_oil_orders(page: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delivered/' + page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
})
},
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) { if (response.data.ok) {
notify({ user.value = response.data.user;
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
} }
}) })
}, .catch(() => {
}, user.value = null
})
}
const get_oil_orders = async (pageVal: number) => {
try {
const response = await deliveryService.getDelivered(pageVal)
deliveries.value = response.data || []
} catch (error) {
console.error('Error fetching delivered deliveries:', error)
deliveries.value = []
}
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
}
// Lifecycle
onMounted(() => {
userStatus()
getPage(page.value)
}) })
</script> </script>

View File

@@ -121,109 +121,95 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import { ref, onMounted } 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 { deliveryService } from '../../../services/deliveryService'
import PaginationComp from '../../../components/pagination.vue' import { Delivery } from '../../../types/models'
import SideBar from '../../../layouts/sidebar/sidebar.vue' import Header from '../../../layouts/headers/headerauth.vue'
import Footer from '../../../layouts/footers/footer.vue' import PaginationComp from '../../../components/pagination.vue'
import {notify} from "@kyvg/vue3-notification"; import SideBar from '../../../layouts/sidebar/sidebar.vue'
import Footer from '../../../layouts/footers/footer.vue'
export default defineComponent({ import {notify} from "@kyvg/vue3-notification";
name: 'deliveryFinalized',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
token: null,
user: null,
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,
options: {
edgeNavigation: false,
format: false,
template: PaginationComp
}
}
},
created() {
this.userStatus()
},
mounted() {
this.getPage(this.page)
},
methods: {
getPage: function (page: any) {
// we simulate an api call that fetch the records from a backend
this.deliveries = [];
this.get_oil_orders(page)
},
userStatus() { // Reactive data
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami'; const token = ref(null)
axios({ const user = ref(null)
method: 'get', const deliveries = ref<Delivery[]>([])
url: path, const page = ref(1)
withCredentials: true, const perPage = ref(50)
headers: authHeader(), const recordsLength = ref(0)
}) const options = ref({
.then((response: any) => { edgeNavigation: false,
if (response.data.ok) { format: false,
this.user = response.data.user; template: PaginationComp
} })
})
.catch(() => {
this.user = null
})
},
get_oil_orders(page: any) { // Functions
let path = import.meta.env.VITE_BASE_URL + '/delivery/finalized/' + page; const getPage = (pageVal: any) => {
axios({ deliveries.value = [];
method: 'get', get_oil_orders(pageVal)
url: path, }
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
})
},
deleteCall(delivery_id: any) { const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id; let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({ axios({
method: 'delete', method: 'get',
url: path, url: path,
headers: authHeader(), withCredentials: true,
}).then((response: any) => { headers: authHeader(),
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
},
},
}) })
</script> .then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const get_oil_orders = async (pageVal: number) => {
try {
const response = await deliveryService.getFinalized(pageVal)
deliveries.value = response.data || []
} catch (error) {
console.error('Error fetching finalized deliveries:', error)
deliveries.value = []
}
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
}
// Lifecycle
onMounted(() => {
userStatus()
getPage(page.value)
})
</script>
<style scoped> <style scoped>

View File

@@ -122,104 +122,93 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { Delivery } from '../../../types/models'
import Header from '../../../layouts/headers/headerauth.vue' import Header from '../../../layouts/headers/headerauth.vue'
import PaginationComp from '../../../components/pagination.vue' import PaginationComp from '../../../components/pagination.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({ // Reactive data
name: 'deliveryIssue', const token = ref(null)
const user = ref(null)
const deliveries = ref<Delivery[]>([])
const page = ref(1)
const perPage = ref(50)
const recordsLength = ref(0)
const options = ref({
edgeNavigation: false,
format: false,
template: PaginationComp
})
components: { // Functions
Header, const getPage = (pageVal: any) => {
SideBar, deliveries.value = [];
Footer, get_oil_orders(pageVal)
}, }
data() { const userStatus = () => {
return { let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
token: null, axios({
user: null, method: 'get',
deliveries: [] as any[], url: path,
page: 1, withCredentials: true,
perPage: 50, headers: authHeader(),
recordsLength: 0, })
options: { .then((response: any) => {
edgeNavigation: false,
format: false,
template: PaginationComp
}
}
},
created() {
this.userStatus()
},
mounted() {
this.getPage(this.page)
},
methods: {
getPage: function (page: any) {
// we simulate an api call that fetch the records from a backend
this.deliveries = [];
this.get_oil_orders(page)
},
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;
}
})
.catch(() => {
this.user = null
})
},
get_oil_orders(page: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/issue/' + page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
})
},
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) { if (response.data.ok) {
notify({ user.value = response.data.user;
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
} }
}) })
}, .catch(() => {
}, user.value = null
})
}
const get_oil_orders = (pageVal: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/issue/' + pageVal;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
deliveries.value = response.data
})
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
}
// Lifecycle
onMounted(() => {
userStatus()
getPage(page.value)
}) })
</script> </script>

View File

@@ -157,108 +157,94 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } 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 { deliveryService } from '../../../services/deliveryService'
import PaginationComp from '../../../components/pagination.vue' import { Delivery } from '../../../types/models'
import SideBar from '../../../layouts/sidebar/sidebar.vue' import Header from '../../../layouts/headers/headerauth.vue'
import Footer from '../../../layouts/footers/footer.vue' import PaginationComp from '../../../components/pagination.vue'
import { notify } from "@kyvg/vue3-notification"; import SideBar from '../../../layouts/sidebar/sidebar.vue'
import Footer from '../../../layouts/footers/footer.vue'
export default defineComponent({ import { notify } from "@kyvg/vue3-notification";
name: 'deliveryPending',
components: {
Header,
SideBar,
Footer,
},
data() {
return {
token: null,
user: null,
deliveries: [] as any[],
page: 1,
perPage: 50,
recordsLength: 0,
options: {
edgeNavigation: false,
format: false,
template: PaginationComp
}
}
},
created() {
this.userStatus()
},
mounted() {
this.getPage(this.page)
},
methods: {
getPage: function (page: any) {
this.deliveries = [];
this.get_oil_orders(page)
},
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;
}
})
.catch(() => {
this.user = null
})
},
// Reactive data
get_oil_orders(page: any) { const token = ref(null)
let path = import.meta.env.VITE_BASE_URL + '/delivery/pending/' + page; const user = ref(null)
axios({ const deliveries = ref<Delivery[]>([])
method: 'get', const page = ref(1)
url: path, const perPage = ref(50)
headers: authHeader(), const recordsLength = ref(0)
}).then((response: any) => { const options = ref({
this.deliveries = response.data edgeNavigation: false,
}) format: false,
}, template: PaginationComp
})
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
},
}, // Functions
const getPage = (pageVal: any) => {
deliveries.value = [];
get_oil_orders(pageVal)
}
const 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) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const get_oil_orders = async (pageVal: number) => {
try {
const response = await deliveryService.getPending(pageVal)
deliveries.value = response.data || []
} catch (error) {
console.error('Error fetching pending deliveries:', error)
deliveries.value = []
}
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
}
// Lifecycle
onMounted(() => {
userStatus()
getPage(page.value)
})
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -164,174 +164,116 @@
</div> </div>
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { deliveryService } from '../../../services/deliveryService'
import { Delivery } from '../../../types/models'
import Header from '../../../layouts/headers/headerauth.vue' import Header from '../../../layouts/headers/headerauth.vue'
import PaginationComp from '../../../components/pagination.vue' import PaginationComp from '../../../components/pagination.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({ // Reactive data
name: 'deliveryOutForDelivery', const token = ref(null)
const user = ref(null)
const deliveries = ref<Delivery[]>([])
const totals = ref<{ town: string; gallons: number }[]>([])
const grand_total = ref(0)
const page = ref(1)
const perPage = ref(50)
const recordsLength = ref(0)
const options = ref({
edgeNavigation: false,
format: false,
template: PaginationComp
})
components: { // Functions
Header, const getPage = (pageVal: any) => {
SideBar, deliveries.value = [];
Footer, get_oil_orders(pageVal)
}, }
data() { const userStatus = () => {
return { let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
token: null, axios({
user: null, method: 'get',
deliveries: [] as any[], url: path,
totals: [] as any[], withCredentials: true,
grand_total: 0, headers: authHeader(),
page: 1, })
perPage: 50, .then((response: any) => {
recordsLength: 0, if (response.data.ok) {
options: { user.value = response.data.user;
edgeNavigation: false,
format: false,
template: PaginationComp
} }
})
.catch(() => {
user.value = null
})
}
const mod = (date: any) => new Date(date).getTime()
const get_oil_orders = async (pageVal: number) => {
try {
const response = await deliveryService.getOutForDelivery(pageVal)
deliveries.value = response.data || []
// Sort deliveries by Delivery # (id) in descending order
deliveries.value.sort((a, b) => b.id - a.id);
} catch (error) {
console.error('Error fetching out for delivery:', error)
deliveries.value = []
}
}
const get_totals = () => {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/today-totals';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
totals.value = response.data.totals || []
grand_total.value = response.data.grand_total || 0
}).catch((error: any) => {
console.error('Error fetching totals:', error);
totals.value = []
grand_total.value = 0
})
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
} }
}, })
created() { }
this.userStatus()
},
mounted() {
this.getPage(this.page)
this.get_totals()
},
methods: {
getPage: function (page: any) {
this.deliveries = [];
this.get_oil_orders(page)
},
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;
}
})
.catch(() => {
this.user = null
})
},
// Lifecycle
mod: (date: any) => new Date (date).getTime(), onMounted(() => {
get_oil_orders(page: any) { userStatus()
let path = import.meta.env.VITE_BASE_URL + '/delivery/outfordelivery/' + page; getPage(page.value)
axios({ get_totals()
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
// Sort deliveries by Delivery # (id) in descending order
this.deliveries.sort((a, b) => b.id - a.id);
})
},
get_totals() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/today-totals';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.totals = response.data.totals || []
this.grand_total = response.data.grand_total || 0
}).catch((error: any) => {
console.error('Error fetching totals:', error);
this.totals = []
this.grand_total = 0
})
},
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
},
// printtTicketAll() {
// let path = import.meta.env.VITE_PRINT_URL + '/command/printticket/print_today';
// axios({
// method: 'get',
// url: path,
// headers: authHeader(),
// }).then((response: any) => {
// if (response.data.ok) {
// notify({
// title: "Success",
// text: "Sent to Printer",
// type: "success",
// });
// this.getPage(this.page)
// } else {
// notify({
// title: "Failure",
// text: "error printing",
// type: "success",
// });
// }
// })
// },
// printTicket(delivery_id: number) {
// let path = import.meta.env.VITE_PRINT_URL + '/command/printticket/' + delivery_id;
// axios({
// method: 'options',
// url: path,
// headers: authHeader(),
// }).then((response: any) => {
// if (response.data.ok) {
// notify({
// title: "Success",
// text: "Sent to Printer",
// type: "success",
// });
// this.getPage(this.page)
// } else {
// notify({
// title: "Failure",
// text: "error printing",
// type: "success",
// });
// }
// })
// },
},
}) })
</script> </script>

View File

@@ -157,173 +157,164 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { Delivery } from '../../../types/models'
import Header from '../../../layouts/headers/headerauth.vue' import Header from '../../../layouts/headers/headerauth.vue'
import PaginationComp from '../../../components/pagination.vue' import PaginationComp from '../../../components/pagination.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({ interface TownTotal {
name: 'deliveryOutForDelivery', town: string;
gallons: number;
}
components: { // Reactive data
Header, const token = ref(null)
SideBar, const user = ref(null)
Footer, const deliveries = ref<Delivery[]>([])
}, const totals = ref<TownTotal[]>([])
const grand_total = ref(0)
const page = ref(1)
const perPage = ref(50)
const recordsLength = ref(0)
const options = ref({
edgeNavigation: false,
format: false,
template: PaginationComp
})
data() { // Functions
return { const getPage = (pageVal: any) => {
token: null, deliveries.value = [];
user: null, get_oil_orders(pageVal)
deliveries: [] as any[], }
totals: [] as any[],
grand_total: 0, const userStatus = () => {
page: 1, let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
perPage: 50, axios({
recordsLength: 0, method: 'get',
options: { url: path,
edgeNavigation: false, withCredentials: true,
format: false, headers: authHeader(),
template: PaginationComp })
.then((response: any) => {
if (response.data.ok) {
user.value = response.data.user;
} }
})
.catch(() => {
user.value = null
})
}
const get_oil_orders = (pageVal: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/tommorrow/' + pageVal;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
deliveries.value = response.data
})
}
const get_totals = () => {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/tomorrow-totals';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
totals.value = response.data.totals || []
grand_total.value = response.data.grand_total || 0
}).catch((error: any) => {
console.error('Error fetching totals:', error);
totals.value = []
grand_total.value = 0
})
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
} }
}, })
created() { }
this.userStatus()
},
mounted() {
this.getPage(this.page)
this.get_totals()
},
methods: {
getPage: function (page: any) {
this.deliveries = [];
this.get_oil_orders(page)
},
userStatus() { const printtTicketAll = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami'; let path = import.meta.env.VITE_PRINT_URL + '/command/printticket/print_tommorrow';
axios({ axios({
method: 'get', method: 'delete',
url: path, url: path,
withCredentials: true, headers: authHeader(),
headers: authHeader(), }).then((response: any) => {
}) if (response.data.ok) {
.then((response: any) => { notify({
if (response.data.ok) { title: "Success",
this.user = response.data.user; text: "Sent to Printer",
} type: "success",
}) });
.catch(() => { getPage(page.value)
this.user = null } else {
}) notify({
}, title: "Failure",
get_oil_orders(page: any) { text: "error printing",
let path = import.meta.env.VITE_BASE_URL + '/delivery/tommorrow/' + page; type: "success",
axios({ });
method: 'get', }
url: path, })
headers: authHeader(), }
}).then((response: any) => {
this.deliveries = response.data
})
},
get_totals() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/tomorrow-totals';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.totals = response.data.totals || []
this.grand_total = response.data.grand_total || 0
}).catch((error: any) => {
console.error('Error fetching totals:', error);
this.totals = []
this.grand_total = 0
})
},
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
},
printtTicketAll() {
let path = import.meta.env.VITE_PRINT_URL + '/command/printticket/print_tommorrow';
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "Sent to Printer",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error printing",
type: "success",
});
}
})
},
printTicket(delivery_id: number) {
let path = import.meta.env.VITE_PRINT_URL + '/command/printticket/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "Sent to Printer",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error printing",
type: "success",
});
}
})
},
},
const printTicket = (delivery_id: number) => {
let path = import.meta.env.VITE_PRINT_URL + '/command/printticket/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "Sent to Printer",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error printing",
type: "success",
});
}
})
}
// Lifecycle
onMounted(() => {
userStatus()
getPage(page.value)
get_totals()
}) })
</script> </script>

View File

@@ -137,127 +137,112 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { deliveryService } from '../../../services/deliveryService'
import { Delivery } from '../../../types/models'
import Header from '../../../layouts/headers/headerauth.vue' import Header from '../../../layouts/headers/headerauth.vue'
import PaginationComp from '../../../components/pagination.vue' import PaginationComp from '../../../components/pagination.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({ // Reactive data
name: 'deliveryWaiting', const token = ref(null)
const user = ref(null)
const deliveries = ref<Delivery[]>([])
const totals = ref<{ town: string; gallons: number }[]>([])
const grand_total = ref(0)
const page = ref(1)
const perPage = ref(50)
const recordsLength = ref(0)
const options = ref({
edgeNavigation: false,
format: false,
template: PaginationComp
})
components: { // Functions
Header, const getPage = (pageVal: any) => {
SideBar, deliveries.value = [];
Footer, get_oil_orders(pageVal)
}, }
data() { const userStatus = () => {
return { let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
token: null, axios({
user: null, method: 'get',
deliveries: [] as any[], url: path,
totals: [] as any[], withCredentials: true,
grand_total: 0, headers: authHeader(),
page: 1, })
perPage: 50, .then((response: any) => {
recordsLength: 0, if (response.data.ok) {
options: { user.value = response.data.user;
edgeNavigation: false,
format: false,
template: PaginationComp
} }
})
.catch(() => {
user.value = null
})
}
const get_oil_orders = async (pageVal: number) => {
try {
const response = await deliveryService.getWaiting(pageVal)
deliveries.value = response.data || []
} catch (error) {
console.error('Error fetching waiting deliveries:', error)
deliveries.value = []
}
}
const get_totals = () => {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/waiting-totals';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
totals.value = response.data.totals || []
grand_total.value = response.data.grand_total || 0
}).catch((error: any) => {
console.error('Error fetching totals:', error);
totals.value = []
grand_total.value = 0
})
}
const deleteCall = (delivery_id: any) => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
getPage(page.value)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
} }
}, })
}
created() { // Lifecycle
this.userStatus() onMounted(() => {
}, userStatus()
mounted() { getPage(page.value)
this.getPage(this.page) get_totals()
this.get_totals()
},
methods: {
getPage: function (page: any) {
// we simulate an api call that fetch the records from a backend
this.deliveries = [];
this.get_oil_orders(page)
},
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;
}
})
.catch(() => {
this.user = null
})
},
get_oil_orders(page: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/waiting/' + page;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.deliveries = response.data
})
},
get_totals() {
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/waiting-totals';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: any) => {
this.totals = response.data.totals || []
this.grand_total = response.data.grand_total || 0
}).catch((error: any) => {
console.error('Error fetching totals:', error);
this.totals = []
this.grand_total = 0
})
},
deleteCall(delivery_id: any) {
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
axios({
method: 'delete',
url: path,
headers: authHeader(),
}).then((response: any) => {
if (response.data.ok) {
notify({
title: "Success",
text: "deleted delivery",
type: "success",
});
this.getPage(this.page)
} else {
notify({
title: "Failure",
text: "error deleting delivery",
type: "success",
});
}
})
},
},
}) })
</script> </script>

View File

@@ -62,6 +62,7 @@ import useValidate from "@vuelidate/core";
import { required, minLength, helpers } from "@vuelidate/validators"; import { required, minLength, helpers } from "@vuelidate/validators";
import Header from "../../layouts/headers/headerauth.vue"; import Header from "../../layouts/headers/headerauth.vue";
import authHeader from "../../services/auth.header"; import authHeader from "../../services/auth.header";
import {Employee} from '../../types/models';
export default defineComponent({ export default defineComponent({
name: "EmployeeChangePassword", name: "EmployeeChangePassword",
@@ -74,7 +75,7 @@ export default defineComponent({
user: null, user: null,
user_admin: 0, user_admin: 0,
loaded: false, loaded: false,
employee: null as any, employee: {} as Employee,
ChangePasswordForm: { ChangePasswordForm: {
new_password: "", new_password: "",
password_confirm: "", password_confirm: "",

View File

@@ -45,7 +45,8 @@
<td>{{ person.employee_phone_number }}</td> <td>{{ person.employee_phone_number }}</td>
<td class="text-right"> <td class="text-right">
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
<router-link :to="{ name: 'employeeEdit', params: { id: person.user_id } }" class="btn btn-sm btn-secondary">Edit</router-link> <router-link :to="{ name: 'employeeEdit', params: { id: person.user_id || 0 } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'employeeEdit', params: { id: person.user_id || 0 } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'employeeProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">View</router-link> <router-link :to="{ name: 'employeeProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link v-if="user && user.user_admin === 0" :to="{ name: 'employeeChangePassword', params: { id: person.id } }" class="btn btn-sm btn-warning">Change Password</router-link> <router-link v-if="user && user.user_admin === 0" :to="{ name: 'employeeChangePassword', params: { id: person.id } }" class="btn btn-sm btn-warning">Change Password</router-link>
</div> </div>
@@ -73,7 +74,7 @@
<p>{{ person.employee_phone_number }}</p> <p>{{ person.employee_phone_number }}</p>
</div> </div>
<div class="card-actions justify-end flex-wrap gap-2 mt-2"> <div class="card-actions justify-end flex-wrap gap-2 mt-2">
<router-link :to="{ name: 'employeeEdit', params: { id: person.user_id } }" class="btn btn-sm btn-secondary">Edit</router-link> <router-link :to="{ name: 'employeeEdit', params: { id: person.user_id || 0 } }" class="btn btn-sm btn-secondary">Edit</router-link>
<router-link :to="{ name: 'employeeProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">View</router-link> <router-link :to="{ name: 'employeeProfile', params: { id: person.id } }" class="btn btn-sm btn-ghost">View</router-link>
<router-link v-if="user && user.user_admin === 0" :to="{ name: 'employeeChangePassword', params: { id: person.id } }" class="btn btn-sm btn-warning">Change Password</router-link> <router-link v-if="user && user.user_admin === 0" :to="{ name: 'employeeChangePassword', params: { id: person.id } }" class="btn btn-sm btn-warning">Change Password</router-link>
</div> </div>
@@ -97,6 +98,7 @@ import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import PaginationComp from '../../components/pagination.vue' import PaginationComp from '../../components/pagination.vue'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../layouts/footers/footer.vue'
import {Employee, User} from '../../types/models'
export default defineComponent({ export default defineComponent({
name: 'EmployeeHome', name: 'EmployeeHome',
@@ -105,8 +107,8 @@ export default defineComponent({
}, },
data() { data() {
return { return {
user: {} as any, user: {} as User,
employees: [] as any[], employees: [] as Employee[],
page: 1, page: 1,
perPage: 50, perPage: 50,
recordsLength: 0, recordsLength: 0,
@@ -136,7 +138,7 @@ export default defineComponent({
} }
}) })
.catch(() => { .catch(() => {
this.user = null; this.user = {} as User;
}); });
}, },
// --- METHOD CORRECTED TO MATCH YOUR SIMPLE ARRAY API RESPONSE --- // --- METHOD CORRECTED TO MATCH YOUR SIMPLE ARRAY API RESPONSE ---

View File

@@ -1,10 +1,10 @@
import EmployeeHome from '../employee/home.vue'; const EmployeeHome = () => import('../employee/home.vue');
import EmployeeCreate from "../employee/create.vue"; const EmployeeCreate = () => import("../employee/create.vue");
import EmployeeEdit from "../employee/edit.vue"; const EmployeeEdit = () => import("../employee/edit.vue");
import EmployeeProfile from "../employee/profile/home.vue"; const EmployeeProfile = () => import("../employee/profile/home.vue");
import EmployeeChangePassword from "../employee/changepassword.vue"; const EmployeeChangePassword = () => import("../employee/changepassword.vue");
const employeeRoutes = [ const employeeRoutes = [
{ {

View File

@@ -1,6 +1,6 @@
import MoneyYear from '../money/profit_year.vue'; const MoneyYear = () => import('../money/profit_year.vue');

View File

@@ -202,463 +202,468 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import axios from 'axios' import { useRoute, useRouter } from 'vue-router'
import axios, { AxiosResponse, AxiosError } from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import type {
CustomerFormData,
CreditCardFormData,
PricingData,
OilPricingResponse,
WhoAmIResponse,
AutoDeliveryData
} from '../../../types/models'
export default defineComponent({ const route = useRoute()
name: 'AuthorizePrechargeAutho', const router = useRouter()
data() { // Reactive data
return { const deliveryId = ref(route.params.id as string)
deliveryId: this.$route.params.id as string, const loaded = ref(false)
loaded: false, const chargeAmount = ref(0)
chargeAmount: 0, const quickGallonAmounts = ref([100, 125, 150, 175, 200, 220])
quickGallonAmounts: [100, 125, 150, 175, 200, 220], const loading = ref(false)
loading: false, const action = ref('') // 'preauthorize' or 'charge'
action: '', // 'preauthorize' or 'charge' const error = ref('')
error: '', const success = ref('')
success: '', const isChargeConfirmationModalVisible = ref(false)
isChargeConfirmationModalVisible: false, const transactionId = ref(0)
transactionId: 0, const user = ref({
user: { user_id: 0,
user_id: 0,
},
autoDelivery: {
id: 0,
customer_id: 0,
customer_full_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
tank_size: 0,
estimated_gallons_left: 0,
house_factor: 0,
auto_status: 0,
},
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: "",
},
currentOilPrice: 0,
pricingTiers: [] as { gallons: number; price: number }[],
}
},
computed: {
selectedCard(): any {
return this.credit_cards.find((card: any) => card.main_card) || this.credit_cards[0]
},
customerStateName(): string {
const states: Record<number, string> = { 0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York' };
return states[this.customer.customer_state] || 'Unknown state';
},
selectedGallonAmount(): number | null {
return this.quickGallonAmounts.find(gal => this.calculatePriceForGallons(gal).toFixed(2) === this.chargeAmount.toFixed(2)) || null;
}
},
mounted() {
this.loadData(this.deliveryId)
this.getPricingTiers()
},
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
},
loadData(deliveryId: string) {
this.userStatus()
this.getAutoDelivery(deliveryId)
this.getOilPricing()
this.getCurrentOilPrice()
},
getCurrentOilPrice() {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
this.currentOilPrice = response.data.price_for_customer;
this.calculateDefaultChargeAmount()
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
},
calculateDefaultChargeAmount() {
this.chargeAmount = this.calculateTotalAsNumber()
},
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;
this.calculateDefaultChargeAmount()
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
},
getAutoDelivery(delivery_id: any) {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/delivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data && response.data.customer_id) {
this.autoDelivery = response.data;
this.getCustomer(this.autoDelivery.customer_id)
this.getCreditCards(this.autoDelivery.customer_id)
} else {
console.error("API Error:", response.data.error || "Failed to fetch auto delivery data.");
}
})
.catch((error: any) => {
console.error("API Error in getAutoDelivery:", error);
notify({
title: "Error",
text: "Could not get automatic 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
})
},
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) => {
this.pricingTiers = Object.entries(response.data).map(([gallons, price]) => ({ gallons: parseInt(gallons, 10), price: Number(price) }));
})
.catch(() => {
notify({
title: "Pricing Error",
text: "Could not retrieve today's pricing.",
type: "error"
});
});
},
isPricingTierSelected(tierGallons: number | string): boolean {
const calculated = this.calculateGallonsToFill()
return calculated == Number(tierGallons)
},
calculatePriceForGallons(gallons: number): number {
let priceForGallons = 0;
const sortedTiers = [...this.pricingTiers].sort((a, b) => Number(a.gallons) - Number(b.gallons));
// Find the highest tier that is less than or equal to the gallons ordered
let applicableTier = sortedTiers.filter(t => gallons >= Number(t.gallons)).pop();
if (applicableTier) {
const pricePerGallon = Number(applicableTier.price) / Number(applicableTier.gallons);
priceForGallons = gallons * pricePerGallon;
} else if (sortedTiers.length > 0) {
// Fallback to the lowest tier's price/gallon if no tier is met (e.g., ordering 50 gallons when lowest tier is 100)
const lowestTier = sortedTiers[0];
const pricePerGallon = Number(lowestTier.price) / Number(lowestTier.gallons);
priceForGallons = gallons * pricePerGallon;
}
return priceForGallons;
},
setGallons(gallons: number) {
this.chargeAmount = this.calculatePriceForGallons(gallons);
},
calculateGallonsToFill() {
return this.autoDelivery.tank_size - this.autoDelivery.estimated_gallons_left
},
calculateSubtotal() {
const gallons = this.calculateGallonsToFill()
const pricePerGallon = this.pricing.price_for_customer || this.currentOilPrice
return (gallons * pricePerGallon).toFixed(2)
},
calculateTotalAmount() {
const subtotal = parseFloat(this.calculateSubtotal())
let total = subtotal
// No additional fees for auto preauthorization
return total.toFixed(2)
},
calculateTotalAsNumber() {
return parseFloat(this.calculateTotalAmount())
},
async handlePreauthorize() {
await this.processPayment('preauthorize')
},
async handleChargeNow() {
this.loading = true
this.isChargeConfirmationModalVisible = true
},
async proceedWithCharge() {
this.isChargeConfirmationModalVisible = false
await this.processPayment('charge')
},
cancelCharge() {
this.isChargeConfirmationModalVisible = false
},
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 {
// Step 2: If payment method is credit, perform the pre-authorization
if (actionType === 'preauthorize') {
if (!this.chargeAmount || this.chargeAmount <= 0) {
throw new Error("Pre-authorization amount must be greater than zero.");
}
const authPayload = {
card_id: (this.selectedCard as any).id,
preauthorize_amount: this.chargeAmount.toFixed(2),
delivery_id: null,
auto_id: this.deliveryId,
};
const authPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/authorize/saved-card/${this.customer.id}`;
const response = await axios.post(authPath, authPayload, { withCredentials: true, headers: authHeader() });
if (!response.data.auth_net_transaction_id || response.data.auth_net_transaction_id.trim() === '') {
throw new Error("Failed transaction: No Authorize.net transaction ID received");
}
this.transactionId = response.data.id;
// Update auto_delivery status to 3
await axios.put(`${import.meta.env.VITE_AUTO_URL}/delivery/update_status/${this.deliveryId}`, {}, { withCredentials: true, headers: authHeader() });
// Create Tickets_Auto_Delivery after successful preauthorize
const ticketPayload = {
gallons_delivered: 0,
payment_type: 11, // 11 for preauthorize, 1 for charge
payment_card_id: this.selectedCard.id,
payment_status: 1 // Pre-authorized status (ready for capture)
};
const ticketUrl = `${import.meta.env.VITE_AUTO_URL}/confirm/auto/create/${this.deliveryId}`;
const ticketResponse = await axios.post(ticketUrl, ticketPayload, { withCredentials: true, headers: authHeader() });
console.log('Ticket response data:', ticketResponse.data, 'type:', typeof ticketResponse.data, 'keys:', ticketResponse.data ? Object.keys(ticketResponse.data) : 'no data');
// Update transaction auto_id to ticket ID
if (this.transactionId && ticketResponse.data) {
const data = Array.isArray(ticketResponse.data) ? ticketResponse.data[0] : ticketResponse.data;
if (data && data.auto_ticket_id !== undefined) {
await axios.put(`${import.meta.env.VITE_AUTHORIZE_URL}/api/transaction/${this.transactionId}/update_auto_id/${data.auto_ticket_id}`, {}, { withCredentials: true, headers: authHeader() });
} else {
console.error('auto_ticket_id is undefined in ticket response');
}
}
// On successful authorization, show success and redirect
this.success = `Preauthorization successful! Transaction ID: ${response.data.auth_net_transaction_id}`;
setTimeout(() => {
this.$router.push({ name: "auto" });
}, 2000);
}
else { // Handle 'charge' action
if (!this.chargeAmount || this.chargeAmount <= 0) {
throw new Error("Charge amount must be greater than zero.");
}
const chargePayload = {
card_id: (this.selectedCard as any).id,
charge_amount: this.chargeAmount.toFixed(2),
delivery_id: this.deliveryId,
};
const chargePath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/charge/saved-card/${this.customer.id}`;
console.log('=== DEBUG: Charge payload ===');
console.log('Calling endpoint:', chargePath);
console.log('Final payload being sent:', chargePayload);
const response = await axios.post(chargePath, chargePayload, { withCredentials: true, headers: authHeader() });
// Update auto_delivery status to 3
await axios.put(`${import.meta.env.VITE_AUTO_URL}/delivery/update_status/${this.deliveryId}`, {}, { withCredentials: true, headers: authHeader() });
// Status codes: 0 = APPROVED, 1 = DECLINED (based on backend TransactionStatus enum)
if (response.data && response.data.status === 0) { // 0 = APPROVED
if (!response.data.auth_net_transaction_id || response.data.auth_net_transaction_id.trim() === '') {
throw new Error("Failed transaction: No Authorize.net transaction ID received");
}
this.transactionId = response.data.id;
this.success = `Charge successful! Transaction ID: ${response.data.auth_net_transaction_id}`;
// Create Tickets_Auto_Delivery after successful charge
const ticketPayload = {
gallons_delivered: 0,
payment_type: 11, // 11 for Authorize charge
payment_card_id: this.selectedCard.id,
payment_status: response.data.status // 0 = APPROVED
};
const ticketUrl = `${import.meta.env.VITE_AUTO_URL}/confirm/auto/create/${this.deliveryId}`;
console.log("POOOPP!")
await axios.post(ticketUrl, ticketPayload, { withCredentials: true, headers: authHeader() });
setTimeout(() => {
this.$router.push({ name: "auto" });
}, 2000);
} else {
// The error message from your backend will be more specific now
throw new Error(`Payment charge failed: ${response.data?.rejection_reason || 'Unknown error'}`);
}
}
} catch (error: any) {
console.log(error)
this.error = error.response?.data?.detail || `Failed to ${actionType} payment`
notify({
title: "Error",
text: this.error,
type: "error",
})
} finally {
this.loading = false
this.action = ''
}
}
},
}) })
const autoDelivery = ref<AutoDeliveryData>({
id: 0,
customer_id: 0,
customer_full_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
tank_size: 0,
estimated_gallons_left: 0,
estimated_gallons_left_prev_day: 0,
house_factor: 0,
auto_status: 0,
account_number: '',
last_fill: '',
days_since_last_fill: 0,
last_updated: '',
tank_height: '',
open_ticket_id: null,
})
const credit_cards = ref<CreditCardFormData[]>([
{
id: 0,
name_on_card: '',
main_card: false,
card_number: '',
expiration_month: '',
type_of_card: '',
last_four_digits: '',
expiration_year: '',
security_number: '',
}
])
const customer = ref<CustomerFormData>({
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: '',
})
const pricing = ref<PricingData>({
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
date: "",
})
const currentOilPrice = ref(0)
const pricingTiers = ref([] as { gallons: number; price: number }[])
// Computed
const selectedCard = computed(() => {
return credit_cards.value.find((card) => card.main_card) || credit_cards.value[0]
})
const customerStateName = computed(() => {
const states: Record<number, string> = { 0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire', 3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York' };
return states[customer.value.customer_state] || 'Unknown state';
})
const selectedGallonAmount = computed(() => {
return quickGallonAmounts.value.find(gal => calculatePriceForGallons(gal).toFixed(2) === chargeAmount.value.toFixed(2)) || null;
})
// Watchers
watch(() => route.params.id, (newId) => {
if (newId !== deliveryId.value) {
resetState()
deliveryId.value = newId as string
loadData(newId as string)
}
})
// Lifecycle
onMounted(() => {
loadData(deliveryId.value)
getPricingTiers()
})
// Functions
const resetState = () => {
loading.value = false
action.value = ''
error.value = ''
success.value = ''
chargeAmount.value = 0
}
const loadData = (deliveryId: string) => {
userStatus()
getAutoDelivery(deliveryId)
getOilPricing()
getCurrentOilPrice()
}
const getCurrentOilPrice = () => {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: AxiosResponse<OilPricingResponse>) => {
currentOilPrice.value = response.data.price_for_customer;
calculateDefaultChargeAmount()
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
}
const calculateDefaultChargeAmount = () => {
chargeAmount.value = calculateTotalAsNumber()
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: AxiosResponse<WhoAmIResponse>) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
}
const getOilPricing = () => {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/table";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<OilPricingResponse>) => {
pricing.value = response.data;
calculateDefaultChargeAmount()
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
}
const getAutoDelivery = (delivery_id: number | string) => {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/delivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<AutoDeliveryData>) => {
if (response.data && response.data.customer_id) {
autoDelivery.value = response.data;
getCustomer(autoDelivery.value.customer_id)
getCreditCards(autoDelivery.value.customer_id)
} else {
console.error("API Error: Failed to fetch auto delivery data.");
}
})
.catch((err: Error) => {
console.error("API Error in getAutoDelivery:", err);
notify({
title: "Error",
text: "Could not get automatic delivery",
type: "error",
});
});
}
const getCreditCards = (user_id: number) => {
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + user_id;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<CreditCardFormData[]>) => {
credit_cards.value = response.data
})
}
const getCustomer = (userid: number) => {
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<CustomerFormData>) => {
customer.value = response.data
})
}
const getPricingTiers = () => {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader()
})
.then((response: AxiosResponse<Record<string, number>>) => {
pricingTiers.value = Object.entries(response.data).map(([gallons, price]) => ({ gallons: parseInt(gallons, 10), price: Number(price) }));
})
.catch(() => {
notify({
title: "Pricing Error",
text: "Could not retrieve today's pricing.",
type: "error"
});
});
}
const isPricingTierSelected = (tierGallons: number | string): boolean => {
const calculated = calculateGallonsToFill()
return calculated == Number(tierGallons)
}
const calculatePriceForGallons = (gallons: number): number => {
let priceForGallons = 0;
const sortedTiers = [...pricingTiers.value].sort((a, b) => Number(a.gallons) - Number(b.gallons));
// Find the highest tier that is less than or equal to the gallons ordered
let applicableTier = sortedTiers.filter(t => gallons >= Number(t.gallons)).pop();
if (applicableTier) {
const pricePerGallon = Number(applicableTier.price) / Number(applicableTier.gallons);
priceForGallons = gallons * pricePerGallon;
} else if (sortedTiers.length > 0) {
// Fallback to the lowest tier's price/gallon if no tier is met (e.g., ordering 50 gallons when lowest tier is 100)
const lowestTier = sortedTiers[0];
const pricePerGallon = Number(lowestTier.price) / Number(lowestTier.gallons);
priceForGallons = gallons * pricePerGallon;
}
return priceForGallons;
}
const setGallons = (gallons: number) => {
chargeAmount.value = calculatePriceForGallons(gallons);
}
const calculateGallonsToFill = () => {
return autoDelivery.value.tank_size - autoDelivery.value.estimated_gallons_left
}
const calculateSubtotal = () => {
const gallons = calculateGallonsToFill()
const pricePerGallon = pricing.value.price_for_customer || currentOilPrice.value
return (gallons * pricePerGallon).toFixed(2)
}
const calculateTotalAmount = () => {
const subtotal = parseFloat(calculateSubtotal())
let total = subtotal
// No additional fees for auto preauthorization
return total.toFixed(2)
}
const calculateTotalAsNumber = () => {
return parseFloat(calculateTotalAmount())
}
const handlePreauthorize = async () => {
await processPayment('preauthorize')
}
const handleChargeNow = async () => {
loading.value = true
isChargeConfirmationModalVisible.value = true
}
const proceedWithCharge = async () => {
isChargeConfirmationModalVisible.value = false
await processPayment('charge')
}
const cancelCharge = () => {
isChargeConfirmationModalVisible.value = false
}
const processPayment = async (actionType: string) => {
if (!selectedCard.value) {
error.value = 'No credit card found for this customer'
return
}
loading.value = true
action.value = actionType
error.value = ''
success.value = ''
try {
// Step 2: If payment method is credit, perform the pre-authorization
if (actionType === 'preauthorize') {
if (!chargeAmount.value || chargeAmount.value <= 0) {
throw new Error("Pre-authorization amount must be greater than zero.");
}
const authPayload = {
card_id: selectedCard.value!.id,
preauthorize_amount: chargeAmount.value.toFixed(2),
delivery_id: null,
auto_id: deliveryId.value,
};
const authPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/authorize/saved-card/${customer.value.id}`;
const response = await axios.post(authPath, authPayload, { withCredentials: true, headers: authHeader() });
if (!response.data.auth_net_transaction_id || response.data.auth_net_transaction_id.trim() === '') {
throw new Error("Failed transaction: No Authorize.net transaction ID received");
}
transactionId.value = response.data.id;
// Update auto_delivery status to 3
await axios.put(`${import.meta.env.VITE_AUTO_URL}/delivery/update_status/${deliveryId.value}`, {}, { withCredentials: true, headers: authHeader() });
// Create Tickets_Auto_Delivery after successful preauthorize
const ticketPayload = {
gallons_delivered: 0,
payment_type: 11, // 11 for preauthorize, 1 for charge
payment_card_id: selectedCard.value!.id,
payment_status: 1 // Pre-authorized status (ready for capture)
};
const ticketUrl = `${import.meta.env.VITE_AUTO_URL}/confirm/auto/create/${deliveryId.value}`;
const ticketResponse = await axios.post(ticketUrl, ticketPayload, { withCredentials: true, headers: authHeader() });
console.log('Ticket response data:', ticketResponse.data, 'type:', typeof ticketResponse.data, 'keys:', ticketResponse.data ? Object.keys(ticketResponse.data) : 'no data');
// Update transaction auto_id to ticket ID
if (transactionId.value && ticketResponse.data) {
const data = Array.isArray(ticketResponse.data) ? ticketResponse.data[0] : ticketResponse.data;
if (data && data.auto_ticket_id !== undefined) {
await axios.put(`${import.meta.env.VITE_AUTHORIZE_URL}/api/transaction/${transactionId.value}/update_auto_id/${data.auto_ticket_id}`, {}, { withCredentials: true, headers: authHeader() });
} else {
console.error('auto_ticket_id is undefined in ticket response');
}
}
// On successful authorization, show success and redirect
success.value = `Preauthorization successful! Transaction ID: ${response.data.auth_net_transaction_id}`;
setTimeout(() => {
router.push({ name: "auto" });
}, 2000);
}
else { // Handle 'charge' action
if (!chargeAmount.value || chargeAmount.value <= 0) {
throw new Error("Charge amount must be greater than zero.");
}
const chargePayload = {
card_id: selectedCard.value!.id,
charge_amount: chargeAmount.value.toFixed(2),
delivery_id: deliveryId.value,
};
const chargePath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/charge/saved-card/${customer.value.id}`;
console.log('=== DEBUG: Charge payload ===');
console.log('Calling endpoint:', chargePath);
console.log('Final payload being sent:', chargePayload);
const response = await axios.post(chargePath, chargePayload, { withCredentials: true, headers: authHeader() });
// Update auto_delivery status to 3
await axios.put(`${import.meta.env.VITE_AUTO_URL}/delivery/update_status/${deliveryId.value}`, {}, { withCredentials: true, headers: authHeader() });
// Status codes: 0 = APPROVED, 1 = DECLINED (based on backend TransactionStatus enum)
if (response.data && response.data.status === 0) { // 0 = APPROVED
if (!response.data.auth_net_transaction_id || response.data.auth_net_transaction_id.trim() === '') {
throw new Error("Failed transaction: No Authorize.net transaction ID received");
}
transactionId.value = response.data.id;
success.value = `Charge successful! Transaction ID: ${response.data.auth_net_transaction_id}`;
// Create Tickets_Auto_Delivery after successful charge
const ticketPayload = {
gallons_delivered: 0,
payment_type: 11, // 11 for Authorize charge
payment_card_id: selectedCard.value!.id,
payment_status: response.data.status // 0 = APPROVED
};
const ticketUrl = `${import.meta.env.VITE_AUTO_URL}/confirm/auto/create/${deliveryId.value}`;
console.log("POOOPP!")
await axios.post(ticketUrl, ticketPayload, { withCredentials: true, headers: authHeader() });
setTimeout(() => {
router.push({ name: "auto" });
}, 2000);
} else {
// The error message from your backend will be more specific now
throw new Error(`Payment charge failed: ${response.data?.rejection_reason || 'Unknown error'}`);
}
}
} catch (err: unknown) {
const axiosErr = err as AxiosError<{ detail?: string }>;
console.log(err)
error.value = axiosErr.response?.data?.detail || `Failed to ${actionType} payment`
notify({
title: "Error",
text: error.value,
type: "error",
})
} finally {
loading.value = false
action.value = ''
}
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -237,341 +237,349 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import { useRoute, useRouter } from 'vue-router'
import axios, { AxiosResponse, AxiosError } from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import type {
CustomerFormData,
CreditCardFormData,
PricingData,
CustomerDescriptionData,
AutoTicketData,
AutoDeliveryData,
PaymentCardResponse,
AuthorizeNetTransactionResponse
} from '../../../types/models'
export default defineComponent({ const route = useRoute()
name: 'captureAuthorizeAuto', const router = useRouter()
data() { // Reactive data
return { const loading = ref(false)
loading: false, const userCardfound = ref(false)
userCardfound: false, const gallonsDelivered = ref('')
gallonsDelivered: '', const captureAmount = ref(0)
captureAmount: 0, const preAuthAmount = ref(0)
preAuthAmount: 0, const transaction = ref<AuthorizeNetTransactionResponse | null>(null)
transaction: null as any, const showPaymentModal = ref(false)
showPaymentModal: false, const modalStep = ref(0)
modalStep: 0, const modalCapturedAmount = ref(0)
modalCapturedAmount: 0,
userCard: { const userCard = ref<CreditCardFormData>({
date_added: '', id: 0,
user_id: '', date_added: '',
card_number: '', user_id: '',
last_four_digits: '', card_number: '',
name_on_card: '', last_four_digits: '',
expiration_month: '', name_on_card: '',
expiration_year: '', expiration_month: '',
type_of_card: '', expiration_year: '',
security_number: '', type_of_card: '',
accepted_or_declined: '', security_number: '',
main_card: '', accepted_or_declined: '',
}, main_card: false,
customerDescription: {
customer_id: 0,
account_number: '',
company_id: 0,
fill_location: 0,
description: '',
},
customer: {
id: 0,
user_id: 0,
customer_address: '',
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
},
autoDelivery: {
id: 0,
customer_id: 0,
account_number: '',
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_full_name: '',
last_fill: '',
days_since_last_fill: 0,
last_updated: '',
estimated_gallons_left: 0,
estimated_gallons_left_prev_day: 0,
tank_height: '',
tank_size: '',
house_factor: 0,
auto_status: 0,
open_ticket_id: null,
},
autoTicket: {
id: 0,
customer_id: '',
account_number: '',
customer_town : '',
customer_state : '',
customer_address : '',
customer_zip: '',
customer_full_name : '',
oil_prices_id : '',
fill_date : '',
gallons_delivered : '',
price_per_gallon : '',
total_amount_customer : '',
payment_type : '',
payment_card_id : '',
payment_status : 0,
open_ticket_id: 0
},
pricing: {
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
date: "",
},
total_amount: 0,
discount: 0,
total_amount_after_discount: 0,
}
},
mounted() {
this.getAutoTicket(this.$route.params.id)
this.getTransaction()
},
methods: {
getAutoTicket(delivery_id: any) {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/autoticket/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.autoTicket = response.data;
console.log(this.autoTicket)
this.gallonsDelivered = this.autoTicket.gallons_delivered;
this.captureAmount = parseFloat(this.autoTicket.total_amount_customer || '0');
this.getCustomer(this.autoTicket.customer_id)
this.getAutoDelivery(this.autoTicket.id)
this.getCustomerDescription(this.autoTicket.customer_id)
if (this.autoTicket.payment_card_id) {
this.getPaymentCard(this.autoTicket.payment_card_id)
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
},
getAutoDelivery(delivery_id: any) {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/finddelivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.autoDelivery = response.data;
this.getCustomer(this.autoDelivery.customer_id)
this.getCustomerDescription(this.autoDelivery.customer_id)
})
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
},
getPaymentCard(card_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
if (response.data.userCard.card_number === ''){
this.userCardfound = false;
}
else{
this.userCard = response.data;
this.userCardfound = true;
}
})
.catch(() => {
});
},
getCustomer(user_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
axios.get(path, { withCredentials: true })
.then((response: any) => {
this.customer = response.data;
})
.catch((error: any) => {
notify({ title: "Error", text: "Could not find customer", type: "error" });
console.error("Error fetching customer:", error);
});
},
getCustomerDescription(user_id: any) {
let path = import.meta.env.VITE_BASE_URL + "/customer/description/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: any) => {
this.customerDescription = response.data;
this.loading = false
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
});
},
getTransaction() {
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/auto/transaction/delivery/${this.$route.params.id}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
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) => {
if (error.response && error.response.status === 404) {
notify({ title: "Info", text: "No pre-authorization found. Redirecting to customer profile to update payment method.", type: "info" });
console.log("No transaction found for Automatic - redirecting to customer profile");
this.$router.push({ name: 'customerProfile', params: { id: this.customer.id } });
} else {
notify({ title: "Error", text: "No pre-authorized transaction found", type: "error" });
this.$router.push({ name: 'finalizeTicketAuto', params: { id: this.$route.params.id } });
}
});
},
async capturePayment() {
if (this.autoTicket.payment_status !== 1) {
notify({ title: "Error", text: "Payment already captured or not ready for capture", type: "error" });
return;
}
if (!this.transaction || !this.captureAmount) {
notify({ title: "Error", text: "Invalid capture amount or transaction data", type: "error" });
return;
}
this.loading = true;
try {
const payload = {
charge_amount: this.captureAmount,
auth_net_transaction_id: this.transaction.auth_net_transaction_id
};
const url = `${import.meta.env.VITE_AUTHORIZE_URL}/api/capture/`;
const response = await axios.post(
url,
payload,
{ withCredentials: true, headers: authHeader() }
);
if (response.data && response.data.status === 0) {
this.autoTicket.payment_status = 3; // Update local status immediately
this.modalCapturedAmount = this.captureAmount;
this.showPaymentModal = true;
// Close the ticket and unassign from delivery
this.closeTicket(this.autoTicket.id);
setTimeout(() => { this.modalStep = 1 }, 2000);
setTimeout(() => { this.showPaymentModal = false; this.$router.push({ name: 'auto' }) }, 4000);
} else if (response.data && response.data.status === 1) {
const reason = response.data.rejection_reason || "The payment was declined by the gateway.";
notify({
title: "Payment Declined",
text: reason,
type: "warn",
});
} else {
throw new Error("Invalid response from server during capture.");
}
} catch (error: any) {
const detail = error.response?.data?.detail || "Failed to capture payment due to a server error.";
notify({
title: "Error",
text: detail,
type: "error",
});
console.error("Capture Payment Error:", error);
} finally {
this.loading = false;
}
},
cancelCapture() {
this.$router.push({ name: 'finalizeTicketAuto', params: { id: this.$route.params.id } });
},
async closeTicket(ticket_id: number) {
const path = `${import.meta.env.VITE_AUTO_URL}/confirm/auto/close_ticket/${ticket_id}`;
axios.put(path, {}, { withCredentials: true })
.then(() => {
console.log("Ticket closed successfully");
})
.catch((error: any) => {
notify({
title: "Warning",
text: "Payment captured, but failed to close ticket. Check manually.",
type: "warn",
});
console.error("Error closing ticket:", error);
});
},
getTypeColor(transactionType: number) {
switch (transactionType) {
case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge
case 2: return 'text-purple-600'; // Capture
case 3: return 'text-green-600'; // Delivery/Other
default: return 'text-gray-600';
}
},
},
}) })
const customerDescription = ref<CustomerDescriptionData>({
customer_id: 0,
account_number: '',
company_id: 0,
fill_location: 0,
description: '',
})
const customer = ref<CustomerFormData>({
id: 0,
user_id: 0,
customer_address: '',
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
})
const autoDelivery = ref<AutoDeliveryData>({
id: 0,
customer_id: 0,
account_number: '',
customer_town: '',
customer_state: 0,
customer_address: '',
customer_zip: '',
customer_full_name: '',
last_fill: '',
days_since_last_fill: 0,
last_updated: '',
estimated_gallons_left: 0,
estimated_gallons_left_prev_day: 0,
tank_height: '',
tank_size: '',
house_factor: 0,
auto_status: 0,
open_ticket_id: null,
})
const autoTicket = ref<AutoTicketData>({
id: 0,
customer_id: '',
account_number: '',
customer_town: '',
customer_state: '',
customer_address: '',
customer_zip: '',
customer_full_name: '',
oil_prices_id: '',
fill_date: '',
gallons_delivered: '',
price_per_gallon: '',
total_amount_customer: '',
payment_type: '',
payment_card_id: '',
payment_status: 0,
open_ticket_id: 0
})
const pricing = ref<PricingData>({
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
date: "",
})
const total_amount = ref(0)
const discount = ref(0)
const total_amount_after_discount = ref(0)
// Lifecycle
onMounted(() => {
getAutoTicket(route.params.id)
getTransaction()
})
// Functions
const getAutoTicket = (delivery_id: number | string) => {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/autoticket/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<AutoTicketData>) => {
autoTicket.value = response.data;
console.log(autoTicket.value)
gallonsDelivered.value = autoTicket.value.gallons_delivered;
captureAmount.value = parseFloat(autoTicket.value.total_amount_customer || '0');
getCustomer(autoTicket.value.customer_id)
getAutoDelivery(autoTicket.value.id)
getCustomerDescription(autoTicket.value.customer_id)
if (autoTicket.value.payment_card_id) {
getPaymentCard(autoTicket.value.payment_card_id)
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
}
const getAutoDelivery = (delivery_id: number) => {
let path = import.meta.env.VITE_AUTO_URL + "/delivery/finddelivery/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<AutoDeliveryData>) => {
autoDelivery.value = response.data;
getCustomer(autoDelivery.value.customer_id)
getCustomerDescription(autoDelivery.value.customer_id)
})
.catch(() => {
notify({
title: "Error",
text: "Could not get automatic",
type: "error",
});
});
}
const getPaymentCard = (card_id: number | string) => {
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<PaymentCardResponse>) => {
if (response.data.userCard.card_number === ''){
userCardfound.value = false;
}
else{
userCard.value = response.data.userCard as CreditCardFormData;
userCardfound.value = true;
}
})
.catch(() => {
});
}
const getCustomer = (user_id: number | string) => {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
axios.get<CustomerFormData>(path, { withCredentials: true })
.then((response) => {
customer.value = response.data;
})
.catch((error: Error) => {
notify({ title: "Error", text: "Could not find customer", type: "error" });
console.error("Error fetching customer:", error);
});
}
const getCustomerDescription = (user_id: number | string) => {
let path = import.meta.env.VITE_BASE_URL + "/customer/description/" + user_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<CustomerDescriptionData>) => {
customerDescription.value = response.data;
loading.value = false
})
.catch(() => {
notify({
title: "Error",
text: "Could not find customer",
type: "error",
});
});
}
const getTransaction = () => {
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/auto/transaction/delivery/${route.params.id}`;
axios.get<AuthorizeNetTransactionResponse>(path, { withCredentials: true, headers: authHeader() })
.then((response) => {
transaction.value = response.data;
preAuthAmount.value = parseFloat(String(response.data.preauthorize_amount) || '0');
if (response.data.status !== 0) { // Not approved
preAuthAmount.value = 0;
}
})
.catch((error: AxiosError) => {
if (error.response && error.response.status === 404) {
notify({ title: "Info", text: "No pre-authorization found. Redirecting to customer profile to update payment method.", type: "info" });
console.log("No transaction found for Automatic - redirecting to customer profile");
router.push({ name: 'customerProfile', params: { id: customer.value.id } });
} else {
notify({ title: "Error", text: "No pre-authorized transaction found", type: "error" });
router.push({ name: 'finalizeTicketAuto', params: { id: route.params.id } });
}
});
}
const capturePayment = async () => {
if (autoTicket.value.payment_status !== 1) {
notify({ title: "Error", text: "Payment already captured or not ready for capture", type: "error" });
return;
}
if (!transaction.value || !captureAmount.value) {
notify({ title: "Error", text: "Invalid capture amount or transaction data", type: "error" });
return;
}
loading.value = true;
try {
const payload = {
charge_amount: captureAmount.value,
auth_net_transaction_id: transaction.value.auth_net_transaction_id
};
const url = `${import.meta.env.VITE_AUTHORIZE_URL}/api/capture/`;
const response = await axios.post(
url,
payload,
{ withCredentials: true, headers: authHeader() }
);
if (response.data && response.data.status === 0) {
autoTicket.value.payment_status = 3; // Update local status immediately
modalCapturedAmount.value = captureAmount.value;
showPaymentModal.value = true;
// Close the ticket and unassign from delivery
closeTicket(autoTicket.value.id);
setTimeout(() => { modalStep.value = 1 }, 2000);
setTimeout(() => { showPaymentModal.value = false; router.push({ name: 'auto' }) }, 4000);
} else if (response.data && response.data.status === 1) {
const reason = response.data.rejection_reason || "The payment was declined by the gateway.";
notify({
title: "Payment Declined",
text: reason,
type: "warn",
});
} else {
throw new Error("Invalid response from server during capture.");
}
} catch (err: unknown) {
const error = err as AxiosError<{ detail?: string }>;
const detail = error.response?.data?.detail || "Failed to capture payment due to a server error.";
notify({
title: "Error",
text: detail,
type: "error",
});
console.error("Capture Payment Error:", error);
} finally {
loading.value = false;
}
}
const cancelCapture = () => {
router.push({ name: 'finalizeTicketAuto', params: { id: route.params.id } });
}
const closeTicket = async (ticket_id: number) => {
const path = `${import.meta.env.VITE_AUTO_URL}/confirm/auto/close_ticket/${ticket_id}`;
axios.put(path, {}, { withCredentials: true })
.then(() => {
console.log("Ticket closed successfully");
})
.catch((error: Error) => {
notify({
title: "Warning",
text: "Payment captured, but failed to close ticket. Check manually.",
type: "warn",
});
console.error("Error closing ticket:", error);
});
}
const getTypeColor = (transactionType: number) => {
switch (transactionType) {
case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge
case 2: return 'text-purple-600'; // Capture
case 3: return 'text-green-600'; // Delivery/Other
default: return 'text-gray-600';
}
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -172,501 +172,503 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import axios from 'axios' import { useRoute, useRouter } from 'vue-router'
import axios, { AxiosResponse, AxiosError } from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import type {
DeliveryFormData,
CustomerFormData,
CreditCardFormData,
PricingData,
PromoData,
DeliveryOrderResponse,
DeliveryTotalResponse,
OilPricingResponse,
PromoResponse,
WhoAmIResponse,
UpdateStatusResponse
} from '../../../types/models'
export default defineComponent({ // Router and route
name: 'AuthorizePreauthCharge', const route = useRoute()
const router = useRouter()
data() { // Reactive data
return { const deliveryId = ref(route.params.id as string)
deliveryId: this.$route.params.id as string, const loaded = ref(false)
loaded: false, const chargeAmount = ref(0)
chargeAmount: 0, const loading = ref(false)
loading: false, const action = ref('') // 'preauthorize' or 'charge'
action: '', // 'preauthorize' or 'charge' const error = ref('')
error: '', const success = ref('')
success: '', const isChargeConfirmationModalVisible = ref(false)
isChargeConfirmationModalVisible: false, const user = ref({
user: { user_id: 0,
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")
})
},
updateChargeAmount() {
// Only update if we have all necessary data
if (this.total_amount_after_discount > 0 &&
this.pricing.price_prime !== undefined &&
this.pricing.price_same_day !== undefined &&
this.pricing.price_emergency !== undefined) {
this.chargeAmount = this.calculateTotalAsNumber();
return true;
}
return false;
},
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 = parseFloat(response.data.total_amount) || 0;
this.discount = parseFloat(response.data.discount) || 0;
this.total_amount_after_discount = parseFloat(response.data.total_amount_after_discount) || 0;
// Try to update charge amount with complete pricing
const updated = this.updateChargeAmount();
// Fallback only if pricing not loaded yet and calculation didn't run
if (!updated) {
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
// Trigger a charge amount update if all data is available
this.updateChargeAmount();
}
})
},
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;
// Try to update charge amount when pricing is loaded
this.updateChargeAmount();
})
.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)
},
calculateTotalAmount() {
if (this.total_amount_after_discount == null || this.total_amount_after_discount === undefined) {
return '0.00';
}
let totalNum = Number(this.total_amount_after_discount);
if (isNaN(totalNum)) {
return '0.00';
}
if (this.delivery && this.delivery.prime == 1 && this.pricing && this.pricing.price_prime) {
totalNum += Number(this.pricing.price_prime) || 0;
}
if (this.delivery && this.delivery.same_day == 1 && this.pricing && this.pricing.price_same_day) {
totalNum += Number(this.pricing.price_same_day) || 0;
}
if (this.delivery && this.delivery.emergency == 1 && this.pricing && this.pricing.price_emergency) {
totalNum += Number(this.pricing.price_emergency) || 0;
}
return totalNum.toFixed(2);
},
calculateTotalAsNumber() {
if (this.total_amount_after_discount == null || this.total_amount_after_discount === undefined) {
return 0;
}
let totalNum = Number(this.total_amount_after_discount);
if (isNaN(totalNum)) {
return 0;
}
if (this.delivery && this.delivery.prime == 1 && this.pricing && this.pricing.price_prime) {
totalNum += Number(this.pricing.price_prime) || 0;
}
if (this.delivery && this.delivery.same_day == 1 && this.pricing && this.pricing.price_same_day) {
totalNum += Number(this.pricing.price_same_day) || 0;
}
if (this.delivery && this.delivery.emergency == 1 && this.pricing && this.pricing.price_emergency) {
totalNum += Number(this.pricing.price_emergency) || 0;
}
return totalNum;
},
async handlePreauthorize() {
await this.processPayment('preauthorize')
},
async handleChargeNow() {
if (!this.selectedCard) {
this.error = 'No credit card found for this customer'
return
}
if (!this.chargeAmount || this.chargeAmount <= 0) {
this.error = 'Please enter a valid charge amount'
return
}
this.isChargeConfirmationModalVisible = true
},
async proceedWithCharge() {
this.isChargeConfirmationModalVisible = false
await this.processPayment('charge')
},
cancelCharge() {
this.isChargeConfirmationModalVisible = false
},
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 {
// Step 2: If payment method is credit, perform the pre-authorization
if (actionType === 'preauthorize') {
if (!this.chargeAmount || this.chargeAmount <= 0) {
throw new Error("Pre-authorization amount must be greater than zero.");
}
const authPayload = {
card_id: (this.selectedCard as any).id,
preauthorize_amount: this.chargeAmount.toFixed(2),
delivery_id: this.delivery.id,
};
const authPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/authorize/saved-card/${this.customer.id}`;
const response = await axios.post(authPath, authPayload, { withCredentials: true, headers: authHeader() });
// Update payment type to 11 after successful preauthorization
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/authorize/${this.delivery.id}`, {}, { headers: authHeader() });
} catch (updateError) {
console.error('Failed to update payment type after preauthorization:', updateError);
}
// On successful authorization, show success and redirect
this.success = `Preauthorization successful! Transaction ID: ${response.data?.auth_net_transaction_id || 'N/A'}`;
setTimeout(() => {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
}, 2000);
} else { // Handle 'charge' action
if (!this.chargeAmount || this.chargeAmount <= 0) {
throw new Error("Charge amount must be greater than zero.");
}
// Create a payload that matches the backend's TransactionCreateByCardID schema
const chargePayload = {
card_id: (this.selectedCard as any).id,
charge_amount: this.chargeAmount.toFixed(2),
delivery_id: this.delivery.id,
service_id: this.delivery.service_id || null,
// You can add other fields here if your schema requires them
};
// Use the correct endpoint for charging a saved card
const chargePath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/charge/saved-card/${this.customer.id}`;
console.log('=== DEBUG: Charge payload ===');
console.log('Calling endpoint:', chargePath);
console.log('Final payload being sent:', chargePayload);
const response = await axios.post(chargePath, chargePayload, { withCredentials: true, headers: authHeader() });
// Update payment type to 11 after successful charge
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/authorize/${this.delivery.id}`, {}, { headers: authHeader() });
} catch (updateError) {
console.error('Failed to update payment type after charge:', updateError);
}
// Status codes: 0 = APPROVED, 1 = DECLINED (based on backend TransactionStatus enum)
if (response.data && response.data.status === 0) { // 0 = APPROVED
this.success = `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 {
// The error message from your backend will be more specific now
throw new Error(`Payment charge failed: ${response.data?.rejection_reason || 'Unknown error'}`);
}
}
} catch (error: any) {
console.log(error)
this.error = error.response?.data?.detail || `Failed to ${actionType} payment`
notify({
title: "Error",
text: this.error,
type: "error",
})
} finally {
this.loading = false
this.action = ''
}
}
},
}) })
const delivery = ref<DeliveryFormData>({
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,
})
const credit_cards = ref<CreditCardFormData[]>([
{
id: 0,
name_on_card: '',
main_card: false,
card_number: '',
expiration_month: '',
type_of_card: '',
last_four_digits: '',
expiration_year: '',
security_number: '',
}
])
const customer = ref<CustomerFormData>({
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: '',
})
const pricing = ref<PricingData>({
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
date: "",
})
const promo_active = ref(false)
const promo = ref<PromoData>({
name_of_promotion: '',
description: '',
money_off_delivery: 0,
text_on_ticket: ''
})
const total_amount = ref(0)
const discount = ref(0)
const total_amount_after_discount = ref(0)
// Computed properties
const selectedCard = computed(() => {
return credit_cards.value.find((card) => card.id === delivery.value.payment_card_id)
})
// Lifecycle
onMounted(() => {
loadData(deliveryId.value)
})
// Watchers
watch(() => route.params.id, (newId) => {
if (newId !== deliveryId.value) {
resetState()
deliveryId.value = newId as string
loadData(newId as string)
}
})
// Functions
const resetState = () => {
loading.value = false
action.value = ''
error.value = ''
success.value = ''
chargeAmount.value = 0
promo_active.value = false
total_amount.value = 0
discount.value = 0
total_amount_after_discount.value = 0
deliveryId.value = route.params.id as string
}
const loadData = (deliveryId: string) => {
userStatus()
getOilOrder(deliveryId)
sumdelivery(deliveryId)
getOilPricing()
updatestatus()
}
const updatestatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/delivery/updatestatus';
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<UpdateStatusResponse>) => {
if (response.data.update)
console.log("Updated Status of Deliveries")
})
}
const updateChargeAmount = () => {
// Only update if we have all necessary data
if (total_amount_after_discount.value > 0 &&
pricing.value.price_prime !== undefined &&
pricing.value.price_same_day !== undefined &&
pricing.value.price_emergency !== undefined) {
chargeAmount.value = calculateTotalAsNumber();
return true;
}
return false;
}
const sumdelivery = (delivery_id: number | string) => {
let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<DeliveryTotalResponse>) => {
if (response.data.ok) {
total_amount.value = parseFloat(String(response.data.total_amount)) || 0;
discount.value = parseFloat(String(response.data.discount)) || 0;
total_amount_after_discount.value = parseFloat(String(response.data.total_amount_after_discount)) || 0;
// Try to update charge amount with complete pricing
const updated = updateChargeAmount();
// Fallback only if pricing not loaded yet and calculation didn't run
if (!updated) {
if (promo_active.value) {
chargeAmount.value = total_amount_after_discount.value;
} else {
chargeAmount.value = total_amount.value;
}
}
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
}
const getPromo = (promo_id: number) => {
let path = import.meta.env.VITE_BASE_URL + "/promo/" + promo_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: AxiosResponse<PromoResponse>) => {
if (response.data) {
promo.value = response.data
promo_active.value = true
// Trigger a charge amount update if all data is available
updateChargeAmount();
}
})
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: AxiosResponse<WhoAmIResponse>) => {
if (response.data.ok) {
user.value = response.data.user;
}
})
}
const getOilPricing = () => {
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/table";
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<OilPricingResponse>) => {
pricing.value = response.data;
// Try to update charge amount when pricing is loaded
updateChargeAmount();
})
.catch(() => {
notify({
title: "Error",
text: "Could not get oil pricing",
type: "error",
});
});
}
const getOilOrder = (delivery_id: number | string) => {
let path = import.meta.env.VITE_BASE_URL + "/delivery/order/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<DeliveryOrderResponse>) => {
if (response.data && response.data.ok) {
delivery.value = response.data.delivery as DeliveryFormData;
getCustomer(delivery.value.customer_id)
getCreditCards(delivery.value.customer_id)
if (delivery.value.promo_id != null) {
getPromo(delivery.value.promo_id);
promo_active.value = true;
}
} else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
}
})
.catch((error: Error) => {
console.error("API Error in getOilOrder:", error);
notify({
title: "Error",
text: "Could not get delivery",
type: "error",
});
});
}
const getCreditCards = (user_id: number) => {
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + user_id;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<CreditCardFormData[]>) => {
credit_cards.value = response.data
})
}
const getCustomer = (userid: number) => {
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
axios({
method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<CustomerFormData>) => {
customer.value = response.data
})
}
const calculateSubtotal = () => {
const gallons = delivery.value.gallons_ordered || 0
const pricePerGallon = delivery.value.customer_price || 0
return (gallons * pricePerGallon).toFixed(2)
}
const calculateTotalAmount = () => {
if (total_amount_after_discount.value == null || total_amount_after_discount.value === undefined) {
return '0.00';
}
let totalNum = Number(total_amount_after_discount.value);
if (isNaN(totalNum)) {
return '0.00';
}
if (delivery.value && delivery.value.prime == 1 && pricing.value && pricing.value.price_prime) {
totalNum += Number(pricing.value.price_prime) || 0;
}
if (delivery.value && delivery.value.same_day == 1 && pricing.value && pricing.value.price_same_day) {
totalNum += Number(pricing.value.price_same_day) || 0;
}
if (delivery.value && delivery.value.emergency == 1 && pricing.value && pricing.value.price_emergency) {
totalNum += Number(pricing.value.price_emergency) || 0;
}
return totalNum.toFixed(2);
}
const calculateTotalAsNumber = () => {
if (total_amount_after_discount.value == null || total_amount_after_discount.value === undefined) {
return 0;
}
let totalNum = Number(total_amount_after_discount.value);
if (isNaN(totalNum)) {
return 0;
}
if (delivery.value && delivery.value.prime == 1 && pricing.value && pricing.value.price_prime) {
totalNum += Number(pricing.value.price_prime) || 0;
}
if (delivery.value && delivery.value.same_day == 1 && pricing.value && pricing.value.price_same_day) {
totalNum += Number(pricing.value.price_same_day) || 0;
}
if (delivery.value && delivery.value.emergency == 1 && pricing.value && pricing.value.price_emergency) {
totalNum += Number(pricing.value.price_emergency) || 0;
}
return totalNum;
}
const handlePreauthorize = async () => {
await processPayment('preauthorize')
}
const handleChargeNow = async () => {
if (!selectedCard.value) {
error.value = 'No credit card found for this customer'
return
}
if (!chargeAmount.value || chargeAmount.value <= 0) {
error.value = 'Please enter a valid charge amount'
return
}
isChargeConfirmationModalVisible.value = true
}
const proceedWithCharge = async () => {
isChargeConfirmationModalVisible.value = false
await processPayment('charge')
}
const cancelCharge = () => {
isChargeConfirmationModalVisible.value = false
}
const processPayment = async (actionType: string) => {
if (!selectedCard.value) {
error.value = 'No credit card found for this customer'
return
}
loading.value = true
action.value = actionType
error.value = ''
success.value = ''
try {
// Step 2: If payment method is credit, perform the pre-authorization
if (actionType === 'preauthorize') {
if (!chargeAmount.value || chargeAmount.value <= 0) {
throw new Error("Pre-authorization amount must be greater than zero.");
}
const authPayload = {
card_id: selectedCard.value!.id,
preauthorize_amount: chargeAmount.value.toFixed(2),
delivery_id: delivery.value.id,
};
const authPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/authorize/saved-card/${customer.value.id}`;
const response = await axios.post(authPath, authPayload, { withCredentials: true, headers: authHeader() });
// Update payment type to 11 after successful preauthorization
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/authorize/${delivery.value.id}`, {}, { headers: authHeader() });
} catch (updateError) {
console.error('Failed to update payment type after preauthorization:', updateError);
}
// On successful authorization, show success and redirect
success.value = `Preauthorization successful! Transaction ID: ${response.data?.auth_net_transaction_id || 'N/A'}`;
setTimeout(() => {
router.push({ name: "customerProfile", params: { id: customer.value.id } });
}, 2000);
} else { // Handle 'charge' action
if (!chargeAmount.value || chargeAmount.value <= 0) {
throw new Error("Charge amount must be greater than zero.");
}
// Create a payload that matches the backend's TransactionCreateByCardID schema
const chargePayload = {
card_id: selectedCard.value!.id,
charge_amount: chargeAmount.value.toFixed(2),
delivery_id: delivery.value.id,
service_id: delivery.value.service_id || null,
// You can add other fields here if your schema requires them
};
// Use the correct endpoint for charging a saved card
const chargePath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/charge/saved-card/${customer.value.id}`;
console.log('=== DEBUG: Charge payload ===');
console.log('Calling endpoint:', chargePath);
console.log('Final payload being sent:', chargePayload);
const response = await axios.post(chargePath, chargePayload, { withCredentials: true, headers: authHeader() });
// Update payment type to 11 after successful charge
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/authorize/${delivery.value.id}`, {}, { headers: authHeader() });
} catch (updateError) {
console.error('Failed to update payment type after charge:', updateError);
}
// Status codes: 0 = APPROVED, 1 = DECLINED (based on backend TransactionStatus enum)
if (response.data && response.data.status === 0) { // 0 = APPROVED
success.value = `Charge successful! Transaction ID: ${response.data.auth_net_transaction_id || 'N/A'}`;
setTimeout(() => {
router.push({ name: "customerProfile", params: { id: customer.value.id } });
}, 2000);
} else {
// The error message from your backend will be more specific now
throw new Error(`Payment charge failed: ${response.data?.rejection_reason || 'Unknown error'}`);
}
}
} catch (err: unknown) {
const axiosErr = err as AxiosError<{ detail?: string }>;
console.log(err)
error.value = axiosErr.response?.data?.detail || `Failed to ${actionType} payment`
notify({
title: "Error",
text: error.value,
type: "error",
})
} finally {
loading.value = false
action.value = ''
}
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -278,403 +278,409 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import { useRoute, useRouter } from 'vue-router'
import axios, { AxiosResponse, AxiosError } 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"
import type {
CustomerFormData,
CreditCardFormData,
PricingData,
PromoData,
DeliveryOrderResponse,
DeliveryTotalResponse,
OilPricingResponse,
PromoResponse,
PaymentCardResponse,
AuthorizeNetTransactionResponse
} from '../../../types/models'
export default defineComponent({ const route = useRoute()
name: 'captureAuthorize', const router = useRouter()
components: { // Reactive data
Header, const loading = ref(false)
SideBar, const userCardfound = ref(false)
Footer, const gallonsDelivered = ref('')
}, const captureAmount = ref(0)
const preAuthAmount = ref(0)
const transaction = ref<AuthorizeNetTransactionResponse | null>(null)
const showPaymentModal = ref(false)
const modalStep = ref(0)
const modalCapturedAmount = ref(0)
data() { const userCard = ref<CreditCardFormData>({
return { id: 0,
loading: false, date_added: '',
userCardfound: false, user_id: '',
gallonsDelivered: '', card_number: '',
captureAmount: 0, last_four_digits: '',
preAuthAmount: 0, name_on_card: '',
transaction: null as any, expiration_month: '',
showPaymentModal: false, expiration_year: '',
modalStep: 0, type_of_card: '',
modalCapturedAmount: 0, security_number: '',
accepted_or_declined: '',
userCard: { main_card: false,
date_added: '',
user_id: '',
card_number: '',
last_four_digits: '',
name_on_card: '',
expiration_month: '',
expiration_year: '',
type_of_card: '',
security_number: '',
accepted_or_declined: '',
main_card: '',
},
customer: {
id: 0,
user_id: 0,
customer_address: '',
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
},
deliveryOrder: {
id: '',
customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
gallons_ordered: 0,
customer_asked_for_fill: 0,
gallons_delivered: '',
customer_filled: 0,
delivery_status: 0,
when_ordered: '',
when_delivered: '',
expected_delivery_date: '',
automatic: 0,
oil_id: 0,
supplier_price: '',
customer_price: '',
customer_temperature: '',
dispatcher_notes: '',
prime: 0,
same_day: 0,
emergency: 0,
promo_id: 0,
payment_type: 0,
payment_card_id: '',
driver_employee_id: 0,
driver_first_name: '',
driver_last_name: '',
total_price: 0,
},
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,
}
},
mounted() {
this.getOilOrder(this.$route.params.id)
this.getOilPricing()
this.getTransaction()
},
methods: {
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 = parseFloat(response.data.total_amount) || 0;
this.discount = parseFloat(response.data.discount) || 0;
this.total_amount_after_discount = parseFloat(response.data.total_amount_after_discount) || 0;
// Set capture amount to the calculated total including fees and discount
this.captureAmount = this.calculateTotalAsNumber();
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not get totals",
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
}
})
},
getOilOrder(delivery_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/delivery/order/${delivery_id}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
if (response.data && response.data.ok) {
this.deliveryOrder = response.data.delivery;
this.gallonsDelivered = this.deliveryOrder.gallons_delivered;
this.getCustomer(this.deliveryOrder.customer_id);
this.sumdelivery(delivery_id);
if ([1, 2, 3].includes(this.deliveryOrder.payment_type)) {
this.getPaymentCard(this.deliveryOrder.payment_card_id);
}
if (this.deliveryOrder.promo_id != null) {
this.getPromo(this.deliveryOrder.promo_id);
this.promo_active = true;
}
} else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
}
})
.catch((error: any) => console.error("Error fetching oil order:", error));
},
getPaymentCard(card_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/${card_id}`;
axios.get(path, { withCredentials: true })
.then((response: any) => {
if (response.data.userCard && response.data.userCard.card_number !== '') {
this.userCard = response.data;
this.userCardfound = true;
}
})
.catch((error: any) => {
this.userCardfound = false;
console.error("Error fetching payment card:", error);
});
},
getCustomer(user_id: any) {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
axios.get(path, { withCredentials: true })
.then((response: any) => {
this.customer = response.data;
})
.catch((error: any) => {
notify({ title: "Error", text: "Could not find customer", type: "error" });
console.error("Error fetching customer:", error);
});
},
getOilPricing() {
const path = `${import.meta.env.VITE_BASE_URL}/info/price/oil/table`;
axios.get(path, { withCredentials: true })
.then((response: any) => {
this.pricing = response.data;
// Calculate capture amount if delivery order is already loaded
this.calculateCaptureAmount();
})
.catch((error: any) => {
notify({ title: "Error", text: "Could not get oil pricing", type: "error" });
console.error("Error fetching oil pricing:", error);
});
},
getTransaction() {
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/transaction/delivery/${this.$route.params.id}`;
axios.get(path, { withCredentials: true, headers: authHeader() })
.then((response: any) => {
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) => {
if (error.response && error.response.status === 404) {
notify({ title: "Info", text: "No pre-authorization found. Redirecting to customer profile to update payment method.", type: "info" });
console.log("No transaction found for delivery - redirecting to customer profile");
this.$router.push({ name: 'customerProfile', params: { id: this.customer.id } });
} else {
notify({ title: "Error", text: "No pre-authorized transaction found", type: "error" });
this.$router.push({ name: 'finalizeTicket', params: { id: this.$route.params.id } });
}
});
},
async capturePayment() {
if (!this.transaction || !this.captureAmount) {
notify({ title: "Error", text: "Invalid capture amount or transaction data", type: "error" });
return;
}
this.loading = true;
try {
const payload = {
charge_amount: this.captureAmount, // FastAPI handles string/number conversion
auth_net_transaction_id: this.transaction.auth_net_transaction_id
};
// ✅ FIX: Cleaned up URL, removing the unnecessary customer_id query parameter.
const url = `${import.meta.env.VITE_AUTHORIZE_URL}/api/capture/`;
const response = await axios.post(
url,
payload,
{ withCredentials: true, headers: authHeader() }
);
// ✅ FIX: Improved logic to handle both success and declines properly.
if (response.data && response.data.status === 0) {
// This is the APPROVED case
this.modalCapturedAmount = this.captureAmount;
this.showPaymentModal = true;
setTimeout(() => { this.modalStep = 1 }, 2000);
setTimeout(() => { this.showPaymentModal = false; this.$router.push({ name: 'deliveryOrder', params: { id: this.$route.params.id } }) }, 4000);
} else if (response.data && response.data.status === 1) {
// This is the DECLINED case
const reason = response.data.rejection_reason || "The payment was declined by the gateway.";
notify({
title: "Payment Declined",
text: reason,
type: "warn", // Use 'warn' for declines instead of 'error'
});
} else {
// This handles unexpected responses from the backend.
throw new Error("Invalid response from server during capture.");
}
} catch (error: any) {
// This 'catch' block now only handles network errors or server crashes (500 errors).
const detail = error.response?.data?.detail || "Failed to capture payment due to a server error.";
notify({
title: "Error",
text: detail,
type: "error",
});
console.error("Capture Payment Error:", error);
} finally {
this.loading = false;
}
},
calculateSubtotal() {
const gallons = parseFloat(this.gallonsDelivered || '0') || 0;
const pricePerGallon = typeof this.deliveryOrder.customer_price === 'string' ? parseFloat(this.deliveryOrder.customer_price) : Number(this.deliveryOrder.customer_price) || 0;
return (gallons * pricePerGallon).toFixed(2);
},
calculateTotalAmount() {
if (this.total_amount_after_discount == null || this.total_amount_after_discount === undefined) {
return '0.00';
}
let totalNum = Number(this.total_amount_after_discount);
if (isNaN(totalNum)) {
return '0.00';
}
if (this.deliveryOrder && this.deliveryOrder.prime == 1 && this.pricing && this.pricing.price_prime) {
totalNum += Number(this.pricing.price_prime) || 0;
}
if (this.deliveryOrder && this.deliveryOrder.same_day == 1 && this.pricing && this.pricing.price_same_day) {
totalNum += Number(this.pricing.price_same_day) || 0;
}
if (this.deliveryOrder && this.deliveryOrder.emergency == 1 && this.pricing && this.pricing.price_emergency) {
totalNum += Number(this.pricing.price_emergency) || 0;
}
return totalNum.toFixed(2);
},
calculateTotalAsNumber() {
if (this.total_amount_after_discount == null || this.total_amount_after_discount === undefined) {
return 0;
}
let totalNum = Number(this.total_amount_after_discount);
if (isNaN(totalNum)) {
return 0;
}
if (this.deliveryOrder && this.deliveryOrder.prime == 1 && this.pricing && this.pricing.price_prime) {
totalNum += Number(this.pricing.price_prime) || 0;
}
if (this.deliveryOrder && this.deliveryOrder.same_day == 1 && this.pricing && this.pricing.price_same_day) {
totalNum += Number(this.pricing.price_same_day) || 0;
}
if (this.deliveryOrder && this.deliveryOrder.emergency == 1 && this.pricing && this.pricing.price_emergency) {
totalNum += Number(this.pricing.price_emergency) || 0;
}
return totalNum;
},
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() {
this.$router.push({ name: 'finalizeTicket', params: { id: this.$route.params.id } });
},
getTypeColor(transactionType: number) {
switch (transactionType) {
case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge
case 2: return 'text-purple-600'; // Capture
case 3: return 'text-green-600'; // Delivery/Other
default: return 'text-gray-600';
}
},
},
}) })
const customer = ref<CustomerFormData>({
id: 0,
user_id: 0,
customer_address: '',
customer_first_name: '',
customer_last_name: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
customer_apt: '',
customer_home_type: 0,
customer_phone_number: '',
account_number: '',
})
const deliveryOrder = ref({
id: '',
customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
customer_state: 0,
customer_zip: '',
gallons_ordered: 0,
customer_asked_for_fill: 0,
gallons_delivered: '',
customer_filled: 0,
delivery_status: 0,
when_ordered: '',
when_delivered: '',
expected_delivery_date: '',
automatic: 0,
oil_id: 0,
supplier_price: '',
customer_price: '',
customer_temperature: '',
dispatcher_notes: '',
prime: 0,
same_day: 0,
emergency: 0,
promo_id: 0,
payment_type: 0,
payment_card_id: '',
driver_employee_id: 0,
driver_first_name: '',
driver_last_name: '',
total_price: 0,
})
const pricing = ref<PricingData>({
price_from_supplier: 0,
price_for_customer: 0,
price_for_employee: 0,
price_same_day: 0,
price_prime: 0,
price_emergency: 0,
date: "",
})
const promo_active = ref(false)
const promo = ref<PromoData>({
name_of_promotion: '',
description: '',
money_off_delivery: 0,
text_on_ticket: ''
})
const total_amount = ref(0)
const discount = ref(0)
const total_amount_after_discount = ref(0)
// Lifecycle
onMounted(() => {
getOilOrder(route.params.id)
getOilPricing()
getTransaction()
})
// Functions
const sumdelivery = (delivery_id: number | string) => {
let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
axios({
method: "get",
url: path,
withCredentials: true,
})
.then((response: AxiosResponse<DeliveryTotalResponse>) => {
if (response.data.ok) {
total_amount.value = parseFloat(String(response.data.total_amount)) || 0;
discount.value = parseFloat(String(response.data.discount)) || 0;
total_amount_after_discount.value = parseFloat(String(response.data.total_amount_after_discount)) || 0;
// Set capture amount to the calculated total including fees and discount
captureAmount.value = calculateTotalAsNumber();
}
})
.catch(() => {
notify({
title: "Error",
text: "Could not get totals",
type: "error",
});
});
}
const getPromo = (promo_id: number) => {
let path = import.meta.env.VITE_BASE_URL + "/promo/" + promo_id;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: AxiosResponse<PromoResponse>) => {
if (response.data) {
promo.value = response.data
promo_active.value = true
}
})
}
const getOilOrder = (delivery_id: number | string) => {
const path = `${import.meta.env.VITE_BASE_URL}/delivery/order/${delivery_id}`;
axios.get<DeliveryOrderResponse>(path, { withCredentials: true, headers: authHeader() })
.then((response) => {
if (response.data && response.data.ok) {
deliveryOrder.value = response.data.delivery as typeof deliveryOrder.value;
gallonsDelivered.value = deliveryOrder.value.gallons_delivered;
getCustomer(deliveryOrder.value.customer_id);
sumdelivery(delivery_id);
if ([1, 2, 3].includes(deliveryOrder.value.payment_type)) {
getPaymentCard(deliveryOrder.value.payment_card_id);
}
if (deliveryOrder.value.promo_id != null) {
getPromo(deliveryOrder.value.promo_id);
promo_active.value = true;
}
} else {
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
}
})
.catch((error: Error) => console.error("Error fetching oil order:", error));
}
const getPaymentCard = (card_id: number | string) => {
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/${card_id}`;
axios.get<PaymentCardResponse>(path, { withCredentials: true })
.then((response) => {
if (response.data.userCard && response.data.userCard.card_number !== '') {
userCard.value = response.data.userCard as CreditCardFormData;
userCardfound.value = true;
}
})
.catch((error: Error) => {
userCardfound.value = false;
console.error("Error fetching payment card:", error);
});
}
const getCustomer = (user_id: number) => {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
axios.get<CustomerFormData>(path, { withCredentials: true })
.then((response) => {
customer.value = response.data;
})
.catch((error: Error) => {
notify({ title: "Error", text: "Could not find customer", type: "error" });
console.error("Error fetching customer:", error);
});
}
const getOilPricing = () => {
const path = `${import.meta.env.VITE_BASE_URL}/info/price/oil/table`;
axios.get<OilPricingResponse>(path, { withCredentials: true })
.then((response) => {
pricing.value = response.data;
// Calculate capture amount if delivery order is already loaded
calculateCaptureAmount();
})
.catch((error: Error) => {
notify({ title: "Error", text: "Could not get oil pricing", type: "error" });
console.error("Error fetching oil pricing:", error);
});
}
const getTransaction = () => {
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/transaction/delivery/${route.params.id}`;
axios.get<AuthorizeNetTransactionResponse>(path, { withCredentials: true, headers: authHeader() })
.then((response) => {
transaction.value = response.data;
preAuthAmount.value = parseFloat(String(response.data.preauthorize_amount) || '0');
if (response.data.status !== 0) { // Not approved
preAuthAmount.value = 0;
}
})
.catch((error: AxiosError) => {
if (error.response && error.response.status === 404) {
notify({ title: "Info", text: "No pre-authorization found. Redirecting to customer profile to update payment method.", type: "info" });
console.log("No transaction found for delivery - redirecting to customer profile");
router.push({ name: 'customerProfile', params: { id: customer.value.id } });
} else {
notify({ title: "Error", text: "No pre-authorized transaction found", type: "error" });
router.push({ name: 'finalizeTicket', params: { id: route.params.id } });
}
});
}
const capturePayment = async () => {
if (!transaction.value || !captureAmount.value) {
notify({ title: "Error", text: "Invalid capture amount or transaction data", type: "error" });
return;
}
loading.value = true;
try {
const payload = {
charge_amount: captureAmount.value, // FastAPI handles string/number conversion
auth_net_transaction_id: transaction.value.auth_net_transaction_id
};
// ✅ FIX: Cleaned up URL, removing the unnecessary customer_id query parameter.
const url = `${import.meta.env.VITE_AUTHORIZE_URL}/api/capture/`;
const response = await axios.post(
url,
payload,
{ withCredentials: true, headers: authHeader() }
);
// ✅ FIX: Improved logic to handle both success and declines properly.
if (response.data && response.data.status === 0) {
// This is the APPROVED case
modalCapturedAmount.value = captureAmount.value;
showPaymentModal.value = true;
setTimeout(() => { modalStep.value = 1 }, 2000);
setTimeout(() => { showPaymentModal.value = false; router.push({ name: 'deliveryOrder', params: { id: route.params.id } }) }, 4000);
} else if (response.data && response.data.status === 1) {
// This is the DECLINED case
const reason = response.data.rejection_reason || "The payment was declined by the gateway.";
notify({
title: "Payment Declined",
text: reason,
type: "warn", // Use 'warn' for declines instead of 'error'
});
} else {
// This handles unexpected responses from the backend.
throw new Error("Invalid response from server during capture.");
}
} catch (err: unknown) {
// This 'catch' block now only handles network errors or server crashes (500 errors).
const error = err as AxiosError<{ detail?: string }>;
const detail = error.response?.data?.detail || "Failed to capture payment due to a server error.";
notify({
title: "Error",
text: detail,
type: "error",
});
console.error("Capture Payment Error:", error);
} finally {
loading.value = false;
}
}
const calculateSubtotal = () => {
const gallons = parseFloat(gallonsDelivered.value || '0') || 0;
const pricePerGallon = typeof deliveryOrder.value.customer_price === 'string' ? parseFloat(deliveryOrder.value.customer_price) : Number(deliveryOrder.value.customer_price) || 0;
return (gallons * pricePerGallon).toFixed(2);
}
const calculateTotalAmount = () => {
if (total_amount_after_discount.value == null || total_amount_after_discount.value === undefined) {
return '0.00';
}
let totalNum = Number(total_amount_after_discount.value);
if (isNaN(totalNum)) {
return '0.00';
}
if (deliveryOrder.value && deliveryOrder.value.prime == 1 && pricing.value && pricing.value.price_prime) {
totalNum += Number(pricing.value.price_prime) || 0;
}
if (deliveryOrder.value && deliveryOrder.value.same_day == 1 && pricing.value && pricing.value.price_same_day) {
totalNum += Number(pricing.value.price_same_day) || 0;
}
if (deliveryOrder.value && deliveryOrder.value.emergency == 1 && pricing.value && pricing.value.price_emergency) {
totalNum += Number(pricing.value.price_emergency) || 0;
}
return totalNum.toFixed(2);
}
const calculateTotalAsNumber = () => {
if (total_amount_after_discount.value == null || total_amount_after_discount.value === undefined) {
return 0;
}
let totalNum = Number(total_amount_after_discount.value);
if (isNaN(totalNum)) {
return 0;
}
if (deliveryOrder.value && deliveryOrder.value.prime == 1 && pricing.value && pricing.value.price_prime) {
totalNum += Number(pricing.value.price_prime) || 0;
}
if (deliveryOrder.value && deliveryOrder.value.same_day == 1 && pricing.value && pricing.value.price_same_day) {
totalNum += Number(pricing.value.price_same_day) || 0;
}
if (deliveryOrder.value && deliveryOrder.value.emergency == 1 && pricing.value && pricing.value.price_emergency) {
totalNum += Number(pricing.value.price_emergency) || 0;
}
return totalNum;
}
const calculateCaptureAmount = () => {
// Only calculate if we have both delivery order and pricing data
if (deliveryOrder.value.id && pricing.value.price_for_customer) {
const gallons = typeof gallonsDelivered.value === 'string' ? parseFloat(gallonsDelivered.value) : Number(gallonsDelivered.value) || 0;
const pricePerGallon = typeof deliveryOrder.value.customer_price === 'string' ? parseFloat(deliveryOrder.value.customer_price) : Number(deliveryOrder.value.customer_price) || 0;
let total = gallons * pricePerGallon;
// Add prime fee if applicable
if (deliveryOrder.value.prime == 1) {
const primeFee = typeof pricing.value.price_prime === 'string' ? parseFloat(pricing.value.price_prime) : Number(pricing.value.price_prime) || 0;
total += primeFee;
}
// Add same day fee if applicable
if (deliveryOrder.value.same_day === 1) {
const sameDayFee = typeof pricing.value.price_same_day === 'string' ? parseFloat(pricing.value.price_same_day) : Number(pricing.value.price_same_day) || 0;
total += sameDayFee;
}
captureAmount.value = total;
}
}
const cancelCapture = () => {
router.push({ name: 'finalizeTicket', params: { id: route.params.id } });
}
const getTypeColor = (transactionType: number) => {
switch (transactionType) {
case 1: return 'text-blue-600'; // Auth
case 0: return 'text-orange-600'; // Charge
case 2: return 'text-purple-600'; // Capture
case 3: return 'text-green-600'; // Delivery/Other
default: return 'text-gray-600';
}
}
</script> </script>
<style scoped></style> <style scoped></style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
import PayOil from './oil/pay_oil.vue'; const PayOil = () => import('./oil/pay_oil.vue');
import AuthorizePreauthCharge from './oil/authorize_preauthcharge.vue'; const AuthorizePreauthCharge = () => import('./oil/authorize_preauthcharge.vue');
import CaptureAuthorize from './oil/capture_authorize.vue'; const CaptureAuthorize = () => import('./oil/capture_authorize.vue');
import PayService from './service/pay_service.vue'; const PayService = () => import('./service/pay_service.vue');
import AuthorizeServicePreauthCharge from './service/authorize_preauthcharge.vue'; const AuthorizeServicePreauthCharge = () => import('./service/authorize_preauthcharge.vue');
import ChargeServiceAuthorize from './service/capture_authorize.vue'; const ChargeServiceAuthorize = () => import('./service/capture_authorize.vue');
import AuthorizePrechargeAutho from './auto/authorize_precharge_autho.vue'; const AuthorizePrechargeAutho = () => import('./auto/authorize_precharge_autho.vue');
import CaptureAuthorizeAutho from './auto/capture_authorize_autho.vue'; const CaptureAuthorizeAutho = () => import('./auto/capture_authorize_autho.vue');
const payRoutes = [ const payRoutes = [
// This is for oil delivery // This is for oil delivery

View File

@@ -154,383 +154,382 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import axios from 'axios' import { useRoute, useRouter } from 'vue-router'
import axios, { AxiosResponse, AxiosError } from 'axios'
import authHeader from '../../../services/auth.header' import authHeader from '../../../services/auth.header'
import { notify } from "@kyvg/vue3-notification" import { notify } from "@kyvg/vue3-notification"
import type {
CustomerFormData,
CreditCardFormData,
WhoAmIResponse
} from '../../../types/models'
export default defineComponent({ const route = useRoute()
name: 'AuthorizeServicePreauthCharge', const router = useRouter()
data() { interface ServiceFormData {
return { id: number;
serviceId: this.$route.params.id as string, scheduled_date: string;
loaded: false, customer_id: number;
chargeAmount: 0, customer_name: string;
loading: false, customer_address: string;
action: '', // 'preauthorize' or 'charge' customer_town: string;
error: '', type_service_call: number;
success: '', description: string;
selectedCardId: null as number | null, // Track which card is selected service_cost: string;
user: { payment_card_id: number;
user_id: 0, }
},
service: {
id: 0,
scheduled_date: '',
customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
type_service_call: 0,
description: '',
service_cost: '',
payment_card_id: 0,
},
credit_cards: [
{
id: 0,
name_on_card: '',
main_card: false,
card_number: '',
expiration_month: '',
type_of_card: '',
last_four_digits: '',
expiration_year: '',
security_number: '',
} // Reactive data
], const serviceId = ref(route.params.id as string)
customer: { const loaded = ref(false)
id: 0, const chargeAmount = ref(0)
user_id: 0, const loading = ref(false)
customer_first_name: '', const action = ref('') // 'preauthorize' or 'charge'
customer_last_name: '', const error = ref('')
customer_town: '', const success = ref('')
customer_address: '', const selectedCardId = ref(null as number | null) // Track which card is selected
customer_state: 0, const user = ref({
customer_zip: '', user_id: 0,
customer_apt: '', })
customer_home_type: 0, const service = ref<ServiceFormData>({
customer_phone_number: '', id: 0,
account_number: '', scheduled_date: '',
}, customer_id: 0,
customer_name: '',
customer_address: '',
customer_town: '',
type_service_call: 0,
description: '',
service_cost: '',
payment_card_id: 0,
})
const credit_cards = ref<CreditCardFormData[]>([
{
id: 0,
name_on_card: '',
main_card: false,
card_number: '',
expiration_month: '',
type_of_card: '',
last_four_digits: '',
expiration_year: '',
security_number: '',
}
])
const customer = ref<CustomerFormData>({
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: '',
})
} // Computed
}, const selectedCard = computed(() => {
// If user has selected a card manually, use that
if (selectedCardId.value) {
return credit_cards.value.find((card) => card.id === selectedCardId.value)
}
// Otherwise use automatic selection logic
// First try to find payment_card_id from service
if (service.value.payment_card_id && service.value.payment_card_id > 0) {
return credit_cards.value.find((card) => card.id === service.value.payment_card_id)
}
// Otherwise return the primary card (main_card = true)
return credit_cards.value.find((card) => card.main_card === true) ||
credit_cards.value.find((card) => card.id > 0) || // Any card if no primary
null
})
computed: { // Watchers
selectedCard(): any { watch(() => route.params.id, (newId) => {
// If user has selected a card manually, use that if (newId !== serviceId.value) {
if (this.selectedCardId) { resetState()
return this.credit_cards.find((card: any) => card.id === this.selectedCardId) serviceId.value = newId as string
loadData(newId as string)
}
})
// Lifecycle
onMounted(() => {
loadData(serviceId.value)
})
// Functions
const resetState = () => {
loading.value = false
action.value = ''
error.value = ''
success.value = ''
chargeAmount.value = 0
serviceId.value = route.params.id as string
}
const loadData = (serviceId: string) => {
userStatus()
getServiceOrder(serviceId)
}
const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
axios({
method: 'get',
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: AxiosResponse<WhoAmIResponse>) => {
if (response.data.ok) {
user.value = response.data.user;
} }
// Otherwise use automatic selection logic })
// First try to find payment_card_id from service }
if (this.service.payment_card_id && this.service.payment_card_id > 0) {
return this.credit_cards.find((card: any) => card.id === this.service.payment_card_id)
}
// Otherwise return the primary card (main_card = true)
return this.credit_cards.find((card: any) => card.main_card === true) ||
this.credit_cards.find((card: any) => card.id > 0) || // Any card if no primary
null
}
},
mounted() { const getServiceOrder = (serviceId: number | string) => {
this.loadData(this.serviceId) let path = import.meta.env.VITE_BASE_URL + "/service/" + serviceId;
}, axios({
method: "get",
created() { url: path,
this.watchRoute() withCredentials: true,
}, headers: authHeader(),
})
methods: { .then((response: AxiosResponse<{ ok?: boolean; service?: ServiceFormData } | ServiceFormData[] | ServiceFormData>) => {
watchRoute() { let serviceData: ServiceFormData | undefined;
watch( if (response.data) {
() => this.$route.params.id, // Handle different API response structures
(newId) => { if ('service' in response.data && response.data.service) {
if (newId !== this.serviceId) { // API returns {ok: true, service: {...}} structure
this.resetState() serviceData = response.data.service;
this.serviceId = newId as string } else if (Array.isArray(response.data)) {
this.loadData(newId as string) serviceData = response.data[0]; // Array response
} } else if ('id' in response.data) {
serviceData = response.data as ServiceFormData; // Direct object response
} }
)
},
resetState() { if (serviceData && serviceData.id) {
this.loading = false service.value = {
this.action = '' id: serviceData.id,
this.error = '' scheduled_date: serviceData.scheduled_date,
this.success = '' customer_id: serviceData.customer_id,
this.chargeAmount = 0 customer_name: serviceData.customer_name,
this.serviceId = this.$route.params.id as string customer_address: serviceData.customer_address,
}, customer_town: serviceData.customer_town,
type_service_call: serviceData.type_service_call,
description: serviceData.description,
service_cost: serviceData.service_cost,
payment_card_id: serviceData.payment_card_id || 0,
};
loadData(serviceId: string) { // Fetch related data
this.userStatus() getCustomer(service.value.customer_id);
this.getServiceOrder(serviceId) getCreditCards(service.value.customer_id);
}, } else {
console.error("API Error: Invalid service data received:", serviceData);
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;
}
})
},
getServiceOrder(serviceId: any) {
let path = import.meta.env.VITE_BASE_URL + "/service/" + serviceId;
axios({
method: "get",
url: path,
withCredentials: true,
headers: authHeader(),
})
.then((response: any) => {
let serviceData;
if (response.data) {
// Handle different API response structures
if (response.data.service) {
// API returns {ok: true, service: {...}} structure
serviceData = response.data.service;
} else if (Array.isArray(response.data)) {
serviceData = response.data[0]; // Array response
} else {
serviceData = response.data; // Direct object response
}
if (serviceData && serviceData.id) {
this.service = {
id: serviceData.id,
scheduled_date: serviceData.scheduled_date,
customer_id: serviceData.customer_id,
customer_name: serviceData.customer_name,
customer_address: serviceData.customer_address,
customer_town: serviceData.customer_town,
type_service_call: serviceData.type_service_call,
description: serviceData.description,
service_cost: serviceData.service_cost,
payment_card_id: serviceData.payment_card_id || 0,
};
// Fetch related data
this.getCustomer(this.service.customer_id);
this.getCreditCards(this.service.customer_id);
} else {
console.error("API Error: Invalid service data received:", serviceData);
notify({
title: "Error",
text: "Invalid service data received",
type: "error",
});
}
} else {
console.error("API Error: No response data received");
notify({
title: "Error",
text: "Could not get service data",
type: "error",
});
}
})
.catch((error: any) => {
console.error("API Error in getServiceOrder:", error);
notify({ notify({
title: "Error", title: "Error",
text: "Could not get service data", text: "Invalid service data received",
type: "error", 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) => {
console.log('Credit cards loaded:', response.data?.length || 0, 'cards');
this.credit_cards = response.data || [];
}).catch((error: any) => {
console.error('Failed to load credit cards:', error);
this.credit_cards = [];
});
},
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
})
},
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 {
// Step 2: If payment method is credit, perform the pre-authorization
if (actionType === 'preauthorize') {
if (!this.chargeAmount || this.chargeAmount <= 0) {
throw new Error("Pre-authorization amount must be greater than zero.");
}
const authPayload = {
card_id: (this.selectedCard as any).id,
preauthorize_amount: this.chargeAmount.toFixed(2),
service_id: this.service.id,
delivery_id: null, // No delivery for services
};
const authPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/authorize/saved-card/${this.customer.id}`;
const response = await axios.post(authPath, authPayload, { withCredentials: true, headers: authHeader() });
// Update payment type to 11 after successful preauthorization
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/authorize/service/${this.service.id}`, {
card_id: (this.selectedCard as any).id,
status: actionType === 'preauthorize' ? 1 : 3
}, { headers: authHeader() });
} catch (updateError) {
console.error('Failed to update payment type after preauthorization:', updateError);
}
// On successful authorization, show success and redirect
this.success = `Preauthorization successful! Transaction ID: ${response.data?.auth_net_transaction_id || 'N/A'}`;
setTimeout(() => {
this.$router.push({ name: "customerProfile", params: { id: this.customer.id } });
}, 2000);
} else { // Handle 'charge' action
if (!this.chargeAmount || this.chargeAmount <= 0) {
throw new Error("Charge amount must be greater than zero.");
}
// Create a payload that matches the backend's TransactionCreateByCardID schema
const chargePayload = {
card_id: (this.selectedCard as any).id,
charge_amount: this.chargeAmount.toFixed(2),
service_id: this.service.id,
delivery_id: null, // No delivery for services
// You can add other fields here if your schema requires them
};
// Use the correct endpoint for charging a saved card
const chargePath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/charge/saved-card/${this.customer.id}`;
console.log('=== DEBUG: Charge payload ===');
console.log('Calling endpoint:', chargePath);
console.log('Final payload being sent:', chargePayload);
const response = await axios.post(chargePath, chargePayload, { withCredentials: true, headers: authHeader() });
// Update service cost to the charged amount using new dedicated API
try {
await axios.put(
`${import.meta.env.VITE_BASE_URL}/service/update-cost/${this.service.id}`,
{ service_cost: this.chargeAmount },
{ headers: authHeader(), withCredentials: true }
);
console.log(`✅ Updated service cost to ${this.chargeAmount} for service ${this.service.id}`);
} catch (costError) {
console.error('❌ Failed to update service cost:', costError);
}
// Update payment status after successful charge
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/capture/service/${this.service.id}`, {
card_id: (this.selectedCard as any).id,
status: 3 // Approved status
}, {
headers: authHeader(),
withCredentials: true
});
} catch (updateError) {
console.error('Failed to update payment status after charge:', updateError);
}
// Status codes: 0 = APPROVED, 1 = DECLINED (based on backend TransactionStatus enum)
if (response.data && response.data.status === 0) { // 0 = APPROVED
this.success = `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 {
// The error message from your backend will be more specific now
throw new Error(`Payment charge failed: ${response.data?.rejection_reason || 'Unknown error'}`);
}
} }
} catch (error: any) { } else {
console.log(error) console.error("API Error: No response data received");
this.error = error.response?.data?.detail || `Failed to ${actionType} payment`
notify({ notify({
title: "Error", title: "Error",
text: this.error, text: "Could not get service data",
type: "error", type: "error",
}) });
} finally {
this.loading = false
this.action = ''
} }
}, })
.catch((err: Error) => {
console.error("API Error in getServiceOrder:", err);
notify({
title: "Error",
text: "Could not get service data",
type: "error",
});
});
}
getServiceTypeName(typeId: number): string { const getCreditCards = (user_id: number) => {
const typeMap: { [key: number]: string } = { 0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other' }; let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + user_id;
return typeMap[typeId] || 'Unknown'; axios({
}, method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<CreditCardFormData[]>) => {
console.log('Credit cards loaded:', response.data?.length || 0, 'cards');
credit_cards.value = response.data || [];
}).catch((err: Error) => {
console.error('Failed to load credit cards:', err);
credit_cards.value = [];
});
}
getServiceTypeColor(typeId: number): string { const getCustomer = (userid: number) => {
const colorMap: { [key: number]: string } = { 0: 'primary', 1: 'error', 2: 'warning', 3: 'info', 4: 'neutral' }; let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
return `badge-${colorMap[typeId] || 'neutral'}`; axios({
}, method: 'get',
url: path,
headers: authHeader(),
}).then((response: AxiosResponse<CustomerFormData>) => {
customer.value = response.data
})
}
selectCard(cardId: number) { const handlePreauthorize = async () => {
this.selectedCardId = cardId; await processPayment('preauthorize')
}, }
const handleChargeNow = async () => {
await processPayment('charge')
}
const processPayment = async (actionType: string) => {
if (!selectedCard.value) {
error.value = 'No credit card found for this customer'
return
}
formatScheduledDate(dateString: string): string { loading.value = true
if (!dateString) return 'Not scheduled'; action.value = actionType
return dateString; // Could format with dayjs if needed error.value = ''
success.value = ''
try {
// Step 2: If payment method is credit, perform the pre-authorization
if (actionType === 'preauthorize') {
if (!chargeAmount.value || chargeAmount.value <= 0) {
throw new Error("Pre-authorization amount must be greater than zero.");
}
const authPayload = {
card_id: selectedCard.value!.id,
preauthorize_amount: chargeAmount.value.toFixed(2),
service_id: service.value.id,
delivery_id: null, // No delivery for services
};
const authPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/authorize/saved-card/${customer.value.id}`;
const response = await axios.post(authPath, authPayload, { withCredentials: true, headers: authHeader() });
// Update payment type to 11 after successful preauthorization
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/authorize/service/${service.value.id}`, {
card_id: selectedCard.value!.id,
status: actionType === 'preauthorize' ? 1 : 3
}, { headers: authHeader() });
} catch (updateError) {
console.error('Failed to update payment type after preauthorization:', updateError);
}
// On successful authorization, show success and redirect
success.value = `Preauthorization successful! Transaction ID: ${response.data?.auth_net_transaction_id || 'N/A'}`;
setTimeout(() => {
router.push({ name: "customerProfile", params: { id: customer.value.id } });
}, 2000);
} else { // Handle 'charge' action
if (!chargeAmount.value || chargeAmount.value <= 0) {
throw new Error("Charge amount must be greater than zero.");
}
// Create a payload that matches the backend's TransactionCreateByCardID schema
const chargePayload = {
card_id: selectedCard.value!.id,
charge_amount: chargeAmount.value.toFixed(2),
service_id: service.value.id,
delivery_id: null, // No delivery for services
// You can add other fields here if your schema requires them
};
// Use the correct endpoint for charging a saved card
const chargePath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/charge/saved-card/${customer.value.id}`;
console.log('=== DEBUG: Charge payload ===');
console.log('Calling endpoint:', chargePath);
console.log('Final payload being sent:', chargePayload);
const response = await axios.post(chargePath, chargePayload, { withCredentials: true, headers: authHeader() });
// Update service cost to the charged amount using new dedicated API
try {
await axios.put(
`${import.meta.env.VITE_BASE_URL}/service/update-cost/${service.value.id}`,
{ service_cost: chargeAmount.value },
{ headers: authHeader(), withCredentials: true }
);
console.log(`✅ Updated service cost to ${chargeAmount.value} for service ${service.value.id}`);
} catch (costError) {
console.error('❌ Failed to update service cost:', costError);
}
// Update payment status after successful charge
try {
await axios.put(`${import.meta.env.VITE_BASE_URL}/payment/capture/service/${service.value.id}`, {
card_id: selectedCard.value!.id,
status: 3 // Approved status
}, {
headers: authHeader(),
withCredentials: true
});
} catch (updateError) {
console.error('Failed to update payment status after charge:', updateError);
}
// Status codes: 0 = APPROVED, 1 = DECLINED (based on backend TransactionStatus enum)
if (response.data && response.data.status === 0) { // 0 = APPROVED
success.value = `Charge successful! Transaction ID: ${response.data.auth_net_transaction_id || 'N/A'}`;
setTimeout(() => {
router.push({ name: "customerProfile", params: { id: customer.value.id } });
}, 2000);
} else {
// The error message from your backend will be more specific now
throw new Error(`Payment charge failed: ${response.data?.rejection_reason || 'Unknown error'}`);
}
} }
}, } catch (err: unknown) {
}) const axiosErr = err as AxiosError<{ detail?: string }>;
console.log(err)
error.value = axiosErr.response?.data?.detail || `Failed to ${actionType} payment`
notify({
title: "Error",
text: error.value,
type: "error",
})
} finally {
loading.value = false
action.value = ''
}
}
const getServiceTypeName = (typeId: number): string => {
const typeMap: { [key: number]: string } = { 0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other' };
return typeMap[typeId] || 'Unknown';
}
const getServiceTypeColor = (typeId: number): string => {
const colorMap: { [key: number]: string } = { 0: 'primary', 1: 'error', 2: 'warning', 3: 'info', 4: 'neutral' };
return `badge-${colorMap[typeId] || 'neutral'}`;
}
const selectCard = (cardId: number) => {
selectedCardId.value = cardId;
}
const formatScheduledDate = (dateString: string): string => {
if (!dateString) return 'Not scheduled';
return dateString; // Could format with dayjs if needed
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -264,7 +264,7 @@ const service = ref<Service | null>(null);
const preAuthCard = ref<UserCard | null>(null); const preAuthCard = ref<UserCard | null>(null);
const selectedCard = ref<UserCard | null>(null); const selectedCard = ref<UserCard | null>(null);
const chargeAmount = ref<number>(0); const chargeAmount = ref<number>(0);
const transaction = ref(null as any); const transaction = ref<ServiceTransaction | null>(null);
const preAuthAmount = ref<number>(0); const preAuthAmount = ref<number>(0);
const serviceTransactions = ref<ServiceTransaction[]>([]); const serviceTransactions = ref<ServiceTransaction[]>([]);
const showPaymentModal = ref(false); const showPaymentModal = ref(false);
@@ -356,7 +356,8 @@ const updateServiceCost = async (serviceId: number, newCost: number): Promise<bo
console.error(`❌ SERVICE COST UPDATE FAILED:`, response.data); console.error(`❌ SERVICE COST UPDATE FAILED:`, response.data);
return false; return false;
} }
} catch (error: any) { } catch (err: unknown) {
const error = err as { response?: { data?: unknown }; message?: string };
console.error(`💥 ERROR UPDATING SERVICE COST:`, error.response?.data || error.message); console.error(`💥 ERROR UPDATING SERVICE COST:`, error.response?.data || error.message);
return false; return false;
} }
@@ -459,7 +460,8 @@ const chargeService = async () => {
throw new Error("Invalid response from server during capture."); throw new Error("Invalid response from server during capture.");
} }
} catch (error: any) { } catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } }; message?: string };
const detail = error.response?.data?.detail || "Failed to process payment due to a server error."; const detail = error.response?.data?.detail || "Failed to process payment due to a server error.";
notify({ title: "Error", text: detail, type: "error" }); notify({ title: "Error", text: detail, type: "error" });
console.error("Charge/Capture Service Error:", error); console.error("Charge/Capture Service Error:", error);
@@ -489,7 +491,7 @@ const getTransaction = async () => {
selectedCard.value = cardResponse.data; selectedCard.value = cardResponse.data;
chargeAmount.value = preAuthAmount.value; chargeAmount.value = preAuthAmount.value;
} }
} catch (error: any) { } catch (error: unknown) {
console.error("No pre-authorized transaction found for service:", error); console.error("No pre-authorized transaction found for service:", error);
preAuthAmount.value = service.value?.service_cost || 0; preAuthAmount.value = service.value?.service_cost || 0;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -35,8 +35,8 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { ref, onMounted } from 'vue';
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';
@@ -44,7 +44,7 @@ import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid'; import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
import { CalendarOptions, EventClickArg } from '@fullcalendar/core'; import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
import ServiceEditModal from './ServiceEditModal.vue'; import ServiceEditModal from './ServiceEditModal.vue';
import axios from 'axios'; import axios from 'axios';
import authHeader from '../../services/auth.header'; import authHeader from '../../services/auth.header';
@@ -60,113 +60,111 @@ interface ServiceCall {
service_cost: string; service_cost: string;
} }
export default defineComponent({ // Reactive data
name: 'ServiceCalendar', const user = ref(null)
components: { Header, SideBar, Footer, FullCalendar, ServiceEditModal }, const selectedServiceForEdit = ref(null as Partial<ServiceCall> | null)
data() { const fullCalendar = ref()
return {
user: null, // Functions
selectedServiceForEdit: null as Partial<ServiceCall> | null, // We can remove the fetchEvents method as FullCalendar now handles it.
calendarOptions: { // async fetchEvents(): Promise<void> { ... }
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth', const handleEventClick = (clickInfo: EventClickArg): void => {
weekends: true, // This logic remains the same, as it correctly pulls data from extendedProps
// Instead of a static array, we use a function source. selectedServiceForEdit.value = {
// This is the standard way FullCalendar fetches events. id: parseInt(clickInfo.event.id),
events: `${import.meta.env.VITE_BASE_URL}/service/all`, scheduled_date: clickInfo.event.startStr,
eventClick: this.handleEventClick, customer_name: clickInfo.event.title.split(': ')[1] || 'Unknown Customer',
// Add headers for authentication if needed by your API customer_id: clickInfo.event.extendedProps.customer_id,
eventSourceSuccess: (content) => { type_service_call: clickInfo.event.extendedProps.type_service_call,
// This is where you could transform data if needed description: clickInfo.event.extendedProps.description,
return content; service_cost: clickInfo.event.extendedProps.service_cost,
}, };
eventSourceFailure: (error) => { }
console.error("Failed to fetch calendar events:", error);
} // Calendar options
} as CalendarOptions, const calendarOptions = ref({
}; plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
weekends: true,
// Instead of a static array, we use a function source.
// This is the standard way FullCalendar fetches events.
events: `${import.meta.env.VITE_BASE_URL}/service/all`,
eventClick: handleEventClick,
// Add headers for authentication if needed by your API
eventSourceSuccess: (content) => {
// This is where you could transform data if needed
return content;
}, },
created() { eventSourceFailure: (error) => {
this.userStatus(); console.error("Failed to fetch calendar events:", error);
// We no longer need to call fetchEvents() here because FullCalendar does it automatically. }
}, } as CalendarOptions)
methods: {
// We can remove the fetchEvents method as FullCalendar now handles it.
// async fetchEvents(): Promise<void> { ... }
handleEventClick(clickInfo: EventClickArg): void { // Lifecycle
// This logic remains the same, as it correctly pulls data from extendedProps onMounted(() => {
this.selectedServiceForEdit = { userStatus();
id: parseInt(clickInfo.event.id), // We no longer need to call fetchEvents() here because FullCalendar does it automatically.
scheduled_date: clickInfo.event.startStr, })
customer_name: clickInfo.event.title.split(': ')[1] || 'Unknown Customer',
customer_id: clickInfo.event.extendedProps.customer_id,
type_service_call: clickInfo.event.extendedProps.type_service_call,
description: clickInfo.event.extendedProps.description,
service_cost: clickInfo.event.extendedProps.service_cost,
};
},
closeEditModal() { const closeEditModal = () => {
this.selectedServiceForEdit = null; selectedServiceForEdit.value = null;
}, }
// =================== THIS IS THE CORRECTED SECTION =================== // =================== THIS IS THE CORRECTED SECTION ===================
async handleSaveChanges(updatedService: ServiceCall) { const handleSaveChanges = async (updatedService: ServiceCall) => {
try { try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`; const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true }); await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
// Get the FullCalendar component instance from the ref // Get the FullCalendar component instance from the ref
const calendarApi = (this.$refs.fullCalendar as any).getApi(); const calendarApi = (fullCalendar.value as any).getApi();
if (calendarApi) { if (calendarApi) {
// Tell FullCalendar to re-fetch its events from the source. // Tell FullCalendar to re-fetch its events from the source.
// This is the most reliable way to refresh the view immediately. // This is the most reliable way to refresh the view immediately.
calendarApi.refetchEvents(); calendarApi.refetchEvents();
} }
this.closeEditModal(); closeEditModal();
} catch (error) { } catch (error) {
console.error("Failed to save changes:", error); console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console."); alert("An error occurred while saving. Please check the console.");
}
}
// =================== END OF CORRECTED SECTION ===================
const handleDeleteService = async (serviceId: number) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
await axios.delete(path, { withCredentials: true, headers: authHeader() });
// Also refresh the calendar after a delete
const calendarApi = (fullCalendar.value as any).getApi();
if (calendarApi) {
calendarApi.refetchEvents();
}
closeEditModal();
} catch (error) {
console.error("Error deleting event:", error);
}
}
const 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) {
user.value = response.data.user;
} }
}, })
// =================== END OF CORRECTED SECTION =================== .catch(() => {
user.value = null
async handleDeleteService(serviceId: number) { })
try { }
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
await axios.delete(path, { withCredentials: true, headers: authHeader() });
// Also refresh the calendar after a delete
const calendarApi = (this.$refs.fullCalendar as any).getApi();
if (calendarApi) {
calendarApi.refetchEvents();
}
this.closeEditModal();
} catch (error) {
console.error("Error deleting event:", error);
}
},
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;
}
})
.catch(() => {
this.user = null
})
},
},
});
</script> </script>

View File

@@ -99,8 +99,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, PropType } from 'vue'; import { ref, watch } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import axios from 'axios'; import axios from 'axios';
import authHeader from '../../services/auth.header'; import authHeader from '../../services/auth.header';
@@ -111,88 +111,94 @@ interface EditableService extends Omit<ServiceCall, 'scheduled_date'> { date: st
interface Customer { id: number; account_number: string; customer_first_name: string; customer_last_name: string; customer_address: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; } interface Customer { id: number; account_number: string; customer_first_name: string; customer_last_name: string; customer_address: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; }
interface ServiceParts { customer_id: number; oil_filter: string; oil_filter_2: string; oil_nozzle: string; oil_nozzle_2: string; } interface ServiceParts { customer_id: number; oil_filter: string; oil_filter_2: string; oil_nozzle: string; oil_nozzle_2: string; }
export default defineComponent({ // Props and Emits
name: 'ServiceEditModal', const props = defineProps<{
props: { service: { type: Object as PropType<Partial<ServiceCall>>, required: true } }, service: Partial<ServiceCall>
data() { }>()
return {
editableService: {} as Partial<EditableService>, const emit = defineEmits<{
customer: null as Customer | null, 'close-modal': []
serviceParts: null as ServiceParts | null, 'save-changes': [service: ServiceCall]
isLoadingParts: true, 'delete-service': [serviceId: number]
serviceOptions: [ }>()
{ text: 'Tune-up', value: 0 }, { text: 'No Heat', value: 1 }, { text: 'Fix', value: 2 },
{ text: 'Tank Install', value: 3 }, { text: 'Other', value: 4 }, // Reactive data
], const editableService = ref({} as Partial<EditableService>)
}; const customer = ref(null as Customer | null)
}, const serviceParts = ref(null as ServiceParts | null)
watch: { const isLoadingParts = ref(true)
service: { const serviceOptions = ref([
handler(newVal) { { text: 'Tune-up', value: 0 }, { text: 'No Heat', value: 1 }, { text: 'Fix', value: 2 },
if (!newVal) return; { text: 'Tank Install', value: 3 }, { text: 'Other', value: 4 },
const scheduled = dayjs(newVal.scheduled_date || new Date()); ])
this.editableService = { ...newVal, date: scheduled.format('YYYY-MM-DD'), time: scheduled.hour() };
if (newVal.customer_id) { // Watchers
this.getCustomer(newVal.customer_id); watch(() => props.service, (newVal) => {
this.getServiceParts(newVal.customer_id); if (!newVal) return;
} const scheduled = dayjs(newVal.scheduled_date || new Date());
}, editableService.value = { ...newVal, date: scheduled.format('YYYY-MM-DD'), time: scheduled.hour() };
immediate: true, if (newVal.customer_id) {
deep: true, getCustomer(newVal.customer_id);
}, getServiceParts(newVal.customer_id);
}, }
methods: { }, { immediate: true, deep: true })
getCustomer(customerId: number) {
this.customer = null; // Functions
let path = import.meta.env.VITE_BASE_URL + '/customer/' + customerId; const getCustomer = (customerId: number) => {
axios.get(path, { headers: authHeader() }) customer.value = null;
.then((response: any) => { this.customer = response.data; }) let path = import.meta.env.VITE_BASE_URL + '/customer/' + customerId;
.catch((error: any) => { console.error("Failed to fetch customer details for modal:", error); }); axios.get(path, { headers: authHeader() })
}, .then((response: any) => { customer.value = response.data; })
getServiceParts(customerId: number) { .catch((error: any) => { console.error("Failed to fetch customer details for modal:", error); });
this.isLoadingParts = true; }
this.serviceParts = null;
let path = `${import.meta.env.VITE_BASE_URL}/service/parts/customer/${customerId}`; const getServiceParts = (customerId: number) => {
axios.get(path, { headers: authHeader() }) isLoadingParts.value = true;
.then((response: any) => { this.serviceParts = response.data; }) serviceParts.value = null;
.catch((error: any) => { console.error("Failed to fetch service parts:", error); }) let path = `${import.meta.env.VITE_BASE_URL}/service/parts/customer/${customerId}`;
.finally(() => { this.isLoadingParts = false; }); axios.get(path, { headers: authHeader() })
}, .then((response: any) => { serviceParts.value = response.data; })
async saveChanges() { .catch((error: any) => { console.error("Failed to fetch service parts:", error); })
const date = this.editableService.date; .finally(() => { isLoadingParts.value = false; });
const time = this.editableService.time || 0; }
const combinedDateTime = dayjs(`${date} ${time}:00`).format('YYYY-MM-DDTHH:mm:ss');
const finalPayload = { ...this.service, ...this.editableService, scheduled_date: combinedDateTime }; const saveChanges = async () => {
const date = editableService.value.date;
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${finalPayload.id}`; const time = editableService.value.time || 0;
try { const combinedDateTime = dayjs(`${date} ${time}:00`).format('YYYY-MM-DDTHH:mm:ss');
await axios.put(path, finalPayload, { headers: authHeader(), withCredentials: true }); const finalPayload = { ...props.service, ...editableService.value, scheduled_date: combinedDateTime };
this.$emit('save-changes', finalPayload);
} catch (error) { const path = `${import.meta.env.VITE_BASE_URL}/service/update/${finalPayload.id}`;
console.error("Failed to save changes:", error); try {
alert("An error occurred while saving. Please check the console."); await axios.put(path, finalPayload, { headers: authHeader(), withCredentials: true });
} emit('save-changes', finalPayload as ServiceCall);
}, } catch (error) {
confirmDelete() { console.error("Failed to save changes:", error);
if (this.service.id && window.confirm(`Are you sure you want to delete this service call?`)) { alert("An error occurred while saving. Please check the console.");
this.$emit('delete-service', this.service.id); }
} }
},
getServiceTypeName(typeId: number | undefined | null): string { const confirmDelete = () => {
if (typeId === undefined || typeId === null) return 'Unknown'; if (props.service.id && window.confirm(`Are you sure you want to delete this service call?`)) {
const typeMap: { [key: number]: string } = { 0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other' }; emit('delete-service', props.service.id);
return typeMap[typeId] || 'Unknown'; }
}, }
getServiceTypeColor(typeId: number | undefined | null): string {
if (typeId === undefined || typeId === null) return 'gray'; const getServiceTypeName = (typeId: number | undefined | null): string => {
const colorMap: { [key: number]: string } = { 0: 'blue', 1: 'red', 2: 'green', 3: '#B58900', 4: 'black' }; if (typeId === undefined || typeId === null) return 'Unknown';
return colorMap[typeId] || 'gray'; const typeMap: { [key: number]: string } = { 0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other' };
}, return typeMap[typeId] || 'Unknown';
getStateAbbrev(stateId: number | undefined | null): string { }
if (stateId === undefined || stateId === null) return 'Unknown';
const stateMap: { [key: number]: string } = { 0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY' }; const getServiceTypeColor = (typeId: number | undefined | null): string => {
return stateMap[stateId] || 'Unknown'; if (typeId === undefined || typeId === null) return 'gray';
} const colorMap: { [key: number]: string } = { 0: 'blue', 1: 'red', 2: 'green', 3: '#B58900', 4: 'black' };
}, return colorMap[typeId] || 'gray';
}); }
const getStateAbbrev = (stateId: number | undefined | null): string => {
if (stateId === undefined || stateId === null) return 'Unknown';
const stateMap: { [key: number]: string } = { 0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY' };
return stateMap[stateId] || 'Unknown';
}
</script> </script>

View File

@@ -156,187 +156,176 @@
@delete-service="handleDeleteService" @delete-service="handleDeleteService"
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue' import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface ServiceCall { // Reactive data
id: number; const user = ref(null)
scheduled_date: string; const services = ref<ServiceCall[]>([])
customer_id: number; const isLoading = ref(true)
customer_name: string; const selectedServiceForEdit = ref<ServiceCall | null>(null)
customer_address: string; // --- ADDITIONS FOR TRUNCATION ---
customer_town: string; const wordLimit = ref(50)
type_service_call: number; const expandedIds = ref<number[]>([])
description: string;
service_cost: string; // Lifecycle
payment_status?: number; onMounted(() => {
userStatus();
fetchUpcomingServices();
})
// Functions
const fetchUpcomingServices = async (): Promise<void> => {
isLoading.value = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/upcoming';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
services.value = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch upcoming service calls:", error);
} finally {
isLoading.value = false;
}
} }
export default defineComponent({ const userStatus = () => {
name: 'ServiceHome', let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
components: { Footer, ServiceEditModal }, axios({
data() { method: 'get',
return { url: path,
user: null, withCredentials: true,
services: [] as ServiceCall[], headers: authHeader(),
isLoading: true, })
selectedServiceForEdit: null as ServiceCall | null, .then((response: any) => {
// --- ADDITIONS FOR TRUNCATION --- if (response.data.ok) {
wordLimit: 50, user.value = response.data.user;
expandedIds: [] as number[], }
})
.catch(() => {
user.value = null
})
}
const openEditModal = (service: ServiceCall) => {
selectedServiceForEdit.value = service;
}
const closeEditModal = () => {
selectedServiceForEdit.value = null;
}
const isLongDescription = (text: string): boolean => {
if (!text) return false;
return text.split(/\s+/).length > wordLimit.value;
}
const truncateDescription = (text: string): string => {
if (!isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, wordLimit.value).join(' ') + '...';
}
const isExpanded = (id: number): boolean => {
return expandedIds.value.includes(id);
}
const toggleExpand = (id: number): void => {
const index = expandedIds.value.indexOf(id);
if (index === -1) {
expandedIds.value.push(id);
} else {
expandedIds.value.splice(index, 1);
}
}
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
const index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
services.value[index] = response.data.service;
}
closeEditModal();
} }
}, } catch (error) {
created() { console.error("Failed to save changes:", error);
this.userStatus(); alert("An error occurred while saving. Please check the console.");
this.fetchUpcomingServices(); }
}, }
methods: {
async fetchUpcomingServices(): Promise<void> {
this.isLoading = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/upcoming';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
this.services = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch upcoming service calls:", error);
} finally {
this.isLoading = false;
}
},
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;
}
})
.catch(() => {
this.user = null
})
},
openEditModal(service: ServiceCall) { const handleDeleteService = async (serviceId: number) => {
this.selectedServiceForEdit = service; try {
}, const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
closeEditModal() { if (response.data.ok) {
this.selectedServiceForEdit = null; services.value = services.value.filter(s => s.id !== serviceId);
}, closeEditModal();
isLongDescription(text: string): boolean {
if (!text) return false;
return text.split(/\s+/).length > this.wordLimit;
},
truncateDescription(text: string): string {
if (!this.isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, this.wordLimit).join(' ') + '...';
},
isExpanded(id: number): boolean {
return this.expandedIds.includes(id);
},
toggleExpand(id: number): void {
const index = this.expandedIds.indexOf(id);
if (index === -1) {
this.expandedIds.push(id);
} else {
this.expandedIds.splice(index, 1);
}
},
async handleSaveChanges(updatedService: ServiceCall) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
const index = this.services.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
this.services[index] = response.data.service;
}
this.closeEditModal();
}
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
},
async handleDeleteService(serviceId: number) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
this.services = this.services.filter(s => s.id !== serviceId);
this.closeEditModal();
}
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
},
formatDate(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
},
formatTime(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
},
getServiceTypeName(typeId: number): string {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
},
// --- ADD THIS METHOD ---
formatCurrency(value: string | number): string {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
},
getServiceTypeColor(typeId: number): string {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
},
shouldShowChargeButton(service: any): boolean {
return service.payment_status === null || service.payment_status === undefined;
},
shouldShowCaptureButton(service: any): boolean {
return service.payment_status === 1;
} }
}, } catch (error) {
}) console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
}
const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
}
const formatTime = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
}
const getServiceTypeName = (typeId: number): string => {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
}
const formatCurrency = (value: string | number): string => {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
}
const getServiceTypeColor = (typeId: number): string => {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
}
const shouldShowChargeButton = (service: any): boolean => {
return service.payment_status === null || service.payment_status === undefined;
}
const shouldShowCaptureButton = (service: any): boolean => {
return service.payment_status === 1;
}
</script> </script>

View File

@@ -163,193 +163,176 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue' import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface ServiceCall { // Reactive data
id: number; const user = ref(null)
scheduled_date: string; const services = ref<ServiceCall[]>([])
customer_id: number; const isLoading = ref(true)
customer_name: string; const selectedServiceForEdit = ref<ServiceCall | null>(null)
customer_address: string; // --- ADDITIONS FOR TRUNCATION ---
customer_town: string; const wordLimit = ref(50)
type_service_call: number; const expandedIds = ref<number[]>([])
description: string;
service_cost: string; // Lifecycle
payment_status?: number; onMounted(() => {
userStatus();
fetchPastServices();
})
// Functions
const isLongDescription = (text: string): boolean => {
if (!text) return false;
return text.split(/\s+/).length > wordLimit.value;
} }
export default defineComponent({ const truncateDescription = (text: string): string => {
name: 'ServiceHPast', if (!isLongDescription(text)) return text;
components: { Footer, ServiceEditModal }, const words = text.split(/\s+/);
data() { return words.slice(0, wordLimit.value).join(' ') + '...';
return { }
user: null,
services: [] as ServiceCall[], const isExpanded = (id: number): boolean => {
isLoading: true, return expandedIds.value.includes(id);
selectedServiceForEdit: null as ServiceCall | null, }
// --- ADDITIONS FOR TRUNCATION ---
wordLimit: 50, const toggleExpand = (id: number): void => {
expandedIds: [] as number[], const index = expandedIds.value.indexOf(id);
if (index === -1) {
expandedIds.value.push(id);
} else {
expandedIds.value.splice(index, 1);
}
}
const fetchPastServices = async (): Promise<void> => {
isLoading.value = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/past';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
services.value = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch past service calls:", error);
} finally {
isLoading.value = false;
}
}
const 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) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const openEditModal = (service: ServiceCall) => {
selectedServiceForEdit.value = service;
}
const closeEditModal = () => {
selectedServiceForEdit.value = null;
}
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
const index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
services.value[index] = response.data.service;
}
closeEditModal();
} }
}, } catch (error) {
created() { console.error("Failed to save changes:", error);
this.userStatus(); alert("An error occurred while saving. Please check the console.");
this.fetchPastServices(); }
}, }
methods: {
// --- NEW METHODS FOR TRUNCATION ---
isLongDescription(text: string): boolean {
if (!text) return false;
return text.split(/\s+/).length > this.wordLimit;
},
truncateDescription(text: string): string {
if (!this.isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, this.wordLimit).join(' ') + '...';
},
isExpanded(id: number): boolean {
return this.expandedIds.includes(id);
},
toggleExpand(id: number): void {
const index = this.expandedIds.indexOf(id);
if (index === -1) {
this.expandedIds.push(id);
} else {
this.expandedIds.splice(index, 1);
}
},
// --- API and Data Handling Methods --- const handleDeleteService = async (serviceId: number) => {
async fetchPastServices(): Promise<void> { try {
this.isLoading = true; const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
try { const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
const path = import.meta.env.VITE_BASE_URL + '/service/past'; if (response.data.ok) {
const response = await axios.get(path, { services.value = services.value.filter(s => s.id !== serviceId);
headers: authHeader(), closeEditModal();
withCredentials: true,
});
this.services = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch past service calls:", error);
} finally {
this.isLoading = false;
}
},
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;
}
})
.catch(() => {
this.user = null
})
},
openEditModal(service: ServiceCall) {
this.selectedServiceForEdit = service;
},
closeEditModal() {
this.selectedServiceForEdit = null;
},
async handleSaveChanges(updatedService: ServiceCall) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
const index = this.services.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
this.services[index] = response.data.service;
}
this.closeEditModal();
}
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
},
async handleDeleteService(serviceId: number) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
this.services = this.services.filter(s => s.id !== serviceId);
this.closeEditModal();
}
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
},
// --- Formatting and Display Methods ---
formatCurrency(value: string | number): string {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
},
formatDate(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
},
formatTime(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
},
getServiceTypeName(typeId: number): string {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank_Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
},
getServiceTypeColor(typeId: number): string {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
},
shouldShowChargeButton(service: any): boolean {
return service.payment_status === null || service.payment_status === undefined;
},
shouldShowCaptureButton(service: any): boolean {
return service.payment_status === 1;
} }
}, } catch (error) {
}) console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
}
const formatCurrency = (value: string | number): string => {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
}
const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
}
const formatTime = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
}
const getServiceTypeName = (typeId: number): string => {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank_Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
}
const getServiceTypeColor = (typeId: number): string => {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
}
const shouldShowChargeButton = (service: any): boolean => {
return service.payment_status === null || service.payment_status === undefined;
}
const shouldShowCaptureButton = (service: any): boolean => {
return service.payment_status === 1;
}
</script> </script>

View File

@@ -123,124 +123,109 @@
<Footer /> <Footer />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { ServicePlan } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../layouts/footers/footer.vue'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface ServicePlan { // Reactive data
id: number; const user = ref(null)
customer_id: number; const servicePlans = ref<ServicePlan[]>([])
customer_name: string; const isLoading = ref(true)
customer_address: string;
customer_town: string; // Lifecycle
contract_plan: number; onMounted(() => {
contract_years: number; userStatus();
contract_start_date: string; fetchServicePlans();
})
// Functions
const fetchServicePlans = async (): Promise<void> => {
isLoading.value = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/plans/active';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
servicePlans.value = response.data;
} catch (error) {
console.error("Failed to fetch service plans:", error);
} finally {
isLoading.value = false;
}
} }
export default defineComponent({ const userStatus = () => {
name: 'ServicePlans', let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
components: { Footer }, axios({
data() { method: 'get',
return { url: path,
user: null, withCredentials: true,
servicePlans: [] as ServicePlan[], headers: authHeader(),
isLoading: true, })
} .then((response: any) => {
}, if (response.data.ok) {
created() { user.value = response.data.user;
this.userStatus();
this.fetchServicePlans();
},
methods: {
async fetchServicePlans(): Promise<void> {
this.isLoading = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/plans/active';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
this.servicePlans = response.data;
} catch (error) {
console.error("Failed to fetch service plans:", error);
} finally {
this.isLoading = false;
} }
}, })
.catch(() => {
user.value = null
})
}
userStatus() { const getPlanName = (planType: number): string => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami'; const planNames: { [key: number]: string } = {
axios({ 1: 'Standard',
method: 'get', 2: 'Premium'
url: path, };
withCredentials: true, return planNames[planType] || 'Unknown';
headers: authHeader(), }
})
.then((response: any) => {
if (response.data.ok) {
this.user = response.data.user;
}
})
.catch(() => {
this.user = null
})
},
getPlanName(planType: number): string { const getPlanColor = (planType: number): string => {
const planNames: { [key: number]: string } = { const planColors: { [key: number]: string } = {
1: 'Standard', 1: 'blue',
2: 'Premium' 2: 'gold'
}; };
return planNames[planType] || 'Unknown'; return planColors[planType] || 'gray';
}, }
getPlanColor(planType: number): string { const formatDate = (dateString: string): string => {
const planColors: { [key: number]: string } = { if (!dateString) return 'N/A';
1: 'blue', return dayjs(dateString).format('MMM D, YYYY');
2: 'gold' }
};
return planColors[planType] || 'gray';
},
formatDate(dateString: string): string { const formatEndDate = (startDate: string, years: number): string => {
if (!dateString) return 'N/A'; if (!startDate) return 'N/A';
return dayjs(dateString).format('MMM D, YYYY'); return dayjs(startDate).add(years, 'year').format('MMM D, YYYY');
}, }
formatEndDate(startDate: string, years: number): string { const getStatusText = (startDate: string, years: number): string => {
if (!startDate) return 'N/A'; if (!startDate) return 'Unknown';
return dayjs(startDate).add(years, 'year').format('MMM D, YYYY'); const endDate = dayjs(startDate).add(years, 'year');
}, const now = dayjs();
if (now.isAfter(endDate)) {
return 'Expired';
} else if (now.isAfter(endDate.subtract(30, 'day'))) {
return 'Expiring Soon';
} else {
return 'Active';
}
}
getStatusText(startDate: string, years: number): string { const getStatusBadge = (startDate: string, years: number): string => {
if (!startDate) return 'Unknown'; if (!startDate) return 'badge-ghost';
const endDate = dayjs(startDate).add(years, 'year'); const endDate = dayjs(startDate).add(years, 'year');
const now = dayjs(); const now = dayjs();
if (now.isAfter(endDate)) { if (now.isAfter(endDate)) {
return 'Expired'; return 'badge-error';
} else if (now.isAfter(endDate.subtract(30, 'day'))) { } else if (now.isAfter(endDate.subtract(30, 'day'))) {
return 'Expiring Soon'; return 'badge-warning';
} else { } else {
return 'Active'; return 'badge-success';
} }
}, }
getStatusBadge(startDate: string, years: number): string {
if (!startDate) return 'badge-ghost';
const endDate = dayjs(startDate).add(years, 'year');
const now = dayjs();
if (now.isAfter(endDate)) {
return 'badge-error';
} else if (now.isAfter(endDate.subtract(30, 'day'))) {
return 'badge-warning';
} else {
return 'badge-success';
}
}
},
})
</script> </script>

View File

@@ -163,193 +163,176 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { ref, onMounted } from 'vue'
import axios from 'axios' import axios from 'axios'
import authHeader from '../../services/auth.header' import authHeader from '../../services/auth.header'
import { ServiceCall } from '../../types/models'
import Footer from '../../layouts/footers/footer.vue' import Footer from '../../layouts/footers/footer.vue'
import ServiceEditModal from './ServiceEditModal.vue' import ServiceEditModal from './ServiceEditModal.vue'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface ServiceCall { // Reactive data
id: number; const user = ref(null)
scheduled_date: string; const services = ref<ServiceCall[]>([])
customer_id: number; const isLoading = ref(true)
customer_name: string; const selectedServiceForEdit = ref<ServiceCall | null>(null)
customer_address: string; // --- ADDITIONS FOR TRUNCATION ---
customer_town: string; const wordLimit = ref(50)
type_service_call: number; const expandedIds = ref<number[]>([])
description: string;
service_cost: string; // Lifecycle
payment_status?: number; onMounted(() => {
userStatus();
fetchTodayServices();
})
// Functions
const isLongDescription = (text: string): boolean => {
if (!text) return false;
return text.split(/\s+/).length > wordLimit.value;
} }
export default defineComponent({ const truncateDescription = (text: string): string => {
name: 'ServiceToday', if (!isLongDescription(text)) return text;
components: { Footer, ServiceEditModal }, const words = text.split(/\s+/);
data() { return words.slice(0, wordLimit.value).join(' ') + '...';
return { }
user: null,
services: [] as ServiceCall[], const isExpanded = (id: number): boolean => {
isLoading: true, return expandedIds.value.includes(id);
selectedServiceForEdit: null as ServiceCall | null, }
// --- ADDITIONS FOR TRUNCATION ---
wordLimit: 50, const toggleExpand = (id: number): void => {
expandedIds: [] as number[], const index = expandedIds.value.indexOf(id);
if (index === -1) {
expandedIds.value.push(id);
} else {
expandedIds.value.splice(index, 1);
}
}
const fetchTodayServices = async (): Promise<void> => {
isLoading.value = true;
try {
const path = import.meta.env.VITE_BASE_URL + '/service/today';
const response = await axios.get(path, {
headers: authHeader(),
withCredentials: true,
});
services.value = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch today's service calls:", error);
} finally {
isLoading.value = false;
}
}
const 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) {
user.value = response.data.user;
}
})
.catch(() => {
user.value = null
})
}
const openEditModal = (service: ServiceCall) => {
selectedServiceForEdit.value = service;
}
const closeEditModal = () => {
selectedServiceForEdit.value = null;
}
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
const index = services.value.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
services.value[index] = response.data.service;
}
closeEditModal();
} }
}, } catch (error) {
created() { console.error("Failed to save changes:", error);
this.userStatus(); alert("An error occurred while saving. Please check the console.");
this.fetchTodayServices(); }
}, }
methods: {
// --- NEW METHODS FOR TRUNCATION ---
isLongDescription(text: string): boolean {
if (!text) return false;
return text.split(/\s+/).length > this.wordLimit;
},
truncateDescription(text: string): string {
if (!this.isLongDescription(text)) return text;
const words = text.split(/\s+/);
return words.slice(0, this.wordLimit).join(' ') + '...';
},
isExpanded(id: number): boolean {
return this.expandedIds.includes(id);
},
toggleExpand(id: number): void {
const index = this.expandedIds.indexOf(id);
if (index === -1) {
this.expandedIds.push(id);
} else {
this.expandedIds.splice(index, 1);
}
},
// --- API and Data Handling Methods --- const handleDeleteService = async (serviceId: number) => {
async fetchTodayServices(): Promise<void> { try {
this.isLoading = true; const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
try { const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
const path = import.meta.env.VITE_BASE_URL + '/service/today'; if (response.data.ok) {
const response = await axios.get(path, { services.value = services.value.filter(s => s.id !== serviceId);
headers: authHeader(), closeEditModal();
withCredentials: true,
});
this.services = response.data.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
} catch (error) {
console.error("Failed to fetch today's service calls:", error);
} finally {
this.isLoading = false;
}
},
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;
}
})
.catch(() => {
this.user = null
})
},
openEditModal(service: ServiceCall) {
this.selectedServiceForEdit = service;
},
closeEditModal() {
this.selectedServiceForEdit = null;
},
async handleSaveChanges(updatedService: ServiceCall) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
const index = this.services.findIndex(s => s.id === updatedService.id);
if (index !== -1) {
this.services[index] = response.data.service;
}
this.closeEditModal();
}
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
},
async handleDeleteService(serviceId: number) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
if (response.data.ok) {
this.services = this.services.filter(s => s.id !== serviceId);
this.closeEditModal();
}
} catch (error) {
console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
},
// --- Formatting and Display Methods ---
formatCurrency(value: string | number): string {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
},
formatDate(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
},
formatTime(dateString: string): string {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
},
getServiceTypeName(typeId: number): string {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank_Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
},
getServiceTypeColor(typeId: number): string {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
},
shouldShowChargeButton(service: any): boolean {
return service.payment_status === null || service.payment_status === undefined;
},
shouldShowCaptureButton(service: any): boolean {
return service.payment_status === 1;
} }
}, } catch (error) {
}) console.error("Failed to delete service call:", error);
alert("An error occurred while deleting. Please check the console.");
}
}
const formatCurrency = (value: string | number): string => {
if (value === null || value === undefined || value === '') return '$0.00';
const numberValue = Number(value);
if (isNaN(numberValue)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(numberValue);
}
const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('MMMM D, YYYY');
}
const formatTime = (dateString: string): string => {
if (!dateString) return 'N/A';
return dayjs(dateString).format('h:mm A');
}
const getServiceTypeName = (typeId: number): string => {
const typeMap: { [key: number]: string } = {
0: 'Tune-up',
1: 'No Heat',
2: 'Fix',
3: 'Tank_Install',
4: 'Other',
};
return typeMap[typeId] || 'Unknown Service';
}
const getServiceTypeColor = (typeId: number): string => {
const colorMap: { [key: number]: string } = {
0: 'blue',
1: 'red',
2: 'green',
3: '#B58900',
4: 'black',
};
return colorMap[typeId] || 'gray';
}
const shouldShowChargeButton = (service: any): boolean => {
return service.payment_status === null || service.payment_status === undefined;
}
const shouldShowCaptureButton = (service: any): boolean => {
return service.payment_status === 1;
}
</script> </script>

View File

@@ -55,156 +55,152 @@
/> />
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { ref, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import Header from '../../../layouts/headers/headerauth.vue'; import Header from '../../../layouts/headers/headerauth.vue';
import FullCalendar from '@fullcalendar/vue3'; import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid'; import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
import { CalendarOptions, EventClickArg } from '@fullcalendar/core'; import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
import EventSidebar from './EventSidebar.vue'; import EventSidebar from './EventSidebar.vue';
import ServiceEditModal from '../../service/ServiceEditModal.vue'; import ServiceEditModal from '../../service/ServiceEditModal.vue';
import axios from 'axios'; import axios from 'axios';
import authHeader from '../../../services/auth.header'; import authHeader from '../../../services/auth.header';
interface ServiceCall { id: number; scheduled_date: string; customer_id: number; customer_name: string; customer_address: string; customer_town: string; type_service_call: number; description: string; service_cost: string;} interface ServiceCall { id: number; scheduled_date: string; customer_id: number; customer_name: string; customer_address: string; customer_town: string; type_service_call: number; description: string; service_cost: string;}
interface Customer { id: number; customer_last_name: string; customer_first_name: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; customer_address: string; customer_home_type: number; customer_apt: string; } interface Customer { id: number; customer_last_name: string; customer_first_name: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; customer_address: string; customer_home_type: number; customer_apt: string; }
export default defineComponent({ // Route
name: 'CalendarCustomer', const route = useRoute();
components: { Header, FullCalendar, EventSidebar, ServiceEditModal },
data() {
return {
isLoading: false,
selectedServiceForEdit: null as Partial<ServiceCall> | null,
calendarOptions: {
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
weekends: true,
events: [] as any[],
eventClick: this.handleEventClick,
} as CalendarOptions,
customer: null as Customer | null,
};
},
watch: {
'$route.params.id': {
handler(newId) {
if (newId) this.getCustomer(newId as string);
},
immediate: true,
},
},
created() {
this.fetchEvents();
},
methods: {
// --- THIS IS THE FIX ---
// The logic from ServiceCalendar.vue is now correctly applied here.
handleEventClick(clickInfo: EventClickArg): void {
const events = (this.calendarOptions.events as any[]) || [];
const originalEvent = events.find(e => e.id == clickInfo.event.id);
if (originalEvent) { // Functions declared first (needed for calendarOptions)
// We "flatten" the nested object from the calendar into the simple, const handleEventClick = (clickInfo: EventClickArg): void => {
// flat structure that the modal expects, ensuring customer_id is included. const events = (calendarOptions.value.events as any[]) || [];
this.selectedServiceForEdit = { const originalEvent = events.find(e => e.id == clickInfo.event.id);
id: originalEvent.id,
scheduled_date: originalEvent.start,
customer_id: originalEvent.customer_id, // This was the missing piece
customer_name: originalEvent.title.split(': ')[1] || 'Unknown Customer',
type_service_call: originalEvent.extendedProps.type_service_call,
description: originalEvent.extendedProps.description,
service_cost: originalEvent.extendedProps.description,
customer_address: '',
customer_town: '',
};
}
},
closeEditModal() { if (originalEvent) {
this.selectedServiceForEdit = null; // We "flatten" the nested object from the calendar into the simple,
}, // flat structure that the modal expects, ensuring customer_id is included.
selectedServiceForEdit.value = {
id: originalEvent.id,
scheduled_date: originalEvent.start,
customer_id: originalEvent.customer_id, // This was the missing piece
customer_name: originalEvent.title.split(': ')[1] || 'Unknown Customer',
type_service_call: originalEvent.extendedProps.type_service_call,
description: originalEvent.extendedProps.description,
service_cost: originalEvent.extendedProps.description,
customer_address: '',
customer_town: '',
};
}
};
async handleSaveChanges(updatedService: ServiceCall) { // Reactive data
try { const isLoading = ref(false);
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`; const selectedServiceForEdit = ref<Partial<ServiceCall> | null>(null);
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true }); const calendarOptions = ref<CalendarOptions>({
await this.fetchEvents(); plugins: [dayGridPlugin, interactionPlugin],
this.closeEditModal(); initialView: 'dayGridMonth',
} catch (error) { weekends: true,
console.error("Failed to save changes:", error); events: [] as any[],
alert("An error occurred while saving. Please check the console."); eventClick: handleEventClick,
}
},
async handleDeleteService(serviceId: number) {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { withCredentials: true, headers: authHeader() });
if (response.data.ok === true) {
await this.fetchEvents();
this.closeEditModal();
} else {
console.error("Failed to delete event:", response.data.error);
}
} catch (error) {
console.error("Error deleting event:", error);
}
},
async getCustomer(customerId: string): Promise<void> {
this.isLoading = true;
this.customer = null;
try {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${customerId}`;
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
if (response.data && response.data.id) {
this.customer = response.data;
}
} catch (error) {
console.error("API call to get customer FAILED:", error);
} finally {
this.isLoading = false;
}
},
async fetchEvents(): Promise<void> {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/all`;
const response = await axios.get(path, { headers: authHeader(), withCredentials: true });
this.calendarOptions.events = response.data;
} catch (error) {
console.error("Error fetching all calendar events:", error);
}
},
async handleEventScheduled(eventData: any): Promise<void> {
if (!this.customer) {
alert("Error: A customer must be loaded in the sidebar to create a new event.");
return;
}
try {
const payload = {
expected_delivery_date: eventData.start, type_service_call: eventData.type_service_call,
customer_id: this.customer.id, description: eventData.extendedProps.description,
};
const path = import.meta.env.VITE_BASE_URL + "/service/create";
const response = await axios.post(path, payload, { withCredentials: true, headers: authHeader() });
if (response.data.ok === true) {
await this.fetchEvents();
} else {
console.error("Failed to create event:", response.data.error);
}
} catch (error) {
console.error("Error creating event:", error);
}
},
async handleEventDelete(eventId: string): Promise<void> {
// This is a simple alias now, as handleDeleteService is more specific
await this.handleDeleteService(Number(eventId));
},
},
}); });
const customer = ref<Customer | null>(null);
// Watchers
watch(() => route.params.id, (newId) => {
if (newId) getCustomer(newId as string);
}, { immediate: true });
// Lifecycle
onMounted(() => {
fetchEvents();
});
// Functions
const closeEditModal = () => {
selectedServiceForEdit.value = null;
};
const handleSaveChanges = async (updatedService: ServiceCall) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
await fetchEvents();
closeEditModal();
} catch (error) {
console.error("Failed to save changes:", error);
alert("An error occurred while saving. Please check the console.");
}
};
const handleDeleteService = async (serviceId: number) => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
const response = await axios.delete(path, { withCredentials: true, headers: authHeader() });
if (response.data.ok === true) {
await fetchEvents();
closeEditModal();
} else {
console.error("Failed to delete event:", response.data.error);
}
} catch (error) {
console.error("Error deleting event:", error);
}
};
const getCustomer = async (customerId: string): Promise<void> => {
isLoading.value = true;
customer.value = null;
try {
const path = `${import.meta.env.VITE_BASE_URL}/customer/${customerId}`;
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
if (response.data && response.data.id) {
customer.value = response.data;
}
} catch (error) {
console.error("API call to get customer FAILED:", error);
} finally {
isLoading.value = false;
}
};
const fetchEvents = async (): Promise<void> => {
try {
const path = `${import.meta.env.VITE_BASE_URL}/service/all`;
const response = await axios.get(path, { headers: authHeader(), withCredentials: true });
calendarOptions.value.events = response.data;
} catch (error) {
console.error("Error fetching all calendar events:", error);
}
};
const handleEventScheduled = async (eventData: any): Promise<void> => {
if (!customer.value) {
alert("Error: A customer must be loaded in the sidebar to create a new event.");
return;
}
try {
const payload = {
expected_delivery_date: eventData.start, type_service_call: eventData.type_service_call,
customer_id: customer.value.id, description: eventData.extendedProps.description,
};
const path = import.meta.env.VITE_BASE_URL + "/service/create";
const response = await axios.post(path, payload, { withCredentials: true, headers: authHeader() });
if (response.data.ok === true) {
await fetchEvents();
} else {
console.error("Failed to create event:", response.data.error);
}
} catch (error) {
console.error("Error creating event:", error);
}
};
const handleEventDelete = async (eventId: string): Promise<void> => {
// This is a simple alias now, as handleDeleteService is more specific
await handleDeleteService(Number(eventId));
};
</script> </script>

View File

@@ -77,8 +77,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, PropType } from 'vue'; import { ref, computed } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
interface Customer { interface Customer {
@@ -94,80 +94,79 @@ interface Customer {
customer_apt: string; customer_apt: string;
} }
export default defineComponent({ // Props
name: 'EventSidebar', const props = defineProps<{
props: { customer: Customer | null;
customer: { }>();
type: Object as PropType<Customer | null>,
required: true,
},
},
data() {
return {
selectedService: '' as string | number,
serviceOptions: [
{ text: 'Tune-up', value: 0 },
{ text: 'No Heat', value: 1 },
{ text: 'Fix', value: 2 },
{ text: 'Tank Install', value: 3 },
{ text: 'Other', value: 4 },
],
event: {
title: '',
description: '',
date: dayjs().format('YYYY-MM-DD'),
endDate: '',
time: 12,
},
};
},
computed: {
customerStateName(): string {
if (!this.customer) return '';
const stateMap: { [key: number]: string } = {
0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire',
3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York',
};
return stateMap[this.customer.customer_state] || 'Unknown';
},
customerHomeType(): string {
if (!this.customer) return '';
const homeTypeMap: { [key: number]: string } = {
0: 'Residential', 1: 'Apartment', 2: 'Condo', 3: 'Commercial',
4: 'Business', 5: 'Construction', 6: 'Container',
};
return homeTypeMap[this.customer.customer_home_type] || 'Unknown';
}
},
methods: {
submitEvent() {
if (!this.customer) {
alert("Cannot submit: No customer data is loaded.");
return;
}
const startDateTime = dayjs(`${this.event.date} ${this.event.time}:00`).format('YYYY-MM-DDTHH:mm:ss'); // Emits
const endDateTime = this.event.endDate ? dayjs(this.event.endDate).add(1, 'day').format('YYYY-MM-DD') : undefined; const emit = defineEmits<{
'event-scheduled': [eventData: any];
}>();
const eventPayload = { // Reactive data
title: this.event.title, const selectedService = ref<string | number>('');
start: startDateTime, const serviceOptions = ref([
type_service_call: this.selectedService, { text: 'Tune-up', value: 0 },
end: endDateTime, { text: 'No Heat', value: 1 },
extendedProps: { { text: 'Fix', value: 2 },
description: this.event.description, { text: 'Tank Install', value: 3 },
}, { text: 'Other', value: 4 },
}; ]);
const event = ref({
this.$emit('event-scheduled', eventPayload); title: '',
description: '',
this.event.title = ''; date: dayjs().format('YYYY-MM-DD'),
this.selectedService = ''; endDate: '',
this.event.description = ''; time: 12,
this.event.endDate = '';
this.event.date = dayjs().format('YYYY-MM-DD');
this.event.time = 12;
},
},
}); });
// Computed properties
const customerStateName = computed((): string => {
if (!props.customer) return '';
const stateMap: { [key: number]: string } = {
0: 'Massachusetts', 1: 'Rhode Island', 2: 'New Hampshire',
3: 'Maine', 4: 'Vermont', 5: 'Connecticut', 6: 'New York',
};
return stateMap[props.customer.customer_state] || 'Unknown';
});
const customerHomeType = computed((): string => {
if (!props.customer) return '';
const homeTypeMap: { [key: number]: string } = {
0: 'Residential', 1: 'Apartment', 2: 'Condo', 3: 'Commercial',
4: 'Business', 5: 'Construction', 6: 'Container',
};
return homeTypeMap[props.customer.customer_home_type] || 'Unknown';
});
// Functions
const submitEvent = () => {
if (!props.customer) {
alert("Cannot submit: No customer data is loaded.");
return;
}
const startDateTime = dayjs(`${event.value.date} ${event.value.time}:00`).format('YYYY-MM-DDTHH:mm:ss');
const endDateTime = event.value.endDate ? dayjs(event.value.endDate).add(1, 'day').format('YYYY-MM-DD') : undefined;
const eventPayload = {
title: event.value.title,
start: startDateTime,
type_service_call: selectedService.value,
end: endDateTime,
extendedProps: {
description: event.value.description,
},
};
emit('event-scheduled', eventPayload);
event.value.title = '';
selectedService.value = '';
event.value.description = '';
event.value.endDate = '';
event.value.date = dayjs().format('YYYY-MM-DD');
event.value.time = 12;
};
</script> </script>

View File

@@ -1,10 +1,10 @@
// Import the new component at the top // Import the new component at the top
import ServiceHome from './ServiceHome.vue' const ServiceHome = () => import('./ServiceHome.vue')
import ServicePast from './ServicePast.vue' const ServicePast = () => import('./ServicePast.vue')
import CalendarCustomer from './calender/CalendarCustomer.vue' const CalendarCustomer = () => import('./calender/CalendarCustomer.vue')
import ServiceCalendar from './ServiceCalendar.vue' const ServiceCalendar = () => import('./ServiceCalendar.vue')
import ServiceToday from './ServiceToday.vue' const ServiceToday = () => import('./ServiceToday.vue')
import ServicePlans from './ServicePlans.vue' const ServicePlans = () => import('./ServicePlans.vue')
const serviceRoutes = [ const serviceRoutes = [
{ {

View File

@@ -1,8 +1,8 @@
import Ticket from "../ticket/ticket.vue"; const Ticket = () => import("../ticket/ticket.vue");
import TicketAuto from "../ticket/ticketauto.vue"; const TicketAuto = () => import("../ticket/ticketauto.vue");
const ticketRoutes = [ const ticketRoutes = [

View File

@@ -170,6 +170,7 @@ 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 {AuthorizeTransaction} from '../../../types/models'
export default defineComponent({ export default defineComponent({
name: 'transactionsAuthorize', name: 'transactionsAuthorize',
@@ -182,7 +183,7 @@ export default defineComponent({
data() { data() {
return { return {
transactions: [] as any[], transactions: [] as AuthorizeTransaction[],
} }
}, },
@@ -208,7 +209,7 @@ export default defineComponent({
getStatusText(status: number) { getStatusText(status: number) {
return status === 0 ? 'Approved' : 'Declined' return status === 0 ? 'Approved' : 'Declined'
}, },
getSourceText(transaction: any) { getSourceText(transaction: AuthorizeTransaction) {
if (transaction.auto_id) { if (transaction.auto_id) {
return 'Automatic' return 'Automatic'
} else if (transaction.delivery_id) { } else if (transaction.delivery_id) {
@@ -222,7 +223,7 @@ export default defineComponent({
formatDate(dateStr: string) { formatDate(dateStr: string) {
return dateStr.split('T')[0]; // YYYY-MM-DD return dateStr.split('T')[0]; // YYYY-MM-DD
}, },
getCaptureRoute(transaction: any) { getCaptureRoute(transaction: AuthorizeTransaction) {
if (transaction.service_id) { if (transaction.service_id) {
return { name: 'chargeServiceAuthorize', params: { id: transaction.service_id } }; return { name: 'chargeServiceAuthorize', params: { id: transaction.service_id } };
} else if (transaction.delivery_id) { } else if (transaction.delivery_id) {
@@ -230,7 +231,7 @@ export default defineComponent({
} }
return {}; // fallback, though condition should prevent this return {}; // fallback, though condition should prevent this
}, },
getPreauthRoute(transaction: any) { getPreauthRoute(transaction: AuthorizeTransaction) {
if (transaction.service_id) { if (transaction.service_id) {
return { name: 'chargeServiceAuthorize', params: { id: transaction.service_id } }; return { name: 'chargeServiceAuthorize', params: { id: transaction.service_id } };
} else if (transaction.delivery_id) { } else if (transaction.delivery_id) {

View File

@@ -1,4 +1,4 @@
import AuthorizePage from './authorize/index.vue'; const AuthorizePage = () => import('./authorize/index.vue');
const transactionsRoutes = [ const transactionsRoutes = [
{ {

View File

@@ -0,0 +1,139 @@
import api from './api';
export const adminService = {
// Oil pricing
getOilPricing: () =>
api.get('/admin/oil/get'),
updateOilPricing: (data: any) =>
api.post('/admin/oil/create', data),
// VoIP
getVoipRouting: () =>
api.get('/admin/voip_routing'),
// Employee management
employees: {
create: (data: any) =>
api.post('/employee/create', data),
getById: (id: number) =>
api.get(`/employee/${id}`),
getByIdAlt: (id: number) =>
api.get(`/employee/byid/${id}`),
getByUserId: (id: number) =>
api.get(`/employee/userid/${id}`),
update: (id: number, data: any) =>
api.put(`/employee/edit/${id}`, data),
getAll: (page: number = 1) =>
api.get(`/employee/all/${page}`),
getDrivers: () =>
api.get('/employee/drivers'),
},
// Promotions
promos: {
getAll: () =>
api.get('/promo/all'),
getById: (id: number) =>
api.get(`/promo/${id}`),
getPrice: (id: number) =>
api.get(`/promo/promoprice/${id}`),
create: (data: any) =>
api.post('/promo/create', data),
update: (id: number, data: any) =>
api.put(`/promo/edit/${id}`, data),
delete: (id: number) =>
api.delete(`/promo/delete/${id}`),
enable: (id: number) =>
api.put(`/promo/on/${id}`),
disable: (id: number) =>
api.put(`/promo/off/${id}`),
},
// Statistics
stats: {
deliveryCountToday: () =>
api.get('/stats/delivery/count/today'),
deliveredCountToday: () =>
api.get('/stats/delivery/count/delivered/today'),
employeeDeliveryTotal: (employeeId: number) =>
api.get(`/stats/delivery/total/${employeeId}`),
employeeGallonsTotal: (employeeId: number) =>
api.get(`/stats/gallons/total/${employeeId}`),
weeklyGallons: () =>
api.get('/stats/gallons/week'),
customerGallonsTotal: (customerId: number) =>
api.get(`/stats/gallons/check/total/${customerId}`),
employeePrimesTotal: (employeeId: number) =>
api.get(`/stats/primes/total/${employeeId}`),
userLastDelivery: (userId: number) =>
api.get(`/stats/user/lastdelivery/${userId}`),
userStats: (userId: number) =>
api.get(`/stats/user/${userId}`),
serviceCallsToday: () =>
api.get('/stats/call/count/today'),
sidebarCounts: () =>
api.get('/deliverystatus/stats/sidebar-counts'),
todayTotals: () =>
api.get('/deliverystatus/today-totals'),
tomorrowTotals: () =>
api.get('/deliverystatus/tomorrow-totals'),
waitingTotals: () =>
api.get('/deliverystatus/waiting-totals'),
pendingStatus: () =>
api.get('/deliverystatus/pending'),
},
// Money & reporting
money: {
weeklyProfit: () =>
api.get('/money/profit/week'),
yearlyProfit: () =>
api.get('/money/profit/year'),
customerListReport: () =>
api.get('/report/customers/list'),
},
// Social/comments
social: {
getPosts: (customerId: number, page: number = 1) =>
api.get(`/social/posts/${customerId}/${page}`),
createPost: (customerId: number, data: any) =>
api.post(`/social/create/${customerId}`, data),
deletePost: (postId: number) =>
api.delete(`/social/delete/${postId}`),
},
};
export default adminService;

51
src/services/api.ts Normal file
View File

@@ -0,0 +1,51 @@
import axios from 'axios';
// Main Flask API
const api = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
withCredentials: true,
});
// Authorize.net FastAPI
const authorizeApi = axios.create({
baseURL: import.meta.env.VITE_AUTHORIZE_URL,
withCredentials: true,
});
// Automatic Delivery System
const autoApi = axios.create({
baseURL: import.meta.env.VITE_AUTO_URL,
withCredentials: true,
});
// Request interceptor - add auth token
function addAuthHeader(config: { headers: { Authorization?: string } }) {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
api.interceptors.request.use(addAuthHeader as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
authorizeApi.interceptors.request.use(addAuthHeader as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
autoApi.interceptors.request.use(addAuthHeader as any);
// Response error handler - handle 401 errors
function handleResponseError(error: unknown) {
const err = error as { response?: { status?: number } };
if (err?.response?.status === 401) {
localStorage.removeItem('auth_token');
}
return Promise.reject(error);
}
api.interceptors.response.use(undefined, handleResponseError);
authorizeApi.interceptors.response.use(undefined, handleResponseError);
autoApi.interceptors.response.use(undefined, handleResponseError);
export { api, authorizeApi, autoApi };
export default api;

View File

@@ -0,0 +1,40 @@
import api, { authorizeApi } from './api';
export const authService = {
// Authentication
login: (data: { username: string; password: string }) =>
api.post('/auth/login', data),
register: (data: any) =>
api.post('/auth/register', data),
logout: () =>
api.post('/auth/logout'),
whoami: () =>
api.get('/auth/whoami'),
// Password management
changePassword: (data: { old_password: string; new_password: string }) =>
api.put('/auth/change-password', data),
adminChangePassword: (data: { user_id: number; new_password: string }) =>
api.put('/auth/admin-change-password', data),
unlockAccount: (data: any) =>
api.post('/auth/unlock-account', data),
// Authorize.net account management
authorize: {
checkAccount: (customerId: number) =>
authorizeApi.get(`/user/check-authorize-account/${customerId}`),
createAccount: (customerId: number, data?: any) =>
authorizeApi.post(`/user/create-account/${customerId}`, data),
deleteAccount: (customerId: number) =>
authorizeApi.delete(`/user/delete-account/${customerId}`),
},
};
export default authService;

View File

@@ -0,0 +1,56 @@
import api from './api';
import {
Customer,
CustomerDescription,
TankInspection,
CustomerStats,
CreateCustomerRequest,
UpdateCustomerRequest,
CustomerListResponse,
ApiResponse
} from '../types/models';
export const customerService = {
// CRUD operations
getAll: (page: number = 1): Promise<CustomerListResponse> =>
api.get(`/customer/all/${page}`),
getById: (id: number): Promise<ApiResponse<Customer>> =>
api.get(`/customer/${id}`),
create: (data: CreateCustomerRequest): Promise<ApiResponse<Customer>> =>
api.post('/customer/create', data),
update: (id: number, data: UpdateCustomerRequest): Promise<ApiResponse<Customer>> =>
api.put(`/customer/edit/${id}`, data),
delete: (id: number): Promise<ApiResponse<void>> =>
api.delete(`/customer/delete/${id}`),
getCount: (): Promise<ApiResponse<{ count: number }>> =>
api.get('/customer/count'),
// Profile & details
getDescription: (id: number): Promise<ApiResponse<CustomerDescription>> =>
api.get(`/customer/description/${id}`),
// Tank information
getTank: (id: number): Promise<ApiResponse<TankInspection>> =>
api.get(`/customer/tank/${id}`),
updateTank: (id: number, data: Partial<TankInspection>): Promise<ApiResponse<void>> =>
api.put(`/customer/edit/tank/${id}`, data),
// Automatic delivery
getAutomaticStatus: (id: number): Promise<ApiResponse<{ status: number }>> =>
api.get(`/customer/automatic/status/${id}`),
assignAutomatic: (id: number, data: { status: number }): Promise<ApiResponse<{ status: number }>> =>
api.put(`/customer/automatic/assign/${id}`, data),
// Search
search: (query: string): Promise<ApiResponse<Customer[]>> =>
api.get(`/search/customer?q=${encodeURIComponent(query)}`),
};
export default customerService;

View File

@@ -0,0 +1,128 @@
import api, { autoApi } from './api';
import {
Delivery,
DeliveryNote,
AutoDelivery,
DeliveryListResponse,
CreateDeliveryRequest,
UpdateDeliveryRequest,
ApiResponse
} from '../types/models';
export const deliveryService = {
// CRUD operations
create: (customerId: number, data: CreateDeliveryRequest): Promise<ApiResponse<Delivery>> =>
api.post(`/delivery/create/${customerId}`, data),
getById: (id: number): Promise<ApiResponse<Delivery>> =>
api.get(`/delivery/${id}`),
getOrder: (id: number): Promise<ApiResponse<Delivery>> =>
api.get(`/delivery/order/${id}`),
update: (id: number, data: UpdateDeliveryRequest): Promise<ApiResponse<Delivery>> =>
api.put(`/delivery/edit/${id}`, data),
delete: (id: number): Promise<ApiResponse<void>> =>
api.delete(`/delivery/delete/${id}`),
cancel: (id: number): Promise<ApiResponse<void>> =>
api.put(`/delivery/cancel/${id}`),
markCancelled: (id: number): Promise<ApiResponse<void>> =>
api.put(`/delivery/cancelled/${id}`),
// List operations
getAll: (page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/all/${page}`),
getByCustomer: (customerId: number, page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/customer/${customerId}/${page}`),
getPast1: (customerId: number): Promise<ApiResponse<Delivery[]>> =>
api.get(`/delivery/past1/${customerId}`),
getPast2: (customerId: number): Promise<ApiResponse<Delivery[]>> =>
api.get(`/delivery/past2/${customerId}`),
// Status-based lists
getWaiting: (page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/waiting/${page}`),
getTomorrow: (page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/tommorrow/${page}`),
getOutForDelivery: (page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/outfordelivery/${page}`),
getDelivered: (page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/delivered/${page}`),
getFinalized: (page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/finalized/${page}`),
getPending: (page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/pending/${page}`),
getIssues: (page: number = 1): Promise<DeliveryListResponse> =>
api.get(`/delivery/issue/${page}`),
// Status & totals
updateStatus: (data: { id: number; status: number }): Promise<ApiResponse<void>> =>
api.put('/delivery/updatestatus', data),
getTotal: (id: number): Promise<ApiResponse<{ total: number }>> =>
api.get(`/delivery/total/${id}`),
// Cash handling
handleCash: (id: number, type: string): Promise<ApiResponse<{ amount: number }>> =>
api.get(`/delivery/cash/${id}/${type}`),
// Finalize
finalize: (id: number, data?: { final_price: number }): Promise<ApiResponse<void>> =>
api.put(`/deliverydata/finalize/${id}`, data),
// Auto system endpoints (VITE_AUTO_URL)
auto: {
getDelivery: (id: number) =>
autoApi.get(`/delivery/delivery/${id}`),
getProfileDeliveries: (id: number) =>
autoApi.get(`/delivery/all/profile/${id}`),
getAllCustomers: () =>
autoApi.get('/delivery/all/customers'),
getByCustomer: (id: number) =>
autoApi.get(`/delivery/auto/customer/${id}`),
getTicket: (id: number) =>
autoApi.get(`/delivery/autoticket/${id}`),
findDelivery: (id: number) =>
autoApi.get(`/delivery/finddelivery/${id}`),
updateStatus: (id: number, data: any) =>
autoApi.put(`/delivery/update_status/${id}`, data),
confirm: (data: any) =>
autoApi.post('/confirm/delivery', data),
createTicket: (id: number, data: any) =>
autoApi.post(`/confirm/auto/create/${id}`, data),
createTicketNoPreauth: (id: number, data: any) =>
autoApi.post(`/confirm/auto/create/nopreauth/${id}`, data),
closeTicket: (id: number, data?: any) =>
autoApi.put(`/confirm/auto/close_ticket/${id}`, data),
updateTicket: (id: number, data: any) =>
autoApi.put(`/confirm/auto/update/${id}`, data),
estimateGallons: (customerId: number) =>
autoApi.get(`/fixstuff_customer/estimate_gallons/customer/${customerId}`),
},
};
export default deliveryService;

View File

@@ -0,0 +1,111 @@
import api, { authorizeApi } from './api';
import {
PaymentTransaction,
CreditCard,
AuthorizeTransaction,
TransactionListResponse,
CardListResponse,
CreateCardRequest,
PaymentRequest,
ApiResponse,
TokenizeCardRequest,
UpdateTokenizedCardRequest,
ChargeSavedCardRequest,
ChargeDirectRequest,
CaptureRequest,
PreauthorizeSavedCardRequest,
AuthorizeNetTransactionResponse
} from '../types/models';
export const paymentService = {
// Card management (Main API)
getCard: (id: number): Promise<ApiResponse<CreditCard>> =>
api.get(`/payment/card/${id}`),
getCards: (customerId: number): Promise<ApiResponse<CreditCard[]>> =>
api.get(`/payment/cards/${customerId}`),
getCardsOnFile: (customerId: number): Promise<ApiResponse<CreditCard[]>> =>
api.get(`/payment/cards/onfile/${customerId}`),
getAllCards: (page: number = 1): Promise<CardListResponse> =>
api.get(`/payment/cards/all/${page}`),
createCard: (customerId: number, data: CreateCardRequest): Promise<ApiResponse<CreditCard>> =>
api.post(`/payment/card/create/${customerId}`, data),
updateCard: (id: number, data: Partial<CreditCard>): Promise<ApiResponse<CreditCard>> =>
api.put(`/payment/card/edit/${id}`, data),
removeCard: (id: number): Promise<ApiResponse<void>> =>
api.delete(`/payment/card/remove/${id}`),
removeCardAlt: (id: number): Promise<ApiResponse<void>> =>
api.delete(`/payment/cards/remove/${id}`),
updatePaymentProfile: (id: number, data: { profile_id: string }): Promise<ApiResponse<void>> =>
api.put(`/payment/card/update_payment_profile/${id}`, data),
// Authorization & capture (Main API)
authorizeDelivery: (id: number, data: PaymentRequest): Promise<ApiResponse<PaymentTransaction>> =>
api.put(`/payment/authorize/${id}`, data),
authorizeService: (id: number, data: PaymentRequest): Promise<ApiResponse<PaymentTransaction>> =>
api.put(`/payment/authorize/service/${id}`, data),
cleanupAuthorization: (id: number): Promise<ApiResponse<void>> =>
api.put(`/payment/authorize/cleanup/${id}`),
captureServicePayment: (id: number, data: { amount: number }): Promise<ApiResponse<PaymentTransaction>> =>
api.put(`/payment/capture/service/${id}`, data),
// Service payment
getServicePayment: (id: number, type: string): Promise<ApiResponse<{ amount: number }>> =>
api.get(`/payment/service/payment/${id}/${type}`),
// Transactions
getDeliveryTransaction: (id: number): Promise<ApiResponse<PaymentTransaction>> =>
api.get(`/payment/transaction/delivery/${id}`),
getAuthorizeTransactions: (): Promise<ApiResponse<AuthorizeTransaction[]>> =>
api.get('/payment/transactions/authorize/1'),
getCustomerTransactions: (customerId: number, page: number = 1): Promise<TransactionListResponse> =>
api.get(`/payment/transactions/customer/${customerId}/${page}`),
getServiceTransactions: (serviceId: number): Promise<ApiResponse<PaymentTransaction[]>> =>
api.get(`/payment/transactions/service/${serviceId}`),
// Authorize.net endpoints
authorize: {
tokenizeCard: (customerId: number, data: TokenizeCardRequest): Promise<ApiResponse<CreditCard>> =>
authorizeApi.post(`/api/payments/customers/${customerId}/cards`, data),
updateTokenizedCard: (customerId: number, cardId: number, data: UpdateTokenizedCardRequest): Promise<ApiResponse<CreditCard>> =>
authorizeApi.put(`/api/payments/customers/${customerId}/cards/${cardId}`, data),
authorizeSavedCard: (customerId: number, data: PreauthorizeSavedCardRequest): Promise<ApiResponse<AuthorizeNetTransactionResponse>> =>
authorizeApi.post(`/api/payments/authorize/saved-card/${customerId}`, data),
chargeSavedCard: (customerId: number, data: ChargeSavedCardRequest): Promise<ApiResponse<AuthorizeNetTransactionResponse>> =>
authorizeApi.post(`/api/payments/charge/saved-card/${customerId}`, data),
charge: (customerId: number, data: ChargeDirectRequest): Promise<ApiResponse<AuthorizeNetTransactionResponse>> =>
authorizeApi.post(`/api/charge/${customerId}`, data),
capture: (data: CaptureRequest): Promise<ApiResponse<AuthorizeNetTransactionResponse>> =>
authorizeApi.post('/api/capture/', data),
// Auto transaction endpoints
getAutoTransaction: (deliveryId: number): Promise<ApiResponse<AuthorizeNetTransactionResponse>> =>
authorizeApi.get(`/api/auto/transaction/delivery/${deliveryId}`),
updateAutoTransactionId: (deliveryId: number, newId: number, data?: Record<string, unknown>): Promise<ApiResponse<void>> =>
authorizeApi.put(`/api/auto/transaction/delivery/${deliveryId}/update/${newId}`, data ?? {}),
linkTransactionToAuto: (transactionId: number, autoId: number, data?: Record<string, unknown>): Promise<ApiResponse<void>> =>
authorizeApi.put(`/api/transaction/${transactionId}/update_auto_id/${autoId}`, data ?? {}),
},
};
export default paymentService;

View File

@@ -0,0 +1,31 @@
import api from './api';
export const queryService = {
// Customer types
getCustomerTypes: () =>
api.get('/query/customertype'),
// States
getStates: () =>
api.get('/query/states'),
// Employee types
getEmployeeTypes: () =>
api.get('/query/employeetype'),
// Delivery statuses
getDeliveryStatuses: () =>
api.get('/query/deliverystatus'),
// Oil pricing info
getOilPrice: () =>
api.get('/info/price/oil'),
getOilPriceTable: () =>
api.get('/info/price/oil/table'),
getOilPriceTiers: () =>
api.get('/info/price/oil/tiers'),
};
export default queryService;

View File

@@ -0,0 +1,63 @@
import api from './api';
export const serviceService = {
// CRUD operations
create: (data: any) =>
api.post('/service/create', data),
getById: (id: number) =>
api.get(`/service/${id}`),
update: (id: number, data: any) =>
api.put(`/service/update/${id}`, data),
delete: (id: number) =>
api.delete(`/service/delete/${id}`),
// List operations
getAll: () =>
api.get('/service/all'),
getToday: () =>
api.get('/service/today'),
getUpcoming: () =>
api.get('/service/upcoming'),
getPast: () =>
api.get('/service/past'),
getForCustomer: (customerId: number) =>
api.get(`/service/for-customer/${customerId}`),
// Cost management
updateCost: (id: number, data: any) =>
api.put(`/service/update-cost/${id}`, data),
// Parts
getPartsForCustomer: (customerId: number) =>
api.get(`/service/parts/customer/${customerId}`),
updateParts: (id: number, data: any) =>
api.put(`/service/parts/update/${id}`, data),
// Service plans
plans: {
getActive: () =>
api.get('/service/plans/active'),
getForCustomer: (customerId: number) =>
api.get(`/service/plans/customer/${customerId}`),
create: (data: any) =>
api.post('/service/plans/create', data),
update: (id: number, data: any) =>
api.put(`/service/plans/update/${id}`, data),
delete: (id: number) =>
api.delete(`/service/plans/delete/${id}`),
},
};
export default serviceService;

View File

@@ -3,7 +3,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import axios from 'axios' import axios from 'axios'
import authHeader from '../services/auth.header' // Adjust path if needed import { authService } from '../services/authService'
interface User { interface User {
user_name: string; user_name: string;
@@ -38,7 +38,7 @@ export const useAuthStore = defineStore('auth', () => {
localStorage.setItem('auth_user', JSON.stringify(newUser)) localStorage.setItem('auth_user', JSON.stringify(newUser))
user.value = newUser user.value = newUser
} }
// --- THIS IS THE FIX --- // --- THIS IS THE FIX ---
// The string must be a template literal (using backticks ``) // The string must be a template literal (using backticks ``)
// and should follow the "Bearer <token>" format. // and should follow the "Bearer <token>" format.
@@ -60,9 +60,8 @@ export const useAuthStore = defineStore('auth', () => {
return false // No token, definitely not authenticated return false // No token, definitely not authenticated
} }
try { try {
// Use your existing endpoint to verify the token // Use the centralized auth service
const path = `${import.meta.env.VITE_BASE_URL}/auth/whoami`; const response = await authService.whoami();
const response = await axios.get(path, { headers: authHeader() });
if (response.data && response.data.ok) { if (response.data && response.data.ok) {
user.value = response.data.user user.value = response.data.user
return true return true
@@ -85,4 +84,4 @@ const response = await axios.get(path, { headers: authHeader() });
clearAuth, clearAuth,
checkAuthStatus, checkAuthStatus,
} }
}) })

View File

@@ -2,8 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import axios from 'axios' import { adminService } from '../services/adminService'
import authHeader from '../services/auth.header' // Adjust path if needed
export const useCountsStore = defineStore('counts', () => { export const useCountsStore = defineStore('counts', () => {
// --- STATE --- // --- STATE ---
@@ -16,13 +15,12 @@ export const useCountsStore = defineStore('counts', () => {
const upcoming_service = ref(0) const upcoming_service = ref(0)
const today_service = ref(0) const today_service = ref(0)
const transaction = 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.
async function fetchSidebarCounts() { async function fetchSidebarCounts() {
try { try {
const path = `${import.meta.env.VITE_BASE_URL}/deliverystatus/stats/sidebar-counts`; const response = await adminService.stats.sidebarCounts();
const response = await axios.get(path, { headers: authHeader() });
if (response.data && response.data.ok) { if (response.data && response.data.ok) {
const counts = response.data.counts; const counts = response.data.counts;

View File

@@ -2,7 +2,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import axios from 'axios' import { customerService } from '../services/customerService'
// Define a type for what a search result looks like. // Define a type for what a search result looks like.
interface CustomerSearchResult { interface CustomerSearchResult {
@@ -38,9 +38,7 @@ export const useSearchStore = defineStore('search', () => {
isLoading.value = true; isLoading.value = true;
try { try {
// NOTE: Make sure this URL is correct. You may need to add your VITE_BASE_URL const response = await customerService.search(searchTerm.value);
const path = `${import.meta.env.VITE_BASE_URL}/search/customer?q=${searchTerm.value}`;
const response = await axios.get(path);
searchResults.value = response.data; searchResults.value = response.data;
} catch { // No `error` parameter as requested } catch { // No `error` parameter as requested
searchResults.value = []; searchResults.value = [];
@@ -71,4 +69,4 @@ export const useSearchStore = defineStore('search', () => {
debouncedSearch, debouncedSearch,
clearSearch, clearSearch,
} }
}) })

725
src/types/models.ts Normal file
View File

@@ -0,0 +1,725 @@
/**
* EAMCO Office Frontend API Response Models
*
* This file contains TypeScript interfaces for all API responses
* to replace 'any' types throughout the application.
*/
import {
DeliveryStatusType,
PaymentStatusType,
AutoStatusType,
TransactionStatusType,
CustomerAutomaticStatus
} from '../constants/status';
// Base interfaces
export interface BaseEntity {
id: number;
}
export interface ApiResponse<T> {
ok: boolean;
data?: T;
error?: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
per_page: number;
}
// Customer interfaces
export interface Customer extends BaseEntity {
auth_net_profile_id?: string;
account_number: string;
customer_first_name: string;
customer_last_name: string;
customer_town: string;
customer_state: number;
customer_zip: string;
customer_first_call?: string;
customer_email?: string;
customer_automatic: CustomerAutomaticStatus;
customer_phone_number?: string;
customer_home_type: number;
customer_apt?: string;
customer_address: string;
company_id: number;
customer_latitude?: string;
customer_longitude?: string;
correct_address: boolean;
}
export interface CustomerDescription extends BaseEntity {
customer_id: number;
account_number: string;
company_id: number;
fill_location?: number;
description?: string;
}
export interface TankInspection extends BaseEntity {
customer_id: number;
last_tank_inspection?: string;
tank_status: boolean;
outside_or_inside: boolean;
tank_size: number;
tank_images?: number;
tank_image_upload_dates?: string[];
}
export interface CustomerStats extends BaseEntity {
customer_id: number;
total_calls: number;
service_calls_total: number;
service_calls_total_spent: number;
service_calls_total_profit: number;
oil_deliveries: number;
oil_total_gallons: number;
oil_total_spent: number;
oil_total_profit: number;
}
// Delivery interfaces
export interface Delivery extends BaseEntity {
customer_id: number;
customer_name: string;
customer_address: string;
customer_town: string;
customer_state: string;
customer_zip: number;
gallons_ordered: number;
customer_asked_for_fill: number;
gallons_delivered: number;
customer_filled: number;
delivery_status: DeliveryStatusType;
when_ordered?: string;
when_delivered?: string;
expected_delivery_date?: string;
automatic: number;
automatic_id?: number;
oil_id?: number;
supplier_price?: number;
customer_price?: number;
customer_temperature?: number;
dispatcher_notes?: string;
prime: number;
same_day: number;
emergency: number;
payment_type: number;
payment_card_id?: number;
cash_recieved?: number;
driver_employee_id?: number;
driver_first_name?: string;
driver_last_name?: string;
pre_charge_amount?: number;
total_price: number;
final_price: number;
check_number?: string;
promo_id?: number;
promo_money_discount?: number;
}
export interface DeliveryNote extends BaseEntity {
delivery_id: number;
driver_comments?: string;
time_added: string;
driver_id: number;
driver_name: string;
}
// Payment interfaces
export interface PaymentTransaction extends BaseEntity {
customer_id: number;
delivery_id?: number;
service_id?: number;
amount: number;
payment_status: PaymentStatusType;
transaction_id?: string;
card_id?: number;
transaction_type?: TransactionStatusType;
created_at?: string;
updated_at?: string;
}
export interface CreditCard extends BaseEntity {
user_id: number;
date_added?: string;
card_number: string;
last_four_digits: number;
name_on_card: string;
expiration_month: string;
expiration_year: string;
type_of_card: string;
security_number?: string;
accepted_or_declined?: number;
main_card: boolean;
zip_code?: string;
auth_net_payment_profile_id?: string;
}
export interface AuthorizeTransaction extends BaseEntity {
transaction_id: string;
amount: number;
status: TransactionStatusType;
response_code?: string;
auth_code?: string;
created_at: string;
auth_net_transaction_id?: string;
customer_name?: string;
preauthorize_amount: number | null;
charge_amount: number | null;
transaction_type?: number;
delivery_id?: number;
service_id?: number;
auto_id?: number;
customer_id?: number;
rejection_reason?: string;
}
// Service interfaces
export interface ServiceCall extends BaseEntity {
customer_id: number;
customer_name?: string;
customer_address?: string;
customer_town?: string;
customer_state?: string;
customer_zip?: string;
type_service_call: number;
when_ordered?: string;
scheduled_date: string;
description: string;
service_cost: string;
payment_type?: number;
payment_card_id?: number;
payment_status?: number;
}
export interface ServicePart extends BaseEntity {
service_call_id: number;
part_name: string;
part_number?: string;
quantity: number;
unit_cost: number;
total_cost: number;
}
export interface ServicePlan extends BaseEntity {
customer_id: number;
customer_name: string;
customer_address: string;
customer_town: string;
contract_plan: number;
contract_years: number;
contract_start_date: string;
}
// Employee interfaces
export interface Employee extends BaseEntity {
employee_first_name: string;
employee_last_name: string;
employee_email?: string;
employee_phone?: string;
employee_role: string;
is_active: boolean;
hire_date?: string;
admin_role: boolean;
employee_type?: number;
employee_town?: string;
employee_phone_number?: string;
user_id?: number;
}
export interface User {
user_admin?: number;
// Add other user properties as needed
}
// Auto delivery interfaces
export interface AutoDelivery extends BaseEntity {
customer_id: number;
customer_full_name: string;
account_number: string;
customer_town: string;
customer_state: number;
customer_zip: string;
customer_address: string;
last_fill?: string;
last_updated?: string;
estimated_gallons_left: number;
estimated_gallons_left_prev_day: number;
tank_height?: string;
tank_size: number;
house_factor: number;
auto_status: AutoStatusType;
days_since_last_fill: number;
hot_water_summer: number;
open_ticket_id?: number;
}
// Promo interfaces
export interface Promo extends BaseEntity {
promo_code: string;
promo_name: string;
discount_type: string;
discount_value: number;
is_active: boolean;
start_date?: string;
end_date?: string;
usage_limit?: number;
usage_count: number;
}
// Query/Lookup interfaces
export interface StateOption {
value: number;
text: string;
abbreviation?: string;
}
export interface HomeTypeOption {
value: number;
text: string;
}
export interface PaymentTypeOption {
id: number;
name: string;
}
export interface OilPrice {
id: number;
supplier_name: string;
price_per_gallon: number;
effective_date: string;
}
// Search interfaces
export interface SearchResult {
id: number;
type: 'customer' | 'delivery' | 'service';
title: string;
subtitle?: string;
data: any; // Keep as any for now since search results can be various types
}
// Stats interfaces
export interface DashboardStats {
total_customers: number;
active_deliveries: number;
pending_payments: number;
monthly_revenue: number;
todays_deliveries: number;
}
export interface CompanyStats {
total_revenue: number;
total_profit: number;
total_deliveries: number;
total_service_calls: number;
average_delivery_time: number;
}
// Request interfaces
export interface CreateCustomerRequest {
customer_last_name: string;
customer_first_name: string;
customer_town: string;
customer_state: number;
customer_zip: string;
customer_email?: string;
customer_home_type: number;
customer_phone_number?: string;
customer_address: string;
customer_apt?: string;
customer_description?: string;
}
export interface UpdateCustomerRequest {
customer_last_name?: string;
customer_first_name?: string;
customer_town?: string;
customer_state?: number;
customer_zip?: string;
customer_email?: string;
customer_home_type?: number;
customer_phone_number?: string;
customer_address?: string;
customer_apt?: string;
customer_automatic?: CustomerAutomaticStatus;
}
export interface CreateDeliveryRequest {
customer_id: number;
gallons_ordered: number;
expected_delivery_date?: string;
payment_type: number;
payment_card_id?: number;
promo_id?: number;
dispatcher_notes?: string;
}
export interface UpdateDeliveryRequest {
gallons_delivered?: number;
delivery_status?: DeliveryStatusType;
payment_type?: number;
total_price?: number;
final_price?: number;
driver_employee_id?: number;
dispatcher_notes?: string;
}
export interface CreateCardRequest {
customer_id: number;
card_number: string;
expiration_month: number;
expiration_year: number;
security_number: string;
cardholder_name: string;
}
export interface PaymentRequest {
customer_id: number;
amount: number;
card_id?: number;
delivery_id?: number;
service_id?: number;
}
// Calendar/Event interfaces
export interface CalendarEvent {
id: string;
title: string;
start: string;
end?: string;
backgroundColor?: string;
extendedProps?: {
customer_id: number;
delivery_id?: number;
service_id?: number;
type: 'delivery' | 'service' | 'maintenance';
status: string;
};
}
// Form interfaces
export interface LoginForm {
username: string;
password: string;
}
export interface RegisterForm {
username: string;
email: string;
password: string;
confirm_password: string;
}
export interface ChangePasswordForm {
current_password: string;
new_password: string;
confirm_password: string;
}
// ============================================
// Payment Request Interfaces (Authorize.net)
// ============================================
export interface TokenizeCardRequest {
card_number: string;
expiration_month: string;
expiration_year: string;
cvv: string;
cardholder_name: string;
zip_code?: string;
}
export interface UpdateTokenizedCardRequest {
card_number?: string;
expiration_month?: string;
expiration_year?: string;
cvv?: string;
cardholder_name?: string;
zip_code?: string;
}
export interface ChargeSavedCardRequest {
card_id: number;
charge_amount: string;
delivery_id?: number;
service_id?: number | null;
}
export interface PreauthorizeSavedCardRequest {
card_id: number;
preauthorize_amount: string;
delivery_id?: number;
auto_id?: number;
}
export interface ChargeDirectRequest {
card_number: string;
expiration_date: string;
cvv: string;
charge_amount: string;
preauthorize_amount?: string;
transaction_type: number;
delivery_id?: number;
service_id?: number | null;
card_id?: number;
}
export interface CaptureRequest {
charge_amount: number;
auth_net_transaction_id: string;
}
// ============================================
// API Response Interfaces
// ============================================
export interface AuthorizeNetTransactionResponse {
id: number;
auth_net_transaction_id: string;
preauthorize_amount: number | null;
charge_amount: number | null;
status: number; // 0 = approved, 1 = declined
transaction_type: number; // 0 = charge, 1 = auth, 2 = capture
created_at: string;
rejection_reason?: string;
customer_id?: number;
delivery_id?: number;
service_id?: number;
auto_id?: number;
}
export interface DeliveryOrderResponse {
ok: boolean;
delivery: Delivery;
error?: string;
}
export interface DeliveryTotalResponse {
ok: boolean;
priceprime: number;
pricesameday: number;
priceemergency: number;
total_amount: number;
discount: number;
total_amount_after_discount: number;
}
export interface OilPricingResponse {
price_from_supplier: number;
price_for_customer: number;
price_for_employee: number;
price_same_day: number;
price_prime: number;
price_emergency: number;
date: string;
}
export interface WhoAmIResponse {
ok: boolean;
user: {
user_id: number;
user_admin?: number;
};
}
export interface AuthorizeCheckResponse {
profile_exists: boolean;
has_payment_methods: boolean;
missing_components: string[];
valid_for_charging: boolean;
}
export interface CreateAuthorizeAccountResponse {
success: boolean;
profile_id?: string;
message?: string;
error_detail?: string;
is_duplicate?: boolean;
}
export interface PromoResponse {
id: number;
name_of_promotion: string;
description: string;
money_off_delivery: number;
text_on_ticket: string;
}
export interface CardsOnFileResponse {
cards: number;
}
export interface PaymentCardResponse {
userCard: CreditCard;
}
export interface UpdateStatusResponse {
update: boolean;
}
// ============================================
// Component Local State Interfaces
// ============================================
export interface DeliveryFormData {
id: number;
customer_id: number;
customer_name: string;
customer_address: string;
customer_town: string;
customer_state: number;
customer_zip: string;
gallons_ordered: number;
customer_asked_for_fill: number;
gallons_delivered: number;
customer_filled: number;
delivery_status: number;
when_ordered: string;
when_delivered: string;
expected_delivery_date: string;
automatic: number;
automatic_id?: number;
oil_id: number;
supplier_price: number;
customer_price: number;
customer_temperature: number;
dispatcher_notes: string;
prime: number;
promo_id: number | null;
emergency: number;
same_day: number;
payment_type: number;
payment_card_id: number;
driver_employee_id: number;
driver_first_name: string;
driver_last_name: string;
pre_charge_amount: number;
total_price: number;
final_price?: number;
service_id?: number | null;
}
export interface CustomerFormData {
id: number;
user_id?: number;
customer_first_name: string;
customer_last_name: string;
customer_town: string;
customer_address: string;
customer_state: number;
customer_zip: string;
customer_apt: string;
customer_home_type: number;
customer_phone_number: string;
account_number: string;
auth_net_profile_id?: string | null;
customer_email?: string;
}
export interface CreditCardFormData {
id: number;
name_on_card: string;
main_card: boolean;
card_number: string;
expiration_month: string;
type_of_card: string;
last_four_digits: string | number;
expiration_year: string;
security_number: string;
date_added?: string;
user_id?: number | string;
accepted_or_declined?: number | string;
auth_net_payment_profile_id?: string;
zip_code?: string;
}
export interface PricingData {
price_from_supplier: number;
price_for_customer: number;
price_for_employee: number;
price_same_day: number;
price_prime: number;
price_emergency: number;
date: string;
}
export interface PromoData {
name_of_promotion: string;
description: string;
money_off_delivery: number;
text_on_ticket: string;
}
export interface AutoTicketData {
id: number;
customer_id: number | string;
account_number: string;
customer_town: string;
customer_state: number | string;
customer_address: string;
customer_zip: string;
customer_full_name: string;
oil_prices_id: number | string;
fill_date: string;
gallons_delivered: string;
price_per_gallon: string;
total_amount_customer: string;
payment_type: number | string;
payment_card_id: number | string;
payment_status: number;
open_ticket_id: number | null;
}
export interface AutoDeliveryData {
id: number;
customer_id: number;
account_number: string;
customer_town: string;
customer_state: number;
customer_address: string;
customer_zip: string;
customer_full_name: string;
last_fill: string;
days_since_last_fill: number;
last_updated: string;
estimated_gallons_left: number;
estimated_gallons_left_prev_day: number;
tank_height: string;
tank_size: string | number;
house_factor: number;
auto_status: number;
open_ticket_id: number | null;
}
export interface CustomerDescriptionData {
customer_id: number;
account_number: string;
company_id: number;
fill_location: number;
description: string;
}
// ============================================
// Axios Response Type Helper
// ============================================
export interface AxiosApiResponse<T> {
data: T;
status: number;
statusText: string;
}
// Utility types
export type CustomerListResponse = PaginatedResponse<Customer>;
export type DeliveryListResponse = PaginatedResponse<Delivery>;
export type TransactionListResponse = PaginatedResponse<PaymentTransaction>;
export type CardListResponse = PaginatedResponse<CreditCard>;
export type ServiceListResponse = PaginatedResponse<ServiceCall>;
export type EmployeeListResponse = PaginatedResponse<Employee>;