358 lines
11 KiB
Vue
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> |