101 lines
3.9 KiB
Vue
101 lines
3.9 KiB
Vue
<script setup>
|
|
import { ref, onMounted, computed } from 'vue';
|
|
import axios from 'axios';
|
|
import { usePageTitle } from '../composables/Core/usePageTitle';
|
|
import { useNavigate } from '../composables/Core/useNavigate';
|
|
import LoadingSpinner from '../Components/LoadingSpinner.vue';
|
|
import TransactionListSkeleton from '../Components/Core/Skeleton/TransactionListSkeleton.vue';
|
|
import SearchBar from '../Components/Core/Search/SearchBar.vue';
|
|
|
|
usePageTitle('Shipment Tracking');
|
|
const { navigate } = useNavigate();
|
|
|
|
const shipments = ref([]);
|
|
const loading = ref(true);
|
|
const searchQuery = ref('');
|
|
|
|
const fetchShipments = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const response = await axios.post('/Shipments/List');
|
|
if (response.data.success) {
|
|
shipments.value = response.data.data;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch shipments:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const getStatusBadgeClass = (status) => {
|
|
switch (status) {
|
|
case 'DELIVERED': return 'badge bg-success';
|
|
case 'IN_TRANSIT': return 'badge bg-primary';
|
|
case 'PENDING': return 'badge bg-warning text-dark';
|
|
case 'FAILED': return 'badge bg-danger';
|
|
default: return 'badge bg-secondary';
|
|
}
|
|
};
|
|
|
|
const filteredShipments = computed(() => {
|
|
if (!searchQuery.value) return shipments.value;
|
|
const q = searchQuery.value.toLowerCase();
|
|
return shipments.value.filter(s =>
|
|
s.tracking_number?.toLowerCase().includes(q) ||
|
|
s.customer?.name?.toLowerCase().includes(q) ||
|
|
s.status?.toLowerCase().includes(q)
|
|
);
|
|
});
|
|
|
|
onMounted(fetchShipments);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="shipment-list-page pb-5">
|
|
<div class="tf-container mt-4">
|
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
|
<h3 class="fw_6 mb-0">Shipments</h3>
|
|
<div class="badge bg-soft-primary px-3 py-2 rounded-pill text-primary">
|
|
{{ filteredShipments.length }} active
|
|
</div>
|
|
</div>
|
|
|
|
<SearchBar v-model="searchQuery" placeholder="Search by tracking or customer..." class="mb-4" />
|
|
|
|
<div v-if="loading" class="mt-2 text-center">
|
|
<TransactionListSkeleton :count="8" />
|
|
</div>
|
|
|
|
<div v-else-if="filteredShipments.length === 0" class="text-center py-5 border rounded-20 bg-light">
|
|
<i class="fas fa-box-open fa-3x text-muted mb-3 opacity-20"></i>
|
|
<h5>No shipments found</h5>
|
|
<p class="text-muted">Track your deliveries here</p>
|
|
</div>
|
|
|
|
<div v-else class="list-group list-group-flush rounded-20 overflow-hidden border shadow-sm">
|
|
<div v-for="shipment in filteredShipments" :key="shipment.hashkey"
|
|
class="list-group-item list-group-item-action p-4 border-bottom"
|
|
@click="navigate({ page: 'ShipmentDetail', props: { target: shipment.hashkey } })">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="small text-muted mb-1">#{{ shipment.tracking_number || 'No Tracking' }}</div>
|
|
<h6 class="mb-1">{{ shipment.customer?.name || 'Unknown Customer' }}</h6>
|
|
<div class="small text-muted">
|
|
<i class="fas fa-store me-1"></i> {{ shipment.store?.name || 'Direct Sale' }}
|
|
</div>
|
|
</div>
|
|
<span :class="getStatusBadgeClass(shipment.status)">{{ shipment.status }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.rounded-20 { border-radius: 20px; }
|
|
.bg-soft-primary { background-color: rgba(66, 185, 131, 0.1); }
|
|
.opacity-20 { opacity: 0.2; }
|
|
</style>
|