Files
BarangaySystem/resources/js/Pages/ManageGlobalTransactions.vue
2026-06-06 18:43:00 +08:00

358 lines
11 KiB
Vue

<script setup>
import { usePageTitle } from '../composables/Core/usePageTitle';
usePageTitle('Manage Global Transactions');
import { ref, onMounted, computed, watch } from 'vue'
import { useNavigate } from '../composables/Core/useNavigate'
import FileImage from '../Components/Core/FileImage.vue'
import { useGlobalTransactions } from '../composables/useGlobalTransactions'
import { useUserSettings } from '../composables/useUserSettings'
import SkeletonTable from '../Components/Core/Skeleton/SkeletonTable.vue'
import TableDensityToggle from '../Components/Core/TableDensityToggle.vue'
import SearchBar from '../Components/Core/Search/SearchBar.vue'
import axios from 'axios'
const { navigate } = useNavigate()
const { settings, updateSetting } = useUserSettings();
import BackButton from '../Components/Core/BackButton.vue'
// Route parameters
const urlParams = new URLSearchParams(window.location.search)
const targetHash = urlParams.get('target') || urlParams.get('product_id')
const { transactions, isLoading, fetchTransactions } = useGlobalTransactions(
targetHash ? { product_id: targetHash } : {}
)
// Data state for product (only if in product context)
const productData = ref({})
// Initialize component
onMounted(async () => {
document.title = targetHash ? 'Product Transactions' : 'Global Transactions'
await fetchTransactions()
if (targetHash) {
loadProductData()
}
})
// Load product data if in product context
const loadProductData = async () => {
try {
// Get product details
const productResponse = await axios.post('/View/Product/Details/data', {
target: 'product_id',
data: { product_id: targetHash }
})
if (productResponse.data && productResponse.data.success && productResponse.data.data) {
productData.value = productResponse.data.data
}
} catch (error) {
console.error('Error loading product data:', error)
}
}
// Format currency
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-PH', {
style: 'currency',
currency: 'PHP'
}).format(amount)
}
// Format date
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
// Navigation helpers
const goToManageProduct = () => {
if (targetHash) {
navigate({ page: 'ManageProductAdmin', query: { target: targetHash } })
} else {
navigate({ page: 'ManageProductsAdmin' })
}
}
const goToHome = () => {
navigate({ page: 'Home' })
}
const goToStoreTransactions = () => {
if (productData.value.store_id) {
navigate({
page: 'ManageStoreTransactions',
query: { store_hash: productData.value.store_id }
})
}
}
// Computed for page header
const pageHeader = computed(() => {
if (targetHash && productData.value.name) {
return `Transactions for ${productData.value.name}`
}
return 'Global Transaction History'
})
const tableDensity = computed({
get: () => settings.value.table_density || 'comfortable',
set: (val) => updateSetting('table_density', val)
});
const searchQuery = ref('')
const filteredTransactions = computed(() => {
if (!searchQuery.value) return transactions.value
const q = searchQuery.value.toLowerCase()
return transactions.value.filter(tx => {
const searchText = [
tx.type,
tx.status,
tx.flow?.label,
formatDate(tx.created_at),
tx.amount?.toString()
].filter(Boolean).join(' ').toLowerCase()
return searchText.includes(q)
})
})
const tableDensityClass = computed(() => {
return {
'density-comfortable': tableDensity.value === 'comfortable',
'density-compact': tableDensity.value === 'compact',
'density-ultra': tableDensity.value === 'ultra-compact'
}
})
</script>
<template>
<div class="manage-transactions-page pb-5">
<br><br>
<div class="tf-container">
<!-- Back Button & Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<BackButton
:to="targetHash ? { page: 'ManageProductAdmin', query: { target: targetHash } } : { page: 'Home' }"
/>
<h4 v-if="!productData.name" class="fw_6 mb-0">{{ pageHeader }}</h4>
<button
v-if="!targetHash"
@click="navigate({ page: 'AddTransaction' })"
class="btn btn-primary btn-sm rounded-pill px-3 shadow-sm"
>
<i class="fas fa-plus me-1"></i> Record Transaction
</button>
</div>
<!-- Product Header Card -->
<div v-if="productData.name" class="card shadow-sm mb-4">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-3 mb-3 mb-md-0">
<div class="product-header-photo">
<FileImage
v-if="productData.photourl"
:src="productData.photourl"
class="img-fluid rounded"
fallback="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/146710fe9ece.bin"
/>
<img v-else src="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/146710fe9ece.bin" class="img-fluid rounded" />
</div>
</div>
<div class="col-md-9">
<h3 class="fw_6 mb-1">{{ productData.name }}</h3>
<p class="mb-1 text-muted">
Price: <strong>{{ formatCurrency(productData.price) }} / {{ productData.unitname || 'unit' }}</strong>
</p>
<div class="d-flex gap-2 mt-2">
<!-- Manage Product Button -->
<button
@click="goToManageProduct"
class="btn btn-sm btn-primary"
>
<i class="fas fa-briefcase"></i> Manage Product
</button>
<!-- Store Transactions Button (if applicable) -->
<button
v-if="productData.is_from_store"
@click="goToStoreTransactions"
class="btn btn-sm btn-outline-info"
>
<i class="fas fa-receipt"></i> Store Transactions
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading && transactions.length === 0">
<SkeletonTable :rows="10" :columns="5" />
</div>
<!-- Empty State -->
<div v-else-if="filteredTransactions.length === 0 && !searchQuery" class="card shadow-sm">
<div class="card-body text-center py-5">
<i class="fas fa-receipt fa-4x text-muted mb-3"></i>
<h5>No Transactions Found</h5>
<p class="text-muted">This product has no transaction history yet.</p>
</div>
</div>
<!-- Transactions Card -->
<div class="toolbar-row d-flex align-items-center gap-3 mb-4">
<div class="search-container flex-grow-1">
<SearchBar
v-model="searchQuery"
placeholder="Search transactions by type, status, flow, date, amount..."
/>
</div>
<div class="density-container flex-shrink-0">
<TableDensityToggle v-model="tableDensity" />
</div>
</div>
<div v-if="filteredTransactions.length === 0 && searchQuery" class="card shadow-sm border-0 rounded-20">
<div class="card-body text-center py-5">
<i class="fas fa-search fa-4x text-muted mb-3"></i>
<h5>No Results Found</h5>
<p class="text-muted">No transactions match your search criteria.</p>
<button @click="searchQuery = ''" class="btn btn-outline-primary mt-2">Clear Search</button>
</div>
</div>
<div v-else class="card shadow-sm border-0 rounded-20" :data-table-density="tableDensity">
<div class="card-header bg-transparent border-0 pt-4 px-4 d-flex justify-content-between align-items-center">
<h5 class="fw_7 mb-0">{{ pageHeader }}</h5>
<span class="text-muted small">{{ filteredTransactions.length }} transaction(s)</span>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle density-table mb-0" :class="tableDensityClass">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Amount</th>
<th>Type</th>
<th>Flow</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="transaction in filteredTransactions" :key="transaction.id">
<td>{{ formatDate(transaction.created_at) }}</td>
<td class="fw_6">{{ formatCurrency(transaction.amount) }}</td>
<td><span class="badge bg-info">{{ transaction.type }}</span></td>
<td>
<span v-if="transaction.flow" class="badge" :class="{
'bg-success': transaction.flow.value === 1,
'bg-danger': transaction.flow.value === -1,
'bg-secondary': transaction.flow.value === 0
}">
{{ transaction.flow.label }}
</span>
</td>
<td><span class="badge bg-success">{{ transaction.status }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.card {
border-radius: 12px;
}
.card-header {
background-color: #f8f9fa;
}
.table th {
font-weight: 600;
}
.badge {
padding: 4px 8px;
font-size: 0.75rem;
border-radius: 4px;
}
.toolbar-row {
flex-wrap: nowrap;
}
.search-container {
min-width: 0;
flex: 1;
max-width: 50%;
}
.density-container {
flex: 1;
max-width: 50%;
}
.density-table :deep(thead th) {
transition: padding 0.2s ease;
}
.density-table :deep(tbody td) {
transition: padding 0.2s ease;
}
.density-comfortable :deep(thead th) {
padding: 1rem;
}
.density-comfortable :deep(tbody td) {
padding: 1rem;
}
.density-compact :deep(thead th) {
padding: 0.625rem 0.75rem;
}
.density-compact :deep(tbody td) {
padding: 0.5rem 0.75rem;
}
.density-ultra :deep(thead th) {
padding: 0.375rem 0.5rem;
font-size: 0.7rem;
}
.density-ultra :deep(tbody td) {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.density-ultra :deep(.small) {
font-size: 0.75rem;
}
.density-ultra :deep(.smallest) {
font-size: 0.65rem;
}
@media (max-width: 768px) {
.toolbar-row {
flex-wrap: wrap;
}
.search-container {
max-width: 100%;
width: 100%;
margin-bottom: 0.5rem;
}
.density-container {
width: 100%;
}
}
</style>