239 lines
11 KiB
Vue
239 lines
11 KiB
Vue
<template>
|
|
<div class="flex flex-row items-center justify-end mt-5 gap-3">
|
|
<div class="relative w-32" ref="filterDropdownRef">
|
|
<button @click="isFilterOpen = !isFilterOpen" 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>{{ selectedFilterLabel }}</span>
|
|
<i class="fas fa-chevron-down"></i>
|
|
</button>
|
|
<div v-if="isFilterOpen" class="absolute z-10 w-full mt-1 bg-C border rounded-md shadow-lg border-C">
|
|
<ul class="py-1">
|
|
<li v-for="option in filterOptions" :key="option.value" @click="selectFilter(option)"
|
|
class="px-3 py-2 text-sm cursor-pointer text-D hover:bg-B">
|
|
{{ option.label }}
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
<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>
|
|
|
|
<div class="mt-5">
|
|
<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">Tanggal</th>
|
|
<th class="border-x border-C px-3 py-3">Nama Sales</th>
|
|
<th class="border-x border-C px-3 py-3">Jumlah Item Terjual</th>
|
|
<th class="border-x border-C px-3 py-3">Total Berat Terjual</th>
|
|
<th class="border-x border-C px-3 py-3">Total Pendapatan</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="loading">
|
|
<td colspan="5" 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="!ringkasanLaporan.length">
|
|
<td colspan="5" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td>
|
|
</tr>
|
|
<template v-else v-for="item in ringkasanLaporan" :key="item.tanggal">
|
|
<template v-if="item.sales && item.sales.length > 0">
|
|
<tr class="text-center border-y border-C hover:bg-A">
|
|
<td class="px-3 py-2 border-x border-C bg-white" :rowspan="item.sales.length">{{
|
|
item.tanggal }}</td>
|
|
<td class="px-3 py-2 border-x border-C text-left">{{ item.sales[0].nama }}</td>
|
|
<td class="px-3 py-2 border-x border-C">{{ item.sales[0].item_terjual }}</td>
|
|
<td class="px-3 py-2 border-x border-C">{{ item.sales[0].berat_terjual }}</td>
|
|
<td class="flex justify-center">
|
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="item.sales[0].pendapatan == '-' ? 'text-center' : 'text-right'">
|
|
{{ item.sales[0].pendapatan }}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr v-for="sales in item.sales.slice(1)" :key="sales.nama"
|
|
class="text-center border-y border-C hover:bg-A">
|
|
<td class="px-3 py-2 text-left border-x border-C">{{ sales.nama }}</td>
|
|
<td class="px-3 py-2 border-x border-C">{{ sales.item_terjual }}</td>
|
|
<td class="px-3 py-2 border-x border-C">{{ sales.berat_terjual }}</td>
|
|
<td class="flex justify-center">
|
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="sales.pendapatan == '-' ? 'text-center' : 'text-right'">
|
|
{{ sales.pendapatan }}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr class="font-semibold text-center border-y border-C bg-B hover:bg-C/80">
|
|
<td class="px-3 py-2 border-x border-C" colspan="2">Total</td>
|
|
<td class="px-3 py-2 border-x border-C">{{ item.total_item_terjual }}</td>
|
|
<td class="px-3 py-2 border-x border-C">{{ item.total_berat }}</td>
|
|
<td class="flex justify-center">
|
|
<div :ref="el => { if (el) pendapatanElements.push(el) }" :style="pendapatanStyle" :class="item.sales[0].pendapatan == '-' ? 'text-center' : 'text-right'">
|
|
{{ item.total_pendapatan }}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
<template v-else>
|
|
<tr class="text-center border-y border-C hover:bg-A">
|
|
<td class="px-3 py-2 border-x border-C">{{ item.tanggal }}</td>
|
|
<td colspan="4" class="px-3 py-2 italic text-gray-500 border-x border-C">Tidak ada transaksi
|
|
pada hari ini</td>
|
|
</tr>
|
|
</template>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<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>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from "vue";
|
|
import axios from "axios";
|
|
|
|
// --- State ---
|
|
const isFilterOpen = ref(false);
|
|
const isExportOpen = ref(false);
|
|
const filterDropdownRef = ref(null);
|
|
const exportDropdownRef = ref(null);
|
|
|
|
const filterOptions = ref([
|
|
{ value: 'bulan', label: 'Bulanan' },
|
|
{ value: 'hari', label: 'Harian' }
|
|
]);
|
|
const exportOptions = ref([
|
|
{ value: 'pdf', label: 'Pdf' },
|
|
{ value: 'xls', label: 'Excel' },
|
|
{ value: 'csv', label: 'Csv' }
|
|
]);
|
|
|
|
const filterRingkasan = ref("bulan");
|
|
const exportFormat = ref(null);
|
|
const ringkasanLaporan = ref([]);
|
|
const loading = ref(false);
|
|
const pagination = ref({
|
|
current_page: 1,
|
|
last_page: 1,
|
|
total: 0,
|
|
});
|
|
|
|
const pendapatanWidth = ref(0);
|
|
const pendapatanElements = ref([]);
|
|
|
|
// --- Computed ---
|
|
const selectedFilterLabel = computed(() => {
|
|
return filterOptions.value.find(opt => opt.value === filterRingkasan.value)?.label;
|
|
});
|
|
|
|
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(ringkasanLaporan, async (newValue) => {
|
|
if (newValue && newValue.length > 0) {
|
|
await nextTick();
|
|
let maxWidth = 0;
|
|
pendapatanElements.value.forEach(el => {
|
|
if (el && el.scrollWidth > maxWidth) {
|
|
maxWidth = el.scrollWidth;
|
|
}
|
|
});
|
|
pendapatanWidth.value = maxWidth;
|
|
}
|
|
}, { deep: true });
|
|
|
|
// --- Methods ---
|
|
const fetchRingkasan = async (page = 1) => {
|
|
loading.value = true;
|
|
pendapatanElements.value = [];
|
|
try {
|
|
const response = await axios.get(`/api/laporan?filter=${filterRingkasan.value}&page=${page}`);
|
|
ringkasanLaporan.value = response.data.data;
|
|
pagination.value = {
|
|
current_page: response.data.current_page,
|
|
last_page: response.data.last_page,
|
|
total: response.data.total,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error fetching laporan:", error);
|
|
ringkasanLaporan.value = [];
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const goToPage = (page) => {
|
|
if (page >= 1 && page <= pagination.value.last_page) {
|
|
fetchRingkasan(page);
|
|
}
|
|
};
|
|
|
|
const selectFilter = (option) => {
|
|
filterRingkasan.value = option.value;
|
|
isFilterOpen.value = false;
|
|
goToPage(1);
|
|
};
|
|
|
|
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 (filterDropdownRef.value && !filterDropdownRef.value.contains(event.target)) {
|
|
isFilterOpen.value = false;
|
|
}
|
|
if (exportDropdownRef.value && !exportDropdownRef.value.contains(event.target)) {
|
|
isExportOpen.value = false;
|
|
}
|
|
};
|
|
|
|
// --- Lifecycle Hooks ---
|
|
onMounted(() => {
|
|
fetchRingkasan(pagination.value.current_page);
|
|
document.addEventListener('click', closeDropdownsOnClickOutside);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
|
});
|
|
</script> |