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:
2026-02-03 20:57:11 -05:00
parent 61f93ec4e8
commit 9a4d5dd07b
3 changed files with 694 additions and 232 deletions

View File

@@ -15,11 +15,35 @@
<div class="">Spring Rebuilders - (508) 799-9342</div> <div class="">Spring Rebuilders - (508) 799-9342</div>
</nav> </nav>
<nav> <nav>
<h6 class="footer-title">Google Review link / qrcode</h6> <h6 class="footer-title">Search Shortcuts</h6>
<a class="link link-hover">https://g.page/r/CZHnPQ85LsMUEBM/review</a> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button @click="copyReviewLink" class="btn btn-outline btn-sm ml-2">Copy Link</button> <div class="flex items-center gap-2">
<h6 class="link link-hover"> <img src="../../assets/images/googlereview.png" alt="Company Logo" class="h-10 w-auto" /></h6> <kbd class="kbd kbd-sm bg-neutral text-neutral-content border-neutral-content/30">@</kbd>
<a class="link link-hover"></a> <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> </nav>
</footer> </footer>
</template> </template>
@@ -29,13 +53,45 @@
import { ref } from 'vue'; import { ref } from 'vue';
const copyReviewLink = async () => { 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 { try {
await navigator.clipboard.writeText('https://g.page/r/CZHnPQ85LsMUEBM/review') await navigator.clipboard.writeText(textToCopy);
alert('Link copied to clipboard!') alert('Link copied to clipboard!');
return;
} catch (err) { } catch (err) {
console.error('Failed to copy text: ', err) console.warn('Clipboard API failed, falling back to legacy method.', err);
alert('Failed to copy link. Please try again.')
} }
}
// 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> </script>
<style></style> <style></style>

View File

@@ -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" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg> </svg>
</label> </label>
<div class="flex flex-col items-center">
<span class="text-xs text-base-content/60">{{ dayOfWeek }}</span> <!-- Oil Price Dropdown (replaces date display) -->
<span class="normal-case text-xl font-bold"> <div class="dropdown dropdown-hover">
{{ currentDate }} <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>
<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>
</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"> <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> <li class="p-2 font-semibold">{{ user.user_name }}</li>
<div class="divider my-0"></div> <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> <li><router-link :to="{ name: 'changePassword' }">Change Password</router-link></li>
<div class="divider my-0"></div> <div class="divider my-0"></div>
<li class="menu-title text-xs opacity-60">Theme</li> <li class="menu-title text-xs opacity-60">Theme</li>
@@ -245,9 +332,27 @@ interface RoutingOption {
// Router // Router
const router = useRouter() 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 // Reactive data
const user = ref({} as User) const user = ref({} as User)
const currentPhone = ref('') 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([ const routingOptions = ref([
{ value: 'main', label: '407323' }, { value: 'main', label: '407323' },
{ value: 'sip', label: '407323_auburnoil' }, { value: 'sip', label: '407323_auburnoil' },
@@ -290,12 +395,45 @@ const dayOfWeek = computed((): string => {
return now.toLocaleDateString('en-US', { weekday: 'long' }); 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 // Lifecycle
onMounted(() => { onMounted(() => {
userStatus() userStatus()
updatestatus() 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 // Functions
const userStatus = async () => { const userStatus = async () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami'; let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';

View File

@@ -1,186 +1,293 @@
<!-- src/pages/Index.vue --> <!-- src/pages/Index.vue -->
<template> <template>
<div class="flex"> <div class="flex">
<div class="w-full px-4 md:px-10 "> <div class="w-full px-4 md:px-10 py-4">
<!-- Breadcrumbs & Welcome Header --> <!-- Welcome Header with Greeting -->
<div class="text-sm breadcrumbs"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<ul> <div>
<li><router-link :to="{ name: 'home' }">Home</router-link></li> <h1 class="text-3xl md:text-4xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
</ul> Welcome back, {{ employee.employee_first_name }}!
</div>
<h1 class="text-3xl font-bold mt-4">
Welcome, {{ employee.employee_first_name }}!
</h1> </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 --> <!-- 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 --> <!-- Left Column: Stats & Chart -->
<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="lg:col-span-8 space-y-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold">Today's Deliveries</h3> <!-- Stats Row: Quick Glance Cards -->
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<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"> <!-- 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" /> <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> </svg>
</div> </div>
</div> </div>
<div class="space-y-4"> <div class="text-3xl font-bold">{{ delivery_count }}</div>
<div class="flex items-baseline gap-2"> <div class="flex items-center gap-1 mt-1">
<span class="text-4xl font-bold">{{ delivery_count }}</span> <span class="text-success text-sm font-medium">{{ delivery_count_delivered }}</span>
<span class="text-base-content/60">total deliveries</span> <span class="text-base-content/50 text-xs">delivered</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> </div>
<progress class="progress progress-primary w-full h-2 mt-2" :value="delivery_count_delivered" :max="delivery_count || 1"></progress>
</div> </div>
<!-- Card 2: Today's Oil Price --> <!-- Week Gallons -->
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift"> <div class="stat-card group">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-2">
<h3 class="text-xl font-semibold">Oil Pricing</h3> <span class="text-xs uppercase tracking-wider text-base-content/50 font-semibold">This Week</span>
<div class="w-12 h-12 rounded-full bg-warning/10 flex items-center justify-center"> <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="1.5" stroke="currentColor" class="w-6 h-6 text-warning"> <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" /> <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> </svg>
</div> </div>
</div> </div>
<div class="space-y-3"> <div class="text-3xl font-bold text-success">${{ formatNumber(total_profit_past_week) }}</div>
<div class="flex justify-between items-center"> <div class="text-base-content/50 text-xs mt-1">this week</div>
<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> </div>
</div> </div>
<!-- Card 3: Service Pricing --> <!-- Weekly Trend Chart -->
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift"> <div class="card-glass p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold">Service Pricing</h3> <div>
<div class="w-12 h-12 rounded-full bg-info/10 flex items-center justify-center"> <h3 class="text-lg font-semibold">Weekly Delivery Trend</h3>
<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"> <p class="text-sm text-base-content/50">Gallons delivered over the past 4 weeks</p>
<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>
<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> </svg>
</router-link>
</div> </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>
<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> </div>
</div> </div>
<!-- Card 4: Search Shortcuts --> <!-- Right Column: Map & Quick Actions -->
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift"> <div class="lg:col-span-4 space-y-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold">Search Shortcuts</h3> <!-- Mini Delivery Map -->
<div class="w-12 h-12 rounded-full bg-accent/10 flex items-center justify-center"> <div class="card-glass p-4 overflow-hidden">
<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"> <div class="flex items-center justify-between mb-3">
<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" /> <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> </svg>
Today's Routes
</h3>
<router-link :to="{ name: 'deliveryMap' }" class="btn btn-xs btn-ghost">
Full Map
</router-link>
</div> </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>
<div class="space-y-3 text-sm">
<div class="flex items-center gap-3"> <!-- Map Container -->
<kbd class="kbd kbd-sm">@</kbd> <div class="rounded-lg overflow-hidden h-64 bg-base-300">
<span class="text-base-content/70">Last name</span> <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>
<div class="flex items-center gap-3"> </l-popup>
<kbd class="kbd kbd-sm">!</kbd> </l-marker>
<span class="text-base-content/70">Address</span> </l-map>
</div> <div v-else class="flex flex-col items-center justify-center h-full text-base-content/50">
<div class="flex items-center gap-3"> <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">
<kbd class="kbd kbd-sm">#</kbd> <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" />
<span class="text-base-content/70">Phone number</span> </svg>
</div> <p class="text-sm">No deliveries mapped</p>
<div class="flex items-center gap-3">
<kbd class="kbd kbd-sm">$</kbd>
<span class="text-base-content/70">Account number</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Quick Actions -->
<div class="card-glass p-4">
<!-- Card 5: This Week's Stats --> <h3 class="font-semibold mb-3 flex items-center gap-2">
<!-- <div class="bg-neutral rounded-lg p-5 xl:col-span-4"> <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">
<h3 class="text-xl font-bold mb-4">This Week's Stats</h3> <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" />
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-100 w-full"> </svg>
<div class="stat"> Quick Actions
<div class="stat-title">Total Deliveries</div> </h3>
<div class="stat-value">{{ total_deliveries }}</div> <div class="grid grid-cols-2 gap-2">
<div class="stat-desc">In the last 7 days</div> <router-link :to="{ name: 'customerCreate' }" class="quick-action-btn">
</div> <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">
<div class="stat"> <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" />
<div class="stat-title">Total Gallons</div> </svg>
<div class="stat-value">{{ total_gallons_past_week }}</div> <span>New Customer</span>
<div class="stat-desc">Delivered this week</div> </router-link>
</div> <router-link :to="{ name: 'delivery' }" class="quick-action-btn">
<div class="stat"> <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">
<div class="stat-title">Total Profit</div> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
<div class="stat-value text-success">${{ total_profit_past_week }}</div> </svg>
<div class="stat-desc">Estimated earnings</div> <span>Deliveries</span>
</div> </router-link>
</div> <router-link :to="{ name: 'deliveryOutForDelivery' }" class="quick-action-btn">
</div> --> <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" />
</div> </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>
</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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' 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 { deliveryService } from '../services/deliveryService'
import SideBar from '../layouts/sidebar/sidebar.vue' 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 // Register Chart.js components
const props = defineProps<{ ChartJS.register(
clickCount?: number CategoryScale,
}>() LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
)
// Router // Router
const router = useRouter() const router = useRouter()
// Reactive data // Reactive data
const token = ref(null)
const call_count = ref(0)
const delivery_count = ref(0) const delivery_count = ref(0)
const delivery_count_delivered = ref(0) const delivery_count_delivered = ref(0)
const price_from_supplier = ref(0) const total_gallons_past_week = ref(0)
const today_oil_price = ref(0) const total_profit_past_week = ref(0)
const price_for_employee = ref(0) const total_deliveries = ref(0)
const price_same_day = ref(0)
const price_prime = ref(0)
const price_emergency = ref(0)
const user = ref({ const user = ref({
user_id: 0, user_id: 0,
user_name: '', user_name: '',
@@ -201,24 +308,129 @@ const employee = ref({
employee_type: '', employee_type: '',
employee_state: '', employee_state: '',
}) })
const total_gallons_past_week = ref(0)
const total_profit_past_week = ref(0) // Map data
const total_deliveries = ref(0) const mapDeliveries = ref<DeliveryMapItem[]>([])
const loaded = ref(false) 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 // Lifecycle
onMounted(() => { onMounted(() => {
userStatus() userStatus()
today_delivery_count() today_delivery_count()
today_delivery_delivered() today_delivery_delivered()
today_price_oil()
totalgallonsweek() totalgallonsweek()
totalprofitweek() totalprofitweek()
fetchMapDeliveries()
fetchWeeklyChartData()
}) })
// Functions // API Functions
const userStatus = () => { const userStatus = () => {
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami'; const path = import.meta.env.VITE_BASE_URL + '/auth/whoami'
axios({ axios({
method: "get", method: "get",
url: path, url: path,
@@ -227,44 +439,17 @@ const userStatus = () => {
}) })
.then((response: any) => { .then((response: any) => {
if (response.data.ok) { if (response.data.ok) {
user.value = response.data.user; user.value = response.data.user
employeeStatus() employeeStatus()
} else { } else {
localStorage.removeItem('user'); localStorage.removeItem('user')
router.push('/login'); 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 = () => { 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({ axios({
method: "get", method: "get",
url: path, url: path,
@@ -272,26 +457,12 @@ const employeeStatus = () => {
headers: authHeader(), headers: authHeader(),
}) })
.then((response: any) => { .then((response: any) => {
employee.value = response.data?.employee || response.data; 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;
}) })
} }
const today_delivery_count = () => { 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({ axios({
method: "get", method: "get",
url: path, url: path,
@@ -299,12 +470,12 @@ const today_delivery_count = () => {
headers: authHeader(), headers: authHeader(),
}) })
.then((response: any) => { .then((response: any) => {
delivery_count.value = response.data.data; delivery_count.value = response.data.data
}) })
} }
const today_delivery_delivered = () => { 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({ axios({
method: "get", method: "get",
url: path, url: path,
@@ -312,13 +483,12 @@ const today_delivery_delivered = () => {
headers: authHeader(), headers: authHeader(),
}) })
.then((response: any) => { .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 = () => { const totalgallonsweek = () => {
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil' const path = import.meta.env.VITE_BASE_URL + '/stats/gallons/week'
axios({ axios({
method: "get", method: "get",
url: path, url: path,
@@ -326,12 +496,110 @@ const today_price_oil = () => {
headers: authHeader(), headers: authHeader(),
}) })
.then((response: any) => { .then((response: any) => {
price_from_supplier.value = response.data.price_from_supplier; total_gallons_past_week.value = response.data.total
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;
}) })
} }
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> </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>