feat(frontend): redesign home page with charts, map, and pricing dropdown
- Header: Replace date with oil price dropdown showing all pricing tiers (regular, same day, prime, emergency) and service pricing - Footer: Add search shortcuts reference (@, !, #, $) - Home page complete redesign: - Animated stat cards (today's deliveries, week gallons, deliveries, revenue) - 28-day delivery trend chart using Chart.js - Mini map showing today's delivery routes with Leaflet - Quick actions grid for common tasks - Town distribution visualization with progress bars - Gradient styling and fade-in animations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,11 +15,35 @@
|
||||
<div class="">Spring Rebuilders - (508) 799-9342</div>
|
||||
</nav>
|
||||
<nav>
|
||||
<h6 class="footer-title">Google Review link / qrcode</h6>
|
||||
<a class="link link-hover">https://g.page/r/CZHnPQ85LsMUEBM/review</a>
|
||||
<button @click="copyReviewLink" class="btn btn-outline btn-sm ml-2">Copy Link</button>
|
||||
<h6 class="link link-hover"> <img src="../../assets/images/googlereview.png" alt="Company Logo" class="h-10 w-auto" /></h6>
|
||||
<a class="link link-hover"></a>
|
||||
<h6 class="footer-title">Search Shortcuts</h6>
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<kbd class="kbd kbd-sm bg-neutral text-neutral-content border-neutral-content/30">@</kbd>
|
||||
<span class="text-sm opacity-80">Last name</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<kbd class="kbd kbd-sm bg-neutral text-neutral-content border-neutral-content/30">!</kbd>
|
||||
<span class="text-sm opacity-80">Address</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<kbd class="kbd kbd-sm bg-neutral text-neutral-content border-neutral-content/30">#</kbd>
|
||||
<span class="text-sm opacity-80">Phone</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<kbd class="kbd kbd-sm bg-neutral text-neutral-content border-neutral-content/30">$</kbd>
|
||||
<span class="text-sm opacity-80">Account #</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<nav>
|
||||
<h6 class="footer-title">Google Review</h6>
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="../../assets/images/googlereview.png" alt="Google Review QR" class="h-16 w-auto rounded" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<a class="link link-hover text-xs break-all max-w-32">g.page/r/CZHnPQ85LsMUEBM/review</a>
|
||||
<button @click="copyReviewLink" class="btn btn-outline btn-xs">Copy Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
</template>
|
||||
@@ -29,13 +53,45 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
const copyReviewLink = async () => {
|
||||
const textToCopy = 'https://g.page/r/CZHnPQ85LsMUEBM/review';
|
||||
|
||||
// Try the modern Clipboard API first (works in secure contexts like HTTPS or localhost)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
try {
|
||||
await navigator.clipboard.writeText('https://g.page/r/CZHnPQ85LsMUEBM/review')
|
||||
alert('Link copied to clipboard!')
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
alert('Link copied to clipboard!');
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err)
|
||||
alert('Failed to copy link. Please try again.')
|
||||
console.warn('Clipboard API failed, falling back to legacy method.', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for non-secure contexts (like HTTP LAN)
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = textToCopy;
|
||||
|
||||
// Ensure it's not visible but part of the DOM
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-9999px";
|
||||
textArea.style.top = "0";
|
||||
document.body.appendChild(textArea);
|
||||
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
alert('Link copied to clipboard!');
|
||||
} else {
|
||||
alert('Unable to copy link. Please manually copy: ' + textToCopy);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed', err);
|
||||
alert('Unable to copy link. Please manually copy: ' + textToCopy);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
</script>
|
||||
<style></style>
|
||||
|
||||
@@ -13,11 +13,98 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
</label>
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-xs text-base-content/60">{{ dayOfWeek }}</span>
|
||||
<span class="normal-case text-xl font-bold">
|
||||
{{ currentDate }}
|
||||
|
||||
<!-- Oil Price Dropdown (replaces date display) -->
|
||||
<div class="dropdown dropdown-hover">
|
||||
<label tabindex="0" class="btn btn-ghost gap-2 hover:bg-warning/10 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-warning to-warning/60 flex items-center justify-center shadow-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-warning-content">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="text-xs text-base-content/60 font-medium">Oil Price</span>
|
||||
<span class="text-xl font-bold text-warning" v-if="oilPrice.price_for_customer !== null">
|
||||
${{ formatPrice(oilPrice.price_for_customer) }}<span class="text-xs font-normal text-base-content/60">/gal</span>
|
||||
</span>
|
||||
<span v-else class="loading loading-dots loading-xs"></span>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-base-content/40">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
<div tabindex="0" class="dropdown-content z-[60] mt-2 p-0 shadow-2xl bg-base-100 rounded-xl w-80 border border-base-300">
|
||||
<!-- Header with date -->
|
||||
<div class="bg-gradient-to-r from-warning/20 to-warning/5 p-4 rounded-t-xl border-b border-base-300">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-base-content/60 uppercase tracking-wider font-medium">Today's Pricing</p>
|
||||
<p class="text-sm font-semibold">{{ dayOfWeek }}, {{ currentDate }}</p>
|
||||
</div>
|
||||
<div class="badge badge-warning badge-sm">Live</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Oil Pricing Section -->
|
||||
<div class="p-4">
|
||||
<h4 class="text-xs uppercase tracking-wider text-base-content/50 font-semibold mb-3 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.21 0 003 2.48z" />
|
||||
</svg>
|
||||
Oil Delivery
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center p-2 rounded-lg bg-base-200/50 hover:bg-base-200 transition-colors">
|
||||
<span class="text-sm font-medium">Regular Delivery</span>
|
||||
<span class="font-mono font-bold text-lg text-success">${{ formatPrice(oilPrice.price_for_customer) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-2 rounded-lg hover:bg-base-200/50 transition-colors">
|
||||
<span class="text-sm text-base-content/70">Same Day</span>
|
||||
<span class="font-mono font-semibold">${{ formatPrice(oilPrice.price_same_day) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-2 rounded-lg hover:bg-base-200/50 transition-colors">
|
||||
<span class="text-sm text-base-content/70">Prime</span>
|
||||
<span class="font-mono font-semibold">${{ formatPrice(oilPrice.price_prime) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-2 rounded-lg hover:bg-base-200/50 transition-colors">
|
||||
<span class="text-sm text-base-content/70">Emergency</span>
|
||||
<span class="font-mono font-semibold text-error">${{ formatPrice(oilPrice.price_emergency) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider my-0 px-4"></div>
|
||||
|
||||
<!-- Service Pricing Section -->
|
||||
<div class="p-4">
|
||||
<h4 class="text-xs uppercase tracking-wider text-base-content/50 font-semibold mb-3 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
|
||||
</svg>
|
||||
Service Calls
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-center p-2 rounded-lg bg-base-200/50 hover:bg-base-200 transition-colors">
|
||||
<span class="text-sm font-medium">Per Hour</span>
|
||||
<span class="font-mono font-bold text-lg text-info">$125</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center p-2 rounded-lg hover:bg-base-200/50 transition-colors">
|
||||
<span class="text-sm text-base-content/70">Emergency</span>
|
||||
<span class="font-mono font-semibold text-error">$200</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer with supplier cost (admin only) -->
|
||||
<div v-if="user.user_admin === 0 && oilPrice.price_from_supplier" class="bg-base-200/30 p-3 rounded-b-xl border-t border-base-300">
|
||||
<div class="flex justify-between items-center text-xs text-base-content/50">
|
||||
<span>Supplier Cost</span>
|
||||
<span class="font-mono">${{ formatPrice(oilPrice.price_from_supplier) }}/gal</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +170,7 @@
|
||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li class="p-2 font-semibold">{{ user.user_name }}</li>
|
||||
<div class="divider my-0"></div>
|
||||
<li><router-link :to="{ name: 'employeeProfile', params: { id: user.user_id } }">Profile</router-link></li>
|
||||
<li v-if="user && user.user_id"><router-link :to="{ name: 'employeeProfile', params: { id: user.user_id } }">Profile</router-link></li>
|
||||
<li><router-link :to="{ name: 'changePassword' }">Change Password</router-link></li>
|
||||
<div class="divider my-0"></div>
|
||||
<li class="menu-title text-xs opacity-60">Theme</li>
|
||||
@@ -245,9 +332,27 @@ interface RoutingOption {
|
||||
// Router
|
||||
const router = useRouter()
|
||||
|
||||
// Oil price interface
|
||||
interface OilPrice {
|
||||
price_from_supplier: number | null;
|
||||
price_for_customer: number | null;
|
||||
price_for_employee: number | null;
|
||||
price_same_day: number | null;
|
||||
price_prime: number | null;
|
||||
price_emergency: number | null;
|
||||
}
|
||||
|
||||
// Reactive data
|
||||
const user = ref({} as User)
|
||||
const currentPhone = ref('')
|
||||
const oilPrice = ref<OilPrice>({
|
||||
price_from_supplier: null,
|
||||
price_for_customer: null,
|
||||
price_for_employee: null,
|
||||
price_same_day: null,
|
||||
price_prime: null,
|
||||
price_emergency: null
|
||||
})
|
||||
const routingOptions = ref([
|
||||
{ value: 'main', label: '407323' },
|
||||
{ value: 'sip', label: '407323_auburnoil' },
|
||||
@@ -290,12 +395,45 @@ const dayOfWeek = computed((): string => {
|
||||
return now.toLocaleDateString('en-US', { weekday: 'long' });
|
||||
})
|
||||
|
||||
// Format price safely (handles null, string, and number)
|
||||
const formatPrice = (price: number | string | null): string => {
|
||||
if (price === null || price === undefined) return '—';
|
||||
const numPrice = typeof price === 'string' ? parseFloat(price) : price;
|
||||
if (isNaN(numPrice)) return '—';
|
||||
return numPrice.toFixed(2);
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
userStatus()
|
||||
updatestatus()
|
||||
fetchOilPrice()
|
||||
})
|
||||
|
||||
// Fetch oil pricing
|
||||
const fetchOilPrice = () => {
|
||||
const path = import.meta.env.VITE_BASE_URL + '/info/price/oil';
|
||||
axios({
|
||||
method: 'get',
|
||||
url: path,
|
||||
withCredentials: true,
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
oilPrice.value = {
|
||||
price_from_supplier: response.data.price_from_supplier,
|
||||
price_for_customer: response.data.price_for_customer,
|
||||
price_for_employee: response.data.price_for_employee,
|
||||
price_same_day: response.data.price_same_day,
|
||||
price_prime: response.data.price_prime,
|
||||
price_emergency: response.data.price_emergency
|
||||
};
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Failed to fetch oil pricing:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Functions
|
||||
const userStatus = async () => {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
||||
|
||||
@@ -1,186 +1,293 @@
|
||||
<!-- src/pages/Index.vue -->
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div class="w-full px-4 md:px-10 ">
|
||||
<!-- Breadcrumbs & Welcome Header -->
|
||||
<div class="text-sm breadcrumbs">
|
||||
<ul>
|
||||
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mt-4">
|
||||
Welcome, {{ employee.employee_first_name }}!
|
||||
<div class="w-full px-4 md:px-10 py-4">
|
||||
<!-- Welcome Header with Greeting -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl md:text-4xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
||||
Welcome back, {{ employee.employee_first_name }}!
|
||||
</h1>
|
||||
<p class="text-base-content/60 mt-1">Here's what's happening today</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="badge badge-lg badge-primary gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||
</svg>
|
||||
{{ formattedDate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Dashboard Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 my-6 animate-fade-in">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-fade-in">
|
||||
|
||||
<!-- Card 1: Today's Deliveries -->
|
||||
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift xl:col-span-2">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-semibold">Today's Deliveries</h3>
|
||||
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-primary">
|
||||
<!-- Left Column: Stats & Chart -->
|
||||
<div class="lg:col-span-8 space-y-6">
|
||||
|
||||
<!-- Stats Row: Quick Glance Cards -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<!-- Today's Deliveries -->
|
||||
<div class="stat-card group">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs uppercase tracking-wider text-base-content/50 font-semibold">Today</span>
|
||||
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-primary">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-4xl font-bold">{{ delivery_count }}</span>
|
||||
<span class="text-base-content/60">total deliveries</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span class="font-medium">Completed</span>
|
||||
<span class="font-mono">{{ delivery_count_delivered }} / {{ delivery_count }}</span>
|
||||
</div>
|
||||
<progress class="progress progress-primary w-full h-3" :value="delivery_count_delivered" :max="delivery_count"></progress>
|
||||
</div>
|
||||
<div class="text-3xl font-bold">{{ delivery_count }}</div>
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<span class="text-success text-sm font-medium">{{ delivery_count_delivered }}</span>
|
||||
<span class="text-base-content/50 text-xs">delivered</span>
|
||||
</div>
|
||||
<progress class="progress progress-primary w-full h-2 mt-2" :value="delivery_count_delivered" :max="delivery_count || 1"></progress>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Today's Oil Price -->
|
||||
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-semibold">Oil Pricing</h3>
|
||||
<div class="w-12 h-12 rounded-full bg-warning/10 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-warning">
|
||||
<!-- Week Gallons -->
|
||||
<div class="stat-card group">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs uppercase tracking-wider text-base-content/50 font-semibold">This Week</span>
|
||||
<div class="w-8 h-8 rounded-lg bg-success/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-success">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-3xl font-bold">{{ formatNumber(total_gallons_past_week) }}</div>
|
||||
<div class="text-base-content/50 text-xs mt-1">gallons delivered</div>
|
||||
</div>
|
||||
|
||||
<!-- Week Deliveries -->
|
||||
<div class="stat-card group">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs uppercase tracking-wider text-base-content/50 font-semibold">Deliveries</span>
|
||||
<div class="w-8 h-8 rounded-lg bg-info/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-info">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-3xl font-bold">{{ total_deliveries }}</div>
|
||||
<div class="text-base-content/50 text-xs mt-1">this week</div>
|
||||
</div>
|
||||
|
||||
<!-- Week Profit -->
|
||||
<div class="stat-card group">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs uppercase tracking-wider text-base-content/50 font-semibold">Revenue</span>
|
||||
<div class="w-8 h-8 rounded-lg bg-warning/10 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-warning">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-base-content/70">Per Gallon</span>
|
||||
<span class="text-2xl font-bold font-mono">${{ today_oil_price }}</span>
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-base-content/70">Same Day</span>
|
||||
<span class="font-mono font-semibold">${{ price_same_day }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-base-content/70">Prime</span>
|
||||
<span class="font-mono font-semibold">${{ price_prime }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-base-content/70">Emergency</span>
|
||||
<span class="font-mono font-semibold">${{ price_emergency }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-3xl font-bold text-success">${{ formatNumber(total_profit_past_week) }}</div>
|
||||
<div class="text-base-content/50 text-xs mt-1">this week</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Service Pricing -->
|
||||
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift">
|
||||
<!-- Weekly Trend Chart -->
|
||||
<div class="card-glass p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-semibold">Service Pricing</h3>
|
||||
<div class="w-12 h-12 rounded-full bg-info/10 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-info">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Weekly Delivery Trend</h3>
|
||||
<p class="text-sm text-base-content/50">Gallons delivered over the past 4 weeks</p>
|
||||
</div>
|
||||
<router-link :to="{ name: 'stats' }" class="btn btn-ghost btn-sm gap-1">
|
||||
View Stats
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="h-64">
|
||||
<Line
|
||||
v-if="chartData"
|
||||
:data="chartData as any"
|
||||
:options="chartOptions as any"
|
||||
/>
|
||||
<div v-else class="flex items-center justify-center h-full">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-base-content/70">Per Hour</span>
|
||||
<span class="text-2xl font-bold font-mono">$125</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-base-content/70">Emergency</span>
|
||||
<span class="text-2xl font-bold font-mono">$200</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Search Shortcuts -->
|
||||
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-semibold">Search Shortcuts</h3>
|
||||
<div class="w-12 h-12 rounded-full bg-accent/10 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-accent">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||
<!-- Right Column: Map & Quick Actions -->
|
||||
<div class="lg:col-span-4 space-y-6">
|
||||
|
||||
<!-- Mini Delivery Map -->
|
||||
<div class="card-glass p-4 overflow-hidden">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-semibold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-primary">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
||||
</svg>
|
||||
Today's Routes
|
||||
</h3>
|
||||
<router-link :to="{ name: 'deliveryMap' }" class="btn btn-xs btn-ghost">
|
||||
Full Map
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Map Stats -->
|
||||
<div class="flex gap-2 mb-3">
|
||||
<span class="badge badge-sm badge-primary">{{ mapDeliveries.length }} stops</span>
|
||||
<span class="badge badge-sm badge-secondary">{{ uniqueTowns.length }} towns</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<kbd class="kbd kbd-sm">@</kbd>
|
||||
<span class="text-base-content/70">Last name</span>
|
||||
|
||||
<!-- Map Container -->
|
||||
<div class="rounded-lg overflow-hidden h-64 bg-base-300">
|
||||
<l-map
|
||||
v-if="mapDeliveries.length > 0"
|
||||
ref="map"
|
||||
:zoom="mapZoom"
|
||||
:center="mapCenter"
|
||||
:use-global-leaflet="false"
|
||||
class="h-full w-full"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
layer-type="base"
|
||||
name="OpenStreetMap"
|
||||
></l-tile-layer>
|
||||
<l-marker
|
||||
v-for="delivery in mappedDeliveries"
|
||||
:key="delivery.id"
|
||||
:lat-lng="[parseFloat(delivery.latitude!), parseFloat(delivery.longitude!)]"
|
||||
>
|
||||
<l-popup>
|
||||
<div class="text-sm">
|
||||
<p class="font-bold">{{ delivery.customerName }}</p>
|
||||
<p>{{ delivery.town }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<kbd class="kbd kbd-sm">!</kbd>
|
||||
<span class="text-base-content/70">Address</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<kbd class="kbd kbd-sm">#</kbd>
|
||||
<span class="text-base-content/70">Phone number</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<kbd class="kbd kbd-sm">$</kbd>
|
||||
<span class="text-base-content/70">Account number</span>
|
||||
</l-popup>
|
||||
</l-marker>
|
||||
</l-map>
|
||||
<div v-else class="flex flex-col items-center justify-center h-full text-base-content/50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 mb-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 6.75V15m6-6v8.25m.503 3.498l4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 00-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0z" />
|
||||
</svg>
|
||||
<p class="text-sm">No deliveries mapped</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Card 5: This Week's Stats -->
|
||||
<!-- <div class="bg-neutral rounded-lg p-5 xl:col-span-4">
|
||||
<h3 class="text-xl font-bold mb-4">This Week's Stats</h3>
|
||||
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-100 w-full">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Deliveries</div>
|
||||
<div class="stat-value">{{ total_deliveries }}</div>
|
||||
<div class="stat-desc">In the last 7 days</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Gallons</div>
|
||||
<div class="stat-value">{{ total_gallons_past_week }}</div>
|
||||
<div class="stat-desc">Delivered this week</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Profit</div>
|
||||
<div class="stat-value text-success">${{ total_profit_past_week }}</div>
|
||||
<div class="stat-desc">Estimated earnings</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
<!-- Quick Actions -->
|
||||
<div class="card-glass p-4">
|
||||
<h3 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-accent">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
|
||||
</svg>
|
||||
Quick Actions
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<router-link :to="{ name: 'customerCreate' }" class="quick-action-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
|
||||
</svg>
|
||||
<span>New Customer</span>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'delivery' }" class="quick-action-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
<span>Deliveries</span>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'deliveryOutForDelivery' }" class="quick-action-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
|
||||
</svg>
|
||||
<span>Today's Run</span>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'ServiceCalendar' }" class="quick-action-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||
</svg>
|
||||
<span>Service Calendar</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity / Town Distribution -->
|
||||
<div class="card-glass p-4">
|
||||
<h3 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-info">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z" />
|
||||
</svg>
|
||||
Today by Town
|
||||
</h3>
|
||||
<div v-if="townCounts.length > 0" class="space-y-2">
|
||||
<div v-for="town in townCounts.slice(0, 5)" :key="town.name" class="flex items-center justify-between">
|
||||
<span class="text-sm truncate flex-1">{{ town.name }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-24 bg-base-300 rounded-full h-2">
|
||||
<div
|
||||
class="bg-primary h-2 rounded-full transition-all duration-500"
|
||||
:style="{ width: `${(town.count / townCounts[0].count) * 100}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-sm font-mono font-semibold w-6 text-right">{{ town.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-sm text-base-content/50 text-center py-4">
|
||||
No deliveries scheduled today
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import authHeader from '../services/auth.header'
|
||||
import Header from '../layouts/headers/headerauth.vue'
|
||||
import SideBar from '../layouts/sidebar/sidebar.vue'
|
||||
import { deliveryService } from '../services/deliveryService'
|
||||
import { DeliveryMapItem } from '../types/models'
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import { LMap, LTileLayer, LMarker, LPopup } from "@vue-leaflet/vue-leaflet"
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
} from 'chart.js'
|
||||
import { Line } from 'vue-chartjs'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
clickCount?: number
|
||||
}>()
|
||||
// Register Chart.js components
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
)
|
||||
|
||||
// Router
|
||||
const router = useRouter()
|
||||
|
||||
// Reactive data
|
||||
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)
|
||||
const today_oil_price = ref(0)
|
||||
const price_for_employee = ref(0)
|
||||
const price_same_day = ref(0)
|
||||
const price_prime = ref(0)
|
||||
const price_emergency = ref(0)
|
||||
const total_gallons_past_week = ref(0)
|
||||
const total_profit_past_week = ref(0)
|
||||
const total_deliveries = ref(0)
|
||||
const user = ref({
|
||||
user_id: 0,
|
||||
user_name: '',
|
||||
@@ -201,24 +308,129 @@ const employee = ref({
|
||||
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)
|
||||
|
||||
// Map data
|
||||
const mapDeliveries = ref<DeliveryMapItem[]>([])
|
||||
const mapZoom = ref(10)
|
||||
|
||||
// Chart data
|
||||
const weeklyData = ref<{ date: string; gallons: number }[]>([])
|
||||
|
||||
// Computed
|
||||
const formattedDate = computed(() => {
|
||||
const now = new Date()
|
||||
return now.toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
})
|
||||
|
||||
const mappedDeliveries = computed(() =>
|
||||
mapDeliveries.value.filter(d => d.latitude && d.longitude)
|
||||
)
|
||||
|
||||
const uniqueTowns = computed(() =>
|
||||
[...new Set(mapDeliveries.value.map(d => d.town))]
|
||||
)
|
||||
|
||||
const townCounts = computed(() => {
|
||||
const counts: Record<string, number> = {}
|
||||
mapDeliveries.value.forEach(d => {
|
||||
const town = d.town || 'Unknown'
|
||||
counts[town] = (counts[town] || 0) + 1
|
||||
})
|
||||
return Object.entries(counts)
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
})
|
||||
|
||||
const mapCenter = computed<[number, number]>(() => {
|
||||
if (mappedDeliveries.value.length === 0) {
|
||||
return [42.0654, -71.8984] // Worcester, MA area
|
||||
}
|
||||
const lats = mappedDeliveries.value.map(d => parseFloat(d.latitude!))
|
||||
const lngs = mappedDeliveries.value.map(d => parseFloat(d.longitude!))
|
||||
const avgLat = lats.reduce((a, b) => a + b, 0) / lats.length
|
||||
const avgLng = lngs.reduce((a, b) => a + b, 0) / lngs.length
|
||||
return [avgLat, avgLng]
|
||||
})
|
||||
|
||||
const chartData = computed(() => {
|
||||
if (weeklyData.value.length === 0) return null
|
||||
|
||||
return {
|
||||
labels: weeklyData.value.map(d => {
|
||||
const date = new Date(d.date)
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||
}),
|
||||
datasets: [{
|
||||
label: 'Gallons',
|
||||
data: weeklyData.value.map(d => d.gallons),
|
||||
borderColor: 'rgb(59, 130, 246)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context: { parsed: { y: number | null } }) => {
|
||||
const value = context.parsed?.y ?? 0
|
||||
return `${value.toLocaleString()} gallons`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: (value: string | number) => {
|
||||
if (typeof value === 'number') {
|
||||
return value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
const formatNumber = (num: number) => {
|
||||
if (num >= 1000) {
|
||||
return num.toLocaleString()
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
userStatus()
|
||||
today_delivery_count()
|
||||
today_delivery_delivered()
|
||||
today_price_oil()
|
||||
totalgallonsweek()
|
||||
totalprofitweek()
|
||||
fetchMapDeliveries()
|
||||
fetchWeeklyChartData()
|
||||
})
|
||||
|
||||
// Functions
|
||||
// API Functions
|
||||
const userStatus = () => {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
||||
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami'
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
@@ -227,44 +439,17 @@ const userStatus = () => {
|
||||
})
|
||||
.then((response: any) => {
|
||||
if (response.data.ok) {
|
||||
user.value = response.data.user;
|
||||
user.value = response.data.user
|
||||
employeeStatus()
|
||||
} else {
|
||||
localStorage.removeItem('user');
|
||||
router.push('/login');
|
||||
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;
|
||||
const path = import.meta.env.VITE_BASE_URL + '/employee/userid/' + user.value.user_id
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
@@ -272,26 +457,12 @@ const employeeStatus = () => {
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
employee.value = response.data?.employee || 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;
|
||||
employee.value = response.data?.employee || response.data
|
||||
})
|
||||
}
|
||||
|
||||
const today_delivery_count = () => {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/today'
|
||||
const path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/today'
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
@@ -299,12 +470,12 @@ const today_delivery_count = () => {
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
delivery_count.value = response.data.data;
|
||||
delivery_count.value = response.data.data
|
||||
})
|
||||
}
|
||||
|
||||
const today_delivery_delivered = () => {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/delivered/today'
|
||||
const path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/delivered/today'
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
@@ -312,13 +483,12 @@ const today_delivery_delivered = () => {
|
||||
headers: authHeader(),
|
||||
})
|
||||
.then((response: any) => {
|
||||
console.log(response.data)
|
||||
delivery_count_delivered.value = response.data.data;
|
||||
delivery_count_delivered.value = response.data.data
|
||||
})
|
||||
}
|
||||
|
||||
const today_price_oil = () => {
|
||||
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
|
||||
const totalgallonsweek = () => {
|
||||
const path = import.meta.env.VITE_BASE_URL + '/stats/gallons/week'
|
||||
axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
@@ -326,12 +496,110 @@ const today_price_oil = () => {
|
||||
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;
|
||||
total_gallons_past_week.value = response.data.total
|
||||
})
|
||||
}
|
||||
|
||||
const totalprofitweek = () => {
|
||||
const 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 fetchMapDeliveries = async () => {
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const response = await deliveryService.getForMap(today)
|
||||
if (response.data.ok) {
|
||||
mapDeliveries.value = response.data.deliveries || []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching map deliveries:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchWeeklyChartData = async () => {
|
||||
try {
|
||||
// Get last 28 days of data
|
||||
const endDate = new Date()
|
||||
const startDate = new Date()
|
||||
startDate.setDate(startDate.getDate() - 28)
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
const queryParams = new URLSearchParams({
|
||||
start_date: startDate.toISOString().split('T')[0],
|
||||
end_date: endDate.toISOString().split('T')[0],
|
||||
years: currentYear.toString()
|
||||
})
|
||||
|
||||
const path = import.meta.env.VITE_BASE_URL + `/stats/gallons/daily?${queryParams.toString()}`
|
||||
const response = await axios({
|
||||
method: "get",
|
||||
url: path,
|
||||
withCredentials: true,
|
||||
headers: authHeader(),
|
||||
})
|
||||
|
||||
if (response.data.ok && response.data.years?.length > 0) {
|
||||
weeklyData.value = response.data.years[0].data.map((d: { date: string; gallons: number }) => ({
|
||||
date: d.date,
|
||||
gallons: d.gallons
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching chart data:', err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@apply bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-4 shadow-md hover:shadow-lg transition-all duration-300;
|
||||
}
|
||||
|
||||
.card-glass {
|
||||
@apply bg-gradient-to-br from-neutral/90 to-neutral/70 backdrop-blur-sm rounded-xl shadow-lg border border-base-content/5;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
@apply flex flex-col items-center justify-center gap-1 p-3 rounded-lg bg-base-200/50 hover:bg-primary/10 hover:text-primary transition-all duration-200 text-xs font-medium;
|
||||
}
|
||||
|
||||
/* Leaflet fixes */
|
||||
:deep(.leaflet-container) {
|
||||
z-index: 0;
|
||||
background: hsl(var(--b3));
|
||||
}
|
||||
|
||||
:deep(.leaflet-popup-content-wrapper) {
|
||||
background: hsl(var(--b1));
|
||||
color: hsl(var(--bc));
|
||||
}
|
||||
|
||||
:deep(.leaflet-popup-tip) {
|
||||
background: hsl(var(--b1));
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user