Files
mbmanager-dashboard/src/views/pages/Transaction/ListTransactions.vue
T
2026-08-30 11:25:31 +03:30

399 lines
15 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { FilterMatchMode } from '@primevue/core/api';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref, watch } from 'vue';
import { TransactionService } from '@/service/TransactionService';
import { SettingService } from '@/service/SettingService';
import { useRouter, useRoute } from 'vue-router';
import Chip from 'primevue/chip';
import Tag from 'primevue/tag';
import InputIcon from 'primevue/inputicon';
import IconField from 'primevue/iconfield';
import LoadingCheck from '@/components/LoadingCheck.vue';
import Popover from 'primevue/popover';
const searchMethod = ref(null);
const loadSearchSetting = async () => {
try {
const setting = await SettingService.getSearchSettingByPage('transactions');
searchMethod.value = setting?.searchMethod || 'typesense';
} catch (e) {
console.warn('Failed to load search setting for transactions, defaulting to typesense', e);
searchMethod.value = 'typesense';
}
};
onMounted(async () => {
await loadSearchSetting();
loadData();
});
const toast = useToast();
const dt = ref();
const transactions = ref();
const transactionDialog = ref(false);
const deleteTransactionDialog = ref(false);
const deleteTransactionsDialog = ref(false);
const transaction = ref({});
const selectedTransactions = ref();
const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
});
const submitted = ref(false);
function hideDialog() {
transactionDialog.value = false;
submitted.value = false;
}
const router = useRouter();
function editTransaction(transactionId) {
router.push('/transaction/edit-transaction/' + transactionId);
}
const route = useRoute();
const searchPhrase = ref(route.query.query);
const page = ref(Number(route.query.page) || 1);
const per_page = ref(Number(route.query.per_page) || 20);
const totalCount = ref(0);
const pagesCount = ref(0);
let searchTimeout;
// Update your search watcher
watch(searchPhrase, (newPhrase, oldPhrase) => {
if (newPhrase !== oldPhrase) {
// Debounce search to avoid too many requests
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
router.push({
query: {
...route.query,
query: newPhrase || undefined,
page: 1 // Reset to first page on new search
}
});
}, 300);
}
});
watch(
() => route.query,
() => {
page.value = Number(route.query.page) || 1;
per_page.value = Number(route.query.per_page) || 20;
searchPhrase.value = route.query.query || '';
loadData();
},
{ deep: true }
);
const handleRemoveSearch = () => {
removeFromQuery('query');
searchPhrase.value = '';
};
const removeFromQuery = (queryToRemove) => {
const newQuery = { ...route.query };
delete newQuery[queryToRemove];
router.replace({ query: newQuery }).catch((err) => {
if (err.name !== 'NavigationDuplicated') {
console.error(err);
}
});
};
const onPageChange = (event) => {
const newPage = event.page + 1;
const newPerPage = event.rows;
if (page.value !== newPage || per_page.value !== newPerPage) {
router.push({
query: {
...route.query,
page: newPage,
per_page: newPerPage
}
});
}
};
const dataLoading = ref(true);
const loadData = async () => {
dataLoading.value = true;
try {
if (searchMethod.value === null) {
await loadSearchSetting();
}
let response;
if (searchMethod.value === 'mongo') {
response = await TransactionService.searchTransactionsMongo(route.query);
} else {
response = await TransactionService.searchTransactions(route.query);
}
transactions.value = response.transactions || response;
totalCount.value = response.totalCount || response.length || 0;
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
} catch (error) {
toast.add({ severity: 'error', summary: 'Error', detail: 'خطا در دریافت اطلاعات', life: 3000 });
console.error('Error loading data:', error);
} finally {
dataLoading.value = false;
}
};
const loading = ref(false);
const indexingFinished = ref(false);
const indexingMessage = ref('در حال ایندکس...');
const animationKey = ref(0);
// Function to handle indexing
const indexTransactions = async () => {
loading.value = true;
indexingFinished.value = false;
try {
// Your indexing logic here
await TransactionService.indexTransactions();
// When indexing completes
TransactionService.searchTransactions(route.query)
.then((response) => {
console.log('response', response);
transactions.value = response.transactions || response;
totalCount.value = response.totalCount || response.length || 0;
pagesCount.value = Math.ceil(totalCount.value / per_page.value);
})
.catch((error) => {
toast.add({ severity: 'error', summary: 'Error', detail: 'خطا در دریافت اطلاعات', life: 3000 });
console.error('Error loading data:', error);
});
indexingFinished.value = true;
indexingMessage.value = 'ایندکس با موفقیت انجام شد';
} catch (error) {
indexingMessage.value = 'خطا در ایندکس';
} finally {
// Reset after some time
setTimeout(() => {
loading.value = false;
animationKey.value++; // Force re-render of animation
}, 3000);
}
};
const formatDate = (timestamp) => {
const date = new Date(timestamp * 1000);
// Format using Intl.DateTimeFormat
return new Intl.DateTimeFormat('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
// Helper functions for translations and formatting
const getTypeTranslation = (type) => {
switch (type) {
case 'deposit':
return 'واریز';
case 'withdraw':
return 'برداشت';
default:
return type;
}
};
const getTypeSeverity = (type) => {
switch (type) {
case 'deposit':
return 'success';
case 'withdraw':
return 'danger';
default:
return null;
}
};
const getStatusTranslation = (status) => {
switch (status) {
case 'failed':
return 'ناموفق';
case 'pending':
return 'در انتظار';
case 'finished':
return 'تکمیل شده';
default:
return status;
}
};
const getStatusSeverity = (status) => {
switch (status) {
case 'failed':
return 'danger';
case 'pending':
return 'warn';
case 'finished':
return 'success';
default:
return null;
}
};
const op = ref();
const activeDescription = ref('');
const toggle = (event, data) => {
activeDescription.value = data;
op.value.toggle(event);
};
</script>
<template>
<div>
<div class="card">
<Toolbar class="mb-6">
<template #start>
<!-- <Button label="ساخت تراکنش جدید" icon="pi pi-plus" severity="secondary" class="mr-2 mx-3" @click="router.push('/transaction/create-transaction')" />-->
<Button @click="indexTransactions" :disabled="loading" severity="secondary mx-3" class="indexing-button">
<div class="button-content">
<div v-if="!loading">ایندکس کردن تراکنش‌ها</div>
<div v-else class="loading-content flex">
<LoadingCheck :key="animationKey" :is-complete="indexingFinished" />
<Transition enter-active-class="animate__animated animate__fadeIn animate__fast" leave-active-class="animate__animated animate__fadeOut animate__fast" mode="out-in">
<div :key="indexingMessage">{{ indexingMessage }}</div>
</Transition>
</div>
</div>
</Button>
</template>
<template #end> </template>
</Toolbar>
<DataTable
ref="dt"
v-model:selection="selectedTransactions"
:value="transactions"
:loading="dataLoading"
dataKey="id"
:paginator="true"
:rows="per_page"
:filters="filters"
:total-records="totalCount"
:current-page="page - 1"
@page="onPageChange($event)"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
:rowsPerPageOptions="[20, 40, 80]"
currentPageReportTemplate="نمایش {first} تا {last} از {totalRecords} تراکنش"
tableStyle="min-width: 50rem"
stripedRows
filterDisplay="menu"
:globalFilterFields="['type', 'status', 'belongingName', 'createdAt', 'description']"
>
<template #loading>
<div class="flex flex-col items-center justify-center p-8 gap-3">
<ProgressSpinner style="width: 42px; height: 42px" strokeWidth="4" />
<span class="text-sm font-medium text-surface-500">در حال بارگذاری تراکنش‌ها...</span>
</div>
</template>
<template #empty>
<div v-if="!dataLoading" class="p-8 text-center text-surface-500">
<i class="pi pi-inbox text-4xl mb-2 text-surface-400"></i>
<p>هیچ تراکنشی یافت نشد.</p>
</div>
</template>
<template #header>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center gap-3">
<h4 class="m-0 font-bold">مدیریت تراکنش‌ها</h4>
<Tag
v-if="searchMethod"
:value="searchMethod === 'mongo' ? 'جستجو: MongoDB' : 'جستجو: Typesense'"
:severity="searchMethod === 'mongo' ? 'success' : 'info'"
:icon="searchMethod === 'mongo' ? 'pi pi-database' : 'pi pi-bolt'"
class="cursor-pointer text-xs"
v-tooltip.bottom="'کلیک برای تغییر موتور جستجو در تنظیمات'"
@click="router.push('/settings/search-settings')"
/>
</div>
<div class="p-input-icon-right w-full sm:w-72 mt-2 lg:mt-0">
<InputText v-model="searchPhrase" placeholder="جستجو در تراکنش‌ها..." class="w-full" />
</div>
</div>
<Chip v-if="searchPhrase" class="mx-2 mt-2" :label="searchPhrase" removable @remove="handleRemoveSearch" />
</template>
<Column field="_id" header="شناسه" sortable style="width: 10%"></Column>
<Column field="belongingName" header="ملک" sortable style="width: 15%"></Column>
<Column field="type" header="نوع تراکنش" sortable style="width: 15%">
<template #body="{ data }">
<Tag :severity="getTypeSeverity(data.type)" :value="getTypeTranslation(data.type)" />
</template>
<template #filter="{ filterModel, filterCallback }">
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="انتخاب نوع" class="p-column-filter" showClear />
</template>
</Column>
<Column field="status" header="وضعیت" sortable style="width: 15%">
<template #body="{ data }">
<Tag :severity="getStatusSeverity(data.status)" :value="getStatusTranslation(data.status)" />
</template>
<template #filter="{ filterModel, filterCallback }">
<Dropdown v-model="filterModel.value" @change="filterCallback()" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="انتخاب وضعیت" class="p-column-filter" showClear />
</template>
</Column>
<Column field="createdAt" header="تاریخ" sortable style="width: 15%">
<template #body="slotProps">
{{ formatDate(slotProps.data.createdAt) }}
</template>
</Column>
<Column field="description" header="توضیحات" style="width: 10%">
<template #body="{ data }">
<Button v-if="data.description" icon="pi pi-info-circle" text rounded aria-label="توضیحات" @click="toggle($event, data.description)" />
</template>
</Column>
</DataTable>
<Popover ref="op" :showCloseIcon="true" :autoHide="false">
<div class="p-2">{{ activeDescription }}</div>
</Popover>
</div>
<Dialog v-model:visible="deleteTransactionDialog" :style="{ width: '450px' }" header="Confirm" :modal="true">
<div class="flex items-center gap-4">
<i class="pi pi-exclamation-triangle !text-3xl" />
<span v-if="transaction"
>Are you sure you want to delete transaction <b>{{ transaction.transactionCode }}</b
>?</span
>
</div>
<template #footer>
<Button label="خیر" icon="pi pi-times" text @click="deleteTransactionDialog = false" />
<Button label="بله" icon="pi pi-check" @click="deleteTransaction" />
</template>
</Dialog>
<Dialog v-model:visible="deleteTransactionsDialog" :style="{ width: '450px' }" header="Confirm" :modal="true">
<div class="flex items-center gap-4">
<i class="pi pi-exclamation-triangle !text-3xl" />
<span>Are you sure you want to delete the selected transactions?</span>
</div>
<template #footer>
<Button label="No" icon="pi pi-times" text @click="deleteTransactionsDialog = false" />
<Button label="Yes" icon="pi pi-check" text @click="deleteSelectedTransactions" />
</template>
</Dialog>
</div>
</template>