Kasir/resources/js/components/DetailPerNampan.vue
2025-09-08 14:29:17 +07:00

420 lines
14 KiB
Vue

<template>
<div class="my-6">
<hr class="border-B mb-5" />
<!-- Filter Section -->
<div class="flex flex-row my-3 overflow-x-auto gap-1 md:gap-5 lg:gap-8">
<div class="mb-3 w-full">
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label>
<input type="date" v-model="tanggalDipilih" id="pilihTanggal"
class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:outline-none focus:ring-D focus:ring-opacity-50 p-2" />
</div>
<div class="mb-3 w-full">
<label class="text-D/80" for="pilihSales">Filter Sales:</label>
<InputSelect :options="opsiSales" v-model="salesDipilih" id="pilihSales" />
</div>
<div class="mb-3 w-full">
<label class="text-D/80" for="pilihPelanggan">Nama Pembeli:</label>
<InputField placeholder="Nama pelanggan" v-model="namaPembeli" id="pilihPelanggan" />
</div>
<div class="mb-3 w-full">
<label class="text-D/80" for="pilihProduk">Filter Produk:</label>
<InputSelect :options="opsiProduk" v-model="produkDipilih" id="pilihProduk" />
</div>
</div>
<!-- Export Section -->
<div class="flex flex-row items-center justify-between mt-5 gap-3">
<!-- Summary Cards -->
<div class="flex gap-4" v-if="data?.rekap_harian">
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Item</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_item_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Berat</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_berat_terjual }}</div>
</div>
<div class="bg-A p-3 rounded-md border border-C">
<div class="text-xs text-D/60">Total Pendapatan</div>
<div class="font-semibold text-D">{{ data.rekap_harian.total_pendapatan }}</div>
</div>
</div>
<!-- Export Dropdown -->
<div class="relative w-40" ref="exportDropdownRef">
<button @click="isExportOpen = !isExportOpen" type="button"
class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none">
<span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span>
<i class="fas fa-chevron-down"></i>
</button>
<div v-if="isExportOpen" class="absolute z-10 w-full mt-1 bg-C border rounded-md shadow-lg border-C right-0">
<ul class="py-1">
<li v-for="option in exportOptions" :key="option.value" @click="selectExport(option)"
class="px-3 py-2 text-sm cursor-pointer text-D hover:bg-B">
{{ option.label }}
</li>
</ul>
</div>
</div>
</div>
<!-- Table Section -->
<div class="mt-5 overflow-x-auto">
<table class="w-full border-collapse border border-C rounded-md">
<thead>
<tr class="bg-C text-D rounded-t-md">
<th class="border-x border-C px-3 py-3">
<button @click="handleSort('nama_nampan')"
class="flex items-center justify-between w-full text-left hover:text-D/80 transition-colors">
<span>Nama Nampan</span>
<i :class="getSortIcon('nama_nampan')" class="ml-2"></i>
</button>
</th>
<th class="border-x border-C px-3 py-3">
<button @click="handleSort('jumlah_item_terjual')"
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
<span>Item Terjual</span>
<i :class="getSortIcon('jumlah_item_terjual')" class="ml-2"></i>
</button>
</th>
<th class="border-x border-C px-3 py-3">
<button @click="handleSort('berat_terjual')"
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
<span>Total Berat</span>
<i :class="getSortIcon('berat_terjual')" class="ml-2"></i>
</button>
</th>
<th class="border-x border-C px-3 py-3">
<button @click="handleSort('pendapatan')"
class="flex items-center justify-center w-full hover:text-D/80 transition-colors">
<span>Total Pendapatan</span>
<i :class="getSortIcon('pendapatan')" class="ml-2"></i>
</button>
</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="4" class="p-4">
<div class="flex items-center justify-center w-full h-30">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div>
<span class="ml-2 text-gray-600">Memuat data...</span>
</div>
</td>
</tr>
<tr v-else-if="!sortedNampan.length">
<td colspan="4" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td>
</tr>
<template v-else v-for="item in sortedNampan" :key="item.nama_nampan">
<tr class="text-center border-y border-C hover:bg-A">
<td class="border-x border-C px-3 py-2 text-left">{{ item.nama_nampan }}</td>
<td class="border-x border-C px-3 py-2">{{ item.jumlah_item_terjual }}</td>
<td class="border-x border-C px-3 py-2">{{ item.berat_terjual }}</td>
<td class="border-x border-C px-3 py-2">
<div class="flex justify-center">
<div :ref="el => { if (el) pendapatanElements.push(el) }"
:style="pendapatanStyle"
:class="item.pendapatan == '-' ? 'text-center' : 'text-right'">
{{ item.pendapatan }}
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
<!-- Pagination -->
<div v-if="pagination.total > 0 && pagination.last_page > 1" class="flex items-center justify-end gap-2 mt-4">
<button @click="goToPage(pagination.current_page - 1)"
:disabled="pagination.current_page === 1 || loading"
class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80">
Sebelumnya
</button>
<span class="text-sm text-D">
Halaman {{ pagination.current_page }} dari {{ pagination.last_page }}
</span>
<button @click="goToPage(pagination.current_page + 1)"
:disabled="(pagination.current_page === pagination.last_page) || loading"
class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80">
Berikutnya
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
import InputSelect from './InputSelect.vue';
import InputField from './InputField.vue';
import axios from 'axios';
// --- State ---
const isExportOpen = ref(false);
const exportDropdownRef = ref(null);
const exportOptions = ref([
{ value: 'pdf', label: 'Pdf' },
{ value: 'xls', label: 'Excel' },
{ value: 'csv', label: 'Csv' }
]);
const exportFormat = ref(null);
const tanggalDipilih = ref('');
const data = ref(null);
const loading = ref(false);
// Sorting state
const sortBy = ref(null);
const sortOrder = ref('asc'); // 'asc' or 'desc'
const pagination = ref({
current_page: 1,
last_page: 1,
total: 0,
});
const pendapatanWidth = ref(0);
const pendapatanElements = ref([]);
const salesDipilih = ref(null);
const opsiSales = ref([
{ label: 'Semua Sales', value: null, selected: true },
]);
const produkDipilih = ref(null);
const opsiProduk = ref([
{ label: 'Semua Produk', value: null, selected: true },
]);
const namaPembeli = ref(null);
// --- Computed ---
const nampan = computed(() => data.value?.nampan || []);
const sortedNampan = computed(() => {
if (!sortBy.value || !nampan.value.length) {
return nampan.value;
}
const sorted = [...nampan.value].sort((a, b) => {
let aValue = a[sortBy.value];
let bValue = b[sortBy.value];
// Handle different data types
if (sortBy.value === 'nama_nampan') {
// String comparison
aValue = aValue?.toString().toLowerCase() || '';
bValue = bValue?.toString().toLowerCase() || '';
} else if (sortBy.value === 'jumlah_item_terjual') {
// Numeric comparison
aValue = parseInt(aValue) || 0;
bValue = parseInt(bValue) || 0;
} else if (sortBy.value === 'berat_terjual') {
// Handle weight values (remove unit if exists)
aValue = parseFloat(aValue?.toString().replace(/[^\d.-]/g, '')) || 0;
bValue = parseFloat(bValue?.toString().replace(/[^\d.-]/g, '')) || 0;
} else if (sortBy.value === 'pendapatan') {
// Handle currency values (remove currency symbols and commas)
if (aValue === '-') aValue = 0;
if (bValue === '-') bValue = 0;
aValue = parseFloat(aValue?.toString().replace(/[^\d.-]/g, '')) || 0;
bValue = parseFloat(bValue?.toString().replace(/[^\d.-]/g, '')) || 0;
}
if (sortOrder.value === 'asc') {
if (typeof aValue === 'string') {
return aValue.localeCompare(bValue);
}
return aValue - bValue;
} else {
if (typeof aValue === 'string') {
return bValue.localeCompare(aValue);
}
return bValue - aValue;
}
});
return sorted;
});
const selectedExportLabel = computed(() => {
return exportOptions.value.find(opt => opt.value === exportFormat.value)?.label || 'Export Laporan';
});
const pendapatanStyle = computed(() => ({
minWidth: `${pendapatanWidth.value}px`,
padding: '0.5rem 0.75rem'
}));
// --- Watchers ---
watch(nampan, async (newValue) => {
if (newValue && newValue.length > 0) {
await nextTick();
pendapatanElements.value = [];
let maxWidth = 0;
await nextTick();
pendapatanElements.value.forEach(el => {
if (el && el.scrollWidth > maxWidth) {
maxWidth = el.scrollWidth;
}
});
pendapatanWidth.value = maxWidth;
}
}, { deep: true });
// --- Methods ---
const handleSort = (column) => {
if (sortBy.value === column) {
// If same column, toggle sort order
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
// If different column, set new column and default to ascending
sortBy.value = column;
sortOrder.value = 'asc';
}
};
const getSortIcon = (column) => {
if (sortBy.value !== column) {
return 'fas fa-sort text-D/40'; // Default sort icon
}
if (sortOrder.value === 'asc') {
return 'fas fa-sort-up text-D'; // Ascending
} else {
return 'fas fa-sort-down text-D'; // Descending
}
};
const fetchSales = async () => {
try {
const response = await axios.get('/api/sales', {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
const salesData = response.data;
opsiSales.value = [
{ label: 'Semua Sales', value: null },
...salesData.map(sales => ({
label: sales.nama,
value: sales.id,
}))
];
} catch (error) {
console.error('Gagal mengambil data sales:', error);
}
};
const fetchProduk = async () => {
try {
const response = await axios.get('/api/produk', {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
const produkData = response.data;
opsiProduk.value = [
{ label: 'Semua Produk', value: null },
...produkData.map(produk => ({
label: produk.nama,
value: produk.id,
}))
];
} catch (error) {
console.error('Gagal mengambil data produk:', error);
}
};
const fetchData = async (page = 1) => {
if (!tanggalDipilih.value) return;
loading.value = true;
pendapatanElements.value = [];
let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`;
if (salesDipilih.value) queryParams += `&sales_id=${salesDipilih.value}`;
if (produkDipilih.value) queryParams += `&produk_id=${produkDipilih.value}`;
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
try {
const response = await axios.get(`/api/detail-per-nampan?${queryParams}`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
}
});
data.value = response.data;
// Handle pagination data if provided by backend
if (response.data.pagination) {
pagination.value = {
current_page: response.data.pagination.current_page,
last_page: response.data.pagination.last_page,
total: response.data.pagination.total,
};
} else {
// Reset pagination if no pagination data
pagination.value = {
current_page: 1,
last_page: 1,
total: response.data.nampan ? response.data.nampan.length : 0,
};
}
} catch (error) {
console.error('Gagal mengambil data laporan nampan:', error);
data.value = null;
pagination.value = {
current_page: 1,
last_page: 1,
total: 0,
};
} finally {
loading.value = false;
}
};
const goToPage = (page) => {
if (page >= 1 && page <= pagination.value.last_page) {
pagination.value.current_page = page;
fetchData(page);
}
};
const selectExport = (option) => {
exportFormat.value = option.value;
isExportOpen.value = false;
alert(`Fitur Belum dikerjakan. Laporan akan diekspor dalam format ${option.label}`);
};
const closeDropdownsOnClickOutside = (event) => {
if (exportDropdownRef.value && !exportDropdownRef.value.contains(event.target)) {
isExportOpen.value = false;
}
};
// --- Lifecycle Hooks ---
onMounted(() => {
const today = new Date().toISOString().split('T')[0];
tanggalDipilih.value = today;
fetchSales();
fetchProduk();
document.addEventListener('click', closeDropdownsOnClickOutside);
});
onUnmounted(() => {
document.removeEventListener('click', closeDropdownsOnClickOutside);
});
// Watch for filter changes
watch([tanggalDipilih, salesDipilih, produkDipilih, namaPembeli], () => {
pagination.value.current_page = 1; // Reset to first page when filters change
fetchData(1);
}, { immediate: true });
</script>