440 lines
18 KiB
Vue
440 lines
18 KiB
Vue
<!-- src/pages/delivery/viewstatus/tommorrow.vue -->
|
|
<template>
|
|
<div class="flex">
|
|
<div class="w-full px-4 md:px-10 py-4">
|
|
<!-- Breadcrumbs & Title -->
|
|
<div class="text-sm breadcrumbs">
|
|
<ul>
|
|
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Main Content Card -->
|
|
<!-- Page Header with Stats -->
|
|
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mt-4 mb-6">
|
|
<div>
|
|
<h1 class="text-2xl md:text-3xl font-bold flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary/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-primary-content">
|
|
<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.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zM14.25 15h.008v.008H14.25V15zm0 2.25h.008v.008H14.25v-.008zM16.5 15h.008v.008H16.5V15zm0 2.25h.008v.008H16.5v-.008z" />
|
|
</svg>
|
|
</div>
|
|
Scheduled Deliveries
|
|
</h1>
|
|
<p class="text-base-content/60 mt-1 ml-13">Deliveries scheduled for tomorrow</p>
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="flex flex-wrap gap-3">
|
|
<div class="stat-pill">
|
|
<span class="stat-pill-value">{{ deliveries.length }}</span>
|
|
<span class="stat-pill-label">Deliveries</span>
|
|
</div>
|
|
<div class="stat-pill stat-pill-success">
|
|
<span class="stat-pill-value">{{ grand_total.toLocaleString() }}</span>
|
|
<span class="stat-pill-label">Total Gallons</span>
|
|
</div>
|
|
<div class="stat-pill stat-pill-info">
|
|
<span class="stat-pill-value">{{ totals.length }}</span>
|
|
<span class="stat-pill-label">Towns</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Town Breakdown Bar -->
|
|
<div v-if="totals.length > 0" class="mb-6 overflow-x-auto">
|
|
<div class="flex gap-2 pb-2">
|
|
<button
|
|
v-for="total in totals"
|
|
:key="total.town"
|
|
@click="toggleTownFilter(total.town)"
|
|
class="town-chip"
|
|
:class="{ 'town-chip-active': filterTown === total.town }"
|
|
>
|
|
<span class="font-semibold">{{ total.town }}</span>
|
|
<span class="town-chip-count">{{ total.gallons }}</span>
|
|
</button>
|
|
<button
|
|
v-if="filterTown"
|
|
@click="filterTown = ''"
|
|
class="town-chip town-chip-clear"
|
|
>
|
|
<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 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
Clear Filter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search & Actions Bar -->
|
|
<div class="flex flex-col sm:flex-row gap-3 mb-4">
|
|
<div class="relative flex-1 max-w-md">
|
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-base-content/40">
|
|
<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" />
|
|
</svg>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
v-model="searchQuery"
|
|
placeholder="Search by name, address, or delivery #..."
|
|
class="input input-bordered w-full pl-10 bg-base-200/50 focus:bg-base-100"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Table Card -->
|
|
<div class="modern-table-card">
|
|
|
|
|
|
|
|
<!-- DESKTOP VIEW: Table -->
|
|
<div class="hidden xl:block overflow-x-auto">
|
|
<table class="modern-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Delivery #</th>
|
|
<th>Name</th>
|
|
<th>Status</th>
|
|
<th>Town / Address</th>
|
|
<th>Gallons</th>
|
|
|
|
<th class="text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-for="oil in filteredDeliveries" :key="oil.id">
|
|
<tr v-if="oil.id" class="hover:bg-blue-600 hover:text-white">
|
|
<td>{{ oil.id }}</td>
|
|
<td>
|
|
<router-link v-if="oil.customer_id" :to="{ name: 'customerProfile', params: { id: oil.customer_id } }" class="link link-hover">
|
|
{{ oil.customer_name }}
|
|
</router-link>
|
|
<span v-else>{{ oil.customer_name }}</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-sm" :class="{
|
|
'badge-warning': oil.delivery_status == 0,
|
|
'badge-success': oil.delivery_status == 10,
|
|
'badge-info': oil.delivery_status == 2,
|
|
'badge-error': [1, 5].includes(oil.delivery_status),
|
|
}">
|
|
<span v-if="oil.delivery_status == 0">Waiting</span>
|
|
<span v-else-if="oil.delivery_status == 1">Cancelled</span>
|
|
<span v-else-if="oil.delivery_status == 2">Out_for_Delivery</span>
|
|
<span v-else-if="oil.delivery_status == 3">Tomorrow</span>
|
|
<span v-else-if="oil.delivery_status == 4">Partial</span>
|
|
<span v-else-if="oil.delivery_status == 5">Issue</span>
|
|
<span v-else-if="oil.delivery_status == 10">Finalized</span>
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div>{{ oil.customer_town }}</div>
|
|
<div class="text-xs opacity-70">{{ oil.customer_address }}</div>
|
|
</td>
|
|
<td>
|
|
<span v-if="oil.customer_asked_for_fill == 1" class="badge badge-info text-lg h-auto py-1">FILL</span>
|
|
<span v-else class="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg bg-success/10 border border-success/20 text-success font-mono text-lg font-bold shadow-sm">{{ oil.gallons_ordered }} gal</span>
|
|
</td>
|
|
<td>
|
|
<div class="flex flex-col gap-1">
|
|
<span v-if="oil.prime" class="badge badge-error badge-xs">PRIME</span>
|
|
<span v-if="oil.same_day" class="badge badge-error badge-xs">SAME DAY</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-right">
|
|
<div class="flex items-center justify-end gap-1">
|
|
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-xs btn-ghost">View</router-link>
|
|
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-xs btn-info btn-outline">Edit</router-link>
|
|
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-xs btn-accent btn-outline">Finalize</router-link>
|
|
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-xs btn-success btn-outline">Print</router-link>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- MOBILE VIEW: Cards -->
|
|
<div class="xl:hidden space-y-4">
|
|
<template v-for="oil in filteredDeliveries" :key="oil.id">
|
|
<div
|
|
v-if="oil.id"
|
|
class="mobile-card"
|
|
:class="{
|
|
'mobile-card-urgent': oil.emergency,
|
|
'mobile-card-prime': oil.prime && !oil.emergency,
|
|
'mobile-card-sameday': oil.same_day && !oil.prime && !oil.emergency
|
|
}"
|
|
>
|
|
<!-- Card content -->
|
|
<div class="p-3">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h2 class="text-base font-bold">{{ oil.customer_name }}</h2>
|
|
<p class="text-xs text-base-content/60">Delivery #{{ oil.id }}</p>
|
|
</div>
|
|
<div class="badge" :class="{
|
|
'badge-warning': oil.delivery_status == 0,
|
|
'badge-success': oil.delivery_status == 10,
|
|
'badge-info': oil.delivery_status == 2,
|
|
'badge-error': [1, 5].includes(oil.delivery_status),
|
|
}">
|
|
<span v-if="oil.delivery_status == 0">Waiting</span>
|
|
<span v-else-if="oil.delivery_status == 1">Cancelled</span>
|
|
<span v-else-if="oil.delivery_status == 2">Out_for_Delivery</span>
|
|
<span v-else-if="oil.delivery_status == 3">Tomorrow</span>
|
|
<span v-else-if="oil.delivery_status == 4">Partial</span>
|
|
<span v-else-if="oil.delivery_status == 5">Issue</span>
|
|
<span v-else-if="oil.delivery_status == 10">Finalized</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 mt-2">
|
|
<div v-if="oil.prime" class="badge badge-error badge-sm">PRIME</div>
|
|
<div v-if="oil.same_day" class="badge badge-error badge-sm">SAME DAY</div>
|
|
<div v-if="oil.emergency" class="badge badge-error badge-sm">EMERGENCY</div>
|
|
</div>
|
|
|
|
<div class="text-sm mt-3 grid grid-cols-2 gap-x-4 gap-y-2">
|
|
<div>
|
|
<p class="text-xs text-base-content/50">Address</p>
|
|
<p class="font-medium">{{ oil.customer_address }}</p>
|
|
<p class="text-xs">{{ oil.customer_town }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-base-content/50">Gallons</p>
|
|
<p class="font-bold text-lg text-success">
|
|
<span v-if="oil.customer_asked_for_fill" class="badge badge-info badge-xs">FILL</span>
|
|
<span v-else>{{ oil.gallons_ordered }}</span>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-base-content/50">Payment</p>
|
|
<span v-if="oil.payment_type == 0">Cash</span>
|
|
<span v-else-if="oil.payment_type == 1">CC</span>
|
|
<span v-else-if="oil.payment_type == 2">Cash/CC</span>
|
|
<span v-else-if="oil.payment_type == 3">Check</span>
|
|
<span v-else-if="oil.payment_type == 4">Other</span>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-base-content/50">Date</p>
|
|
<p class="font-medium">{{ oil.expected_delivery_date }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 pt-3 mt-3 border-t border-base-content/10">
|
|
<router-link :to="{ name: 'deliveryOrder', params: { id: oil.id } }" class="btn btn-sm btn-ghost flex-1">View</router-link>
|
|
<router-link :to="{ name: 'deliveryEdit', params: { id: oil.id } }" class="btn btn-sm btn-info btn-outline flex-1">Edit</router-link>
|
|
<router-link :to="{ name: 'finalizeTicket', params: { id: oil.id } }" v-if="oil.delivery_status != 10" class="btn btn-sm btn-accent btn-outline flex-1">Finalize</router-link>
|
|
<router-link :to="{ name: 'Ticket', params: { id: oil.id } }" class="btn btn-sm btn-success btn-outline">
|
|
<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="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" />
|
|
</svg>
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div class="mt-6 flex justify-center">
|
|
<pagination @paginate="getPage" :records="recordsLength" v-model="page" :per-page="50" :options="options">
|
|
</pagination>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, markRaw, computed } from 'vue'
|
|
import { deliveryService } from '../../../services/deliveryService'
|
|
import authService from '../../../services/authService'
|
|
import { printService } from '../../../services/printService'
|
|
import { Delivery } from '../../../types/models'
|
|
import Header from '../../../layouts/headers/headerauth.vue'
|
|
import PaginationComp from '../../../components/pagination.vue'
|
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
|
import { notify } from "@kyvg/vue3-notification";
|
|
|
|
interface TownTotal {
|
|
town: string;
|
|
gallons: number;
|
|
}
|
|
|
|
// Reactive data
|
|
const token = ref(null)
|
|
const user = ref(null)
|
|
const deliveries = ref<Delivery[]>([])
|
|
const totals = ref<TownTotal[]>([])
|
|
const grand_total = ref(0)
|
|
const page = ref(1)
|
|
const perPage = ref(50)
|
|
const recordsLength = ref(0)
|
|
const loading = ref(true)
|
|
const searchQuery = ref('')
|
|
const filterTown = ref('')
|
|
const options = ref({
|
|
edgeNavigation: false,
|
|
format: false,
|
|
template: markRaw(PaginationComp)
|
|
})
|
|
|
|
// Computed
|
|
const filteredDeliveries = computed(() => {
|
|
let result = [...deliveries.value]
|
|
|
|
// Filter by town
|
|
if (filterTown.value) {
|
|
result = result.filter(d => d.customer_town === filterTown.value)
|
|
}
|
|
|
|
// Filter by search
|
|
if (searchQuery.value) {
|
|
const query = searchQuery.value.toLowerCase()
|
|
result = result.filter(d =>
|
|
d.customer_name?.toLowerCase().includes(query) ||
|
|
d.customer_address?.toLowerCase().includes(query) ||
|
|
d.customer_town?.toLowerCase().includes(query) ||
|
|
d.id?.toString().includes(query)
|
|
)
|
|
}
|
|
return result
|
|
})
|
|
|
|
// Functions
|
|
const toggleTownFilter = (town: string) => {
|
|
filterTown.value = filterTown.value === town ? '' : town
|
|
}
|
|
const getPage = (pageVal: any) => {
|
|
deliveries.value = [];
|
|
loading.value = true
|
|
get_oil_orders(pageVal)
|
|
}
|
|
|
|
const userStatus = async () => {
|
|
try {
|
|
const response = await authService.whoami();
|
|
if (response.data.ok) {
|
|
user.value = response.data.user;
|
|
}
|
|
} catch (error) {
|
|
user.value = null;
|
|
}
|
|
}
|
|
|
|
const get_oil_orders = async (pageVal: number) => {
|
|
try {
|
|
const response = await deliveryService.getTomorrow(pageVal)
|
|
deliveries.value = response.data?.deliveries || []
|
|
} catch (error) {
|
|
console.error('Error fetching tomorrow deliveries:', error)
|
|
deliveries.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const get_totals = async () => {
|
|
try {
|
|
const response = await deliveryService.getTomorrowTotals();
|
|
totals.value = response.data.totals || [];
|
|
grand_total.value = response.data.grand_total || 0;
|
|
} catch (error) {
|
|
console.error('Error fetching totals:', error);
|
|
totals.value = []
|
|
grand_total.value = 0
|
|
}
|
|
}
|
|
|
|
const deleteCall = async (delivery_id: number) => {
|
|
try {
|
|
const response = await deliveryService.delete(delivery_id);
|
|
if (response.data.ok) {
|
|
notify({
|
|
title: "Success",
|
|
text: "deleted delivery",
|
|
type: "success",
|
|
});
|
|
getPage(page.value)
|
|
} else {
|
|
notify({
|
|
title: "Failure",
|
|
text: "error deleting delivery",
|
|
type: "success",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
notify({
|
|
title: "Failure",
|
|
text: "error deleting delivery",
|
|
type: "success",
|
|
});
|
|
}
|
|
}
|
|
|
|
const printtTicketAll = async () => {
|
|
try {
|
|
const response = await printService.printTomorrow();
|
|
if (response.data.ok) {
|
|
notify({
|
|
title: "Success",
|
|
text: "Sent to Printer",
|
|
type: "success",
|
|
});
|
|
getPage(page.value);
|
|
} else {
|
|
notify({
|
|
title: "Failure",
|
|
text: "error printing",
|
|
type: "success",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
notify({
|
|
title: "Failure",
|
|
text: "error printing",
|
|
type: "success",
|
|
});
|
|
}
|
|
}
|
|
|
|
const printTicket = async (delivery_id: number) => {
|
|
try {
|
|
const response = await printService.printTicket(delivery_id);
|
|
if (response.data.ok) {
|
|
notify({
|
|
title: "Success",
|
|
text: "Sent to Printer",
|
|
type: "success",
|
|
});
|
|
getPage(page.value)
|
|
} else {
|
|
notify({
|
|
title: "Failure",
|
|
text: "error printing",
|
|
type: "success",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
notify({
|
|
title: "Failure",
|
|
text: "error printing",
|
|
type: "success",
|
|
});
|
|
}
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
userStatus()
|
|
getPage(page.value)
|
|
get_totals()
|
|
})
|
|
</script>
|