feat: 5-tier pricing UI, market ticker, delivery map, and stats dashboard
Full frontend companion to the API updates: - Pricing: Oil price admin page now supports 5-tier configuration for same-day/prime/emergency fees with collapsible tier sections - Market Ticker: Add GlobalMarketTicker and OilPriceTicker components with real-time commodity + competitor prices in header bar - Delivery Map: New interactive Leaflet map view for daily deliveries - Stats: Add PricingHistoryChart component and info pages for market trends with daily/weekly/monthly gallon charts and YoY comparisons - Layout: Refactor header navbar to separate search into navbar-center, add oilPrice Pinia store with polling, update sidebar navigation - Forms: Wire tier selection into delivery create/edit flows, update types and services for new pricing and scraper API endpoints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
<!-- src/pages/ticket/ticket.vue -->
|
||||
<template>
|
||||
|
||||
<div class=" max-w-5xl text-black bg-white font-mono text-md">
|
||||
<div class="grid grid-cols-12 pt-10">
|
||||
<div class="col-span-6">
|
||||
|
||||
|
||||
<div class="grid grid-cols-12">
|
||||
<div class="col-span-2 pt-2 pl-4">#2 </div>
|
||||
<div class="col-span-2 pt-2"></div>
|
||||
@@ -15,8 +13,6 @@
|
||||
<div class="col-span-3 text-xs pt-3 ">{{ customer.customer_phone_number }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="grid grid-cols-12 pt-2 pb-2">
|
||||
<div class="col-span-9 pl-5">
|
||||
{{ customer.customer_first_name }} {{ customer.customer_last_name }}
|
||||
@@ -36,28 +32,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="grid grid-cols-12 pl-6 pb-6 gap-10 max-h-32">
|
||||
<div class="col-span-6">
|
||||
<div class="grid grid-cols-12">
|
||||
<div class="col-span-12 ">{{ customer_description.description }}</div>
|
||||
<div class="col-span-12 " v-if="delivery.promo_id !== null">Promo: {{ promo.text_on_ticket
|
||||
}}</div>
|
||||
<div class="col-span-12 "></div>
|
||||
<div class="col-span-12 " v-if="delivery.prime == 1">PRIME</div>
|
||||
<div class="col-span-12 " v-if="delivery.same_day == 1">SAME DAY</div>
|
||||
<div class="col-span-12 " v-if="delivery.emergency == 1">EMERGENCY</div>
|
||||
|
||||
<div class="col-span-12 text-lg" v-if="delivery.payment_type == 0">CASH</div>
|
||||
<div class="col-span-12 text-lg" v-else-if="delivery.payment_type == 1">Credit Card</div>
|
||||
<div class="col-span-12 text-lg" v-else-if="delivery.payment_type == 2">Credit Card/Cash
|
||||
</div>
|
||||
<div class="col-span-12 text-lg" v-else-if="delivery.payment_type == 3">Check</div>
|
||||
<div class="col-span-12 text-lg" v-else-if="delivery.payment_type == 4">Other</div>
|
||||
<div class="col-span-12" v-else></div>
|
||||
<div class="col-span-12 " v-if="delivery.customer_asked_for_fill == 0">
|
||||
{{ delivery.gallons_ordered }}</div>
|
||||
<div class="col-span-12 " v-else>Fill</div>
|
||||
<div class="col-span-12 text-lg">Credit Card</div>
|
||||
<div class="col-span-12" v-if="promo">{{ promo_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-6 border-2" v-if="delivery.dispatcher_notes">
|
||||
@@ -68,8 +49,6 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="grid grid-cols-12">
|
||||
<div class="col-span-6 ">
|
||||
<div class="col-span-12 pl-5">Auburn Oil</div>
|
||||
@@ -86,11 +65,10 @@
|
||||
<div class="col-span-12 pl-5">508 426 8800</div>
|
||||
</div>
|
||||
<div class="col-span-6 ">
|
||||
<div v-if="past_deliveries1.length > 1">
|
||||
<div class="col-span-6" v-for="past_delivery in past_deliveries1">
|
||||
<div class="" v-if="past_delivery.gallons_delivered != 0.00">
|
||||
{{ past_delivery.when_delivered }} - {{ past_delivery.gallons_delivered }}
|
||||
</div>
|
||||
<div v-if="past_deliveries.length > 0">
|
||||
<div class="col-span-6" v-for="past_delivery in past_deliveries"
|
||||
:key="past_delivery.fill_date">
|
||||
{{ past_delivery.fill_date }} - {{ past_delivery.gallons_delivered }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -99,45 +77,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="col-span-6 ">
|
||||
<div class="col-span-4 ">
|
||||
<div class="grid grid-cols-12 ">
|
||||
<div class="col-span-12 h-7 pl-4 pt-2">{{ delivery.when_ordered }}</div>
|
||||
<div class="col-span-12 h-7 pl-4 pt-2">{{ delivery.expected_delivery_date }}</div>
|
||||
|
||||
<div class="col-span-12 h-7 pl-4 pt-2" v-if="delivery.customer_asked_for_fill == 0">
|
||||
{{ delivery.gallons_ordered }}</div>
|
||||
<div class="col-span-12 h-7 pl-4 pt-2" v-else></div>
|
||||
|
||||
<div class="col-span-12 h-7 pl-4 pt-2" v-if="promo_active">
|
||||
<div class="flex gap-2">
|
||||
<div class="line-through"> {{ delivery.customer_price }}</div> ({{ promoprice}})
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 h-7 pl-4 pt-2" v-else>{{ delivery.customer_price }}</div>
|
||||
|
||||
|
||||
<div class="col-span-12 h-7 pl-4 pt-4" v-if="delivery.customer_asked_for_fill == 0">
|
||||
<div v-if="promo_active">
|
||||
{{ total_amount_after_discount }}
|
||||
</div>
|
||||
<div v-else> {{ total_amount }}</div>
|
||||
|
||||
</div>
|
||||
<div class="col-span-12 h-7 pl-4 pt-4" v-else></div>
|
||||
|
||||
<div class="col-span-12 h-7 pl-4 pt-2"></div>
|
||||
<div class="col-span-12 h-7 pl-4 pt-2"></div>
|
||||
<div class="col-span-12 h-7 pl-4 pt-2"></div>
|
||||
<div class="col-span-12 h-7 pl-4 pt-2"></div>
|
||||
<div class="col-span-12 h-7 pl-4 pt-4"> </div>
|
||||
<div class="col-span-12 h-7 pt-6"></div>
|
||||
<div class="col-span-12 h-7"></div>
|
||||
<div class="col-span-12 h-7 pl-8"></div>
|
||||
@@ -149,277 +98,185 @@
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from 'vue'
|
||||
import axios from 'axios'
|
||||
import authHeader from '../../services/auth.header'
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { notify } from "@kyvg/vue3-notification"
|
||||
import { deliveryService } from '../../services/deliveryService'
|
||||
import { customerService } from '../../services/customerService'
|
||||
import { adminService } from '../../services/adminService'
|
||||
import { queryService } from '../../services/queryService'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Ticket',
|
||||
interface PastDelivery {
|
||||
gallons_delivered: number
|
||||
fill_date: string
|
||||
}
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
user: {
|
||||
user_id: 0,
|
||||
},
|
||||
past_deliveries1: [
|
||||
{
|
||||
gallons_delivered: 0,
|
||||
when_delivered: '',
|
||||
}
|
||||
],
|
||||
past_deliveries2: [
|
||||
{
|
||||
gallons_delivered: 0,
|
||||
when_delivered: '',
|
||||
}
|
||||
],
|
||||
delivery: {
|
||||
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: 0,
|
||||
customer_temperature: '',
|
||||
dispatcher_notes: '',
|
||||
prime: 0,
|
||||
same_day: 0,
|
||||
emergency: 0,
|
||||
payment_type: 0,
|
||||
payment_card_id: 0,
|
||||
driver_employee_id: 0,
|
||||
driver_first_name: '',
|
||||
driver_last_name: '',
|
||||
promo_id: 0,
|
||||
},
|
||||
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: '',
|
||||
},
|
||||
customer_tank: {
|
||||
id: 0,
|
||||
last_tank_inspection: null,
|
||||
tank_status: false,
|
||||
outside_or_inside: false,
|
||||
tank_size: 0,
|
||||
},
|
||||
customer_description: {
|
||||
id: 0,
|
||||
customer_id: 0,
|
||||
account_number: '',
|
||||
company_id: '',
|
||||
fill_location: 0,
|
||||
description: '',
|
||||
},
|
||||
promoprice: 0,
|
||||
promo_active: false,
|
||||
promo: {
|
||||
id: 0,
|
||||
name_of_promotion: '',
|
||||
description: '',
|
||||
money_off_delivery: 0,
|
||||
text_on_ticket: ''
|
||||
},
|
||||
priceprime: 0,
|
||||
pricesameday: 0,
|
||||
priceemergency: 0,
|
||||
total_amount: 0,
|
||||
discount: 0,
|
||||
total_amount_after_discount: 0,
|
||||
// State
|
||||
const loaded = ref(false)
|
||||
const past_deliveries = ref<PastDelivery[]>([])
|
||||
const delivery = ref<any>({
|
||||
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: 0,
|
||||
customer_temperature: '',
|
||||
dispatcher_notes: '',
|
||||
prime: 0,
|
||||
same_day: 0,
|
||||
emergency: 0,
|
||||
payment_type: 0,
|
||||
payment_card_id: 0,
|
||||
driver_employee_id: 0,
|
||||
driver_first_name: '',
|
||||
driver_last_name: '',
|
||||
promo_id: 0,
|
||||
})
|
||||
const customer_tank = ref<any>({
|
||||
id: 0,
|
||||
last_tank_inspection: null,
|
||||
tank_status: false,
|
||||
outside_or_inside: false,
|
||||
tank_size: 0,
|
||||
})
|
||||
const customer = ref<any>({
|
||||
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 customer_description = ref<any>({
|
||||
id: 0,
|
||||
customer_id: 0,
|
||||
account_number: '',
|
||||
company_id: '',
|
||||
fill_location: 0,
|
||||
description: '',
|
||||
})
|
||||
const promo = ref<any>(null)
|
||||
const promo_text = ref('')
|
||||
const todays_price = ref(0)
|
||||
|
||||
// Methods
|
||||
async function getOrder(deliveryId: string | number) {
|
||||
try {
|
||||
const response = await deliveryService.getById(Number(deliveryId))
|
||||
delivery.value = (response.data as any)?.delivery || response.data
|
||||
getCustomer(delivery.value.customer_id)
|
||||
if (delivery.value.promo_id) {
|
||||
getPromo(delivery.value.promo_id)
|
||||
}
|
||||
},
|
||||
} catch (error) {
|
||||
notify({
|
||||
title: "Error",
|
||||
text: "Could not get delivery",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
created() {
|
||||
this.getOilOrder(this.$route.params.id)
|
||||
this.sumdelivery(this.$route.params.id);
|
||||
async function getCustomer(userId: number) {
|
||||
try {
|
||||
const response = await customerService.getById(userId)
|
||||
customer.value = (response.data as any)?.customer || response.data
|
||||
getPastDeliveries(customer.value.id)
|
||||
getCustomerDescription(customer.value.id)
|
||||
getCustomerTank(customer.value.id)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch customer:', error)
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.getOilOrder(this.$route.params.id)
|
||||
this.sumdelivery(this.$route.params.id);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
async function getCustomerTank(userId: number) {
|
||||
try {
|
||||
const response = await customerService.getTank(userId)
|
||||
customer_tank.value = (response.data as any)?.tank || response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tank:', error)
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
async function getCustomerDescription(userId: number) {
|
||||
try {
|
||||
const response = await customerService.getDescription(userId)
|
||||
customer_description.value = (response.data as any)?.description || response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch description:', error)
|
||||
}
|
||||
}
|
||||
|
||||
methods: {
|
||||
getOilOrder(delivery_id: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/delivery/order/" + delivery_id;
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
withCredentials: true,
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
if (response.data && response.data.ok) {
|
||||
this.delivery = response.data.delivery;
|
||||
this.getCustomer(this.delivery.customer_id)
|
||||
if (this.delivery.promo_id != null) {
|
||||
this.getPromo(this.delivery.promo_id);
|
||||
this.promo_active = true;
|
||||
this.getPrice(delivery_id)
|
||||
}
|
||||
} else {
|
||||
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
notify({
|
||||
title: "Error",
|
||||
text: "Could not get delivery",
|
||||
type: "error",
|
||||
});
|
||||
});
|
||||
},
|
||||
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.customer_description = response.data?.description || response.data
|
||||
})
|
||||
},
|
||||
async function getPromo(promoId: number) {
|
||||
try {
|
||||
const response = await adminService.promos.getById(promoId)
|
||||
promo.value = response.data
|
||||
promo_text.value = promo.value?.text_on_ticket || ''
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch promo:', error)
|
||||
}
|
||||
}
|
||||
|
||||
sumdelivery(delivery_id: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
withCredentials: true,
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
if (response.data.ok) {
|
||||
this.priceprime = response.data.priceprime;
|
||||
this.pricesameday = response.data.pricesameday;
|
||||
this.priceemergency = response.data.priceemergency;
|
||||
this.total_amount = response.data.total_amount;
|
||||
this.discount = response.data.discount;
|
||||
this.total_amount_after_discount = response.data.total_amount_after_discount;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
notify({
|
||||
title: "Error",
|
||||
text: "Could not get oil pricing",
|
||||
type: "error",
|
||||
});
|
||||
});
|
||||
},
|
||||
async function getTodayPrice() {
|
||||
try {
|
||||
const response = await queryService.getOilPrice()
|
||||
if ((response.data as any).ok) {
|
||||
todays_price.value = (response.data as any).price_for_customer
|
||||
}
|
||||
} catch (error) {
|
||||
notify({
|
||||
title: "Error",
|
||||
text: "Could not get oil pricing",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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?.customer || response.data
|
||||
this.getPastDeliveries1(this.customer.id)
|
||||
|
||||
this.getPastDeliveries2(this.customer.id)
|
||||
this.getCustomerDescription(this.customer.id)
|
||||
this.getCustomerTank(this.customer.id)
|
||||
})
|
||||
},
|
||||
getCustomerTank(userid: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/customer/tank/' + userid;
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.customer_tank = response.data?.tank || response.data
|
||||
})
|
||||
},
|
||||
async function getPastDeliveries(userId: number) {
|
||||
try {
|
||||
const response = await deliveryService.getByCustomer(userId)
|
||||
past_deliveries.value = (response.data as any)?.deliveries || response.data || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch past deliveries:', error)
|
||||
}
|
||||
}
|
||||
|
||||
getPastDeliveries1(userid: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/past1/' + userid;
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.past_deliveries1 = response.data?.deliveries || response.data
|
||||
})
|
||||
},
|
||||
getPastDeliveries2(userid: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/past2/' + userid;
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
headers: authHeader(),
|
||||
}).then((response: any) => {
|
||||
this.past_deliveries2 = response.data?.deliveries || response.data
|
||||
})
|
||||
},
|
||||
// Watchers
|
||||
watch(() => route.params.id, (newId) => {
|
||||
if (newId) {
|
||||
getOrder(newId as string)
|
||||
getTodayPrice()
|
||||
}
|
||||
})
|
||||
|
||||
getPrice(delivery_id: any) {
|
||||
let path = import.meta.env.VITE_BASE_URL + "/promo/promoprice/" + delivery_id;
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
withCredentials: true,
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
if (response.data) {
|
||||
this.promoprice = response.data.price
|
||||
}
|
||||
})
|
||||
},
|
||||
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?.promo || response.data
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
},
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
if (route.params.id) {
|
||||
getOrder(route.params.id as string)
|
||||
getTodayPrice()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user