initial: bootstrap from BukidBountyApp base
This commit is contained in:
358
resources/js/Pages/ManageGlobalTransactions.vue
Normal file
358
resources/js/Pages/ManageGlobalTransactions.vue
Normal file
@@ -0,0 +1,358 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user