safavi works

This commit is contained in:
2026-06-23 09:18:41 +02:00
parent 0e16e7c186
commit 1cf4c9675b
41 changed files with 965 additions and 522 deletions
@@ -1,9 +1,8 @@
<template>
<div class="boursecu-transactions pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
<div class="px-4 pt-4 pb-2">
<h1 class="text-xl font-bold text-gray-900">گزارش‌ها</h1>
</div>
<MobilePageHeader title="تراکنش ها" />
<div class="mpage-content">
<div class="boursecu-transactions__tabs px-4">
<div class="flex gap-5 overflow-x-auto whitespace-nowrap pb-1">
<button
@@ -40,29 +39,43 @@
</div>
</div>
<div class="px-4 mt-4 flex items-center gap-2 flex-wrap">
<div class="flex items-center gap-1 text-sm text-gray-600 shrink-0">
<div v-if="activeTab.showAdvancedFilters" class="boursecu-transactions__filters px-4 mt-4">
<div class="boursecu-transactions__filters-label">
<BasicIcon icon="vuesax-linear:filter" size="16" />
<span>فیلترها:</span>
</div>
<a-select
v-model:value="statusFilter"
allow-clear
class="boursecu-transactions__filter-select min-w-[7rem]"
class="boursecu-transactions__filter-select"
placeholder="وضعیت"
size="large"
@change="applyInlineFilters"
>
<a-select-option value="active">فعال</a-select-option>
<a-select-option value="inactive">غیرفعال</a-select-option>
<a-select-option
v-for="option in statusOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-select-option>
</a-select>
<BaseDatePicker
v-model:value="dateFilter"
class="boursecu-transactions__filter-date min-w-[7rem]"
placeholder="تاریخ"
size="large"
@onchange="applyInlineFilters"
/>
<div class="boursecu-transactions__filter-dates">
<BaseDatePicker
v-model:value="dateFromFilter"
class="boursecu-transactions__filter-date"
placeholder="تاریخ از"
size="large"
@onchange="applyInlineFilters"
/>
<BaseDatePicker
v-model:value="dateToFilter"
class="boursecu-transactions__filter-date"
placeholder="تاریخ تا"
size="large"
@onchange="applyInlineFilters"
/>
</div>
</div>
<div ref="listRef" :class="loading ? 'min-h-[50vh]' : ''" class="px-4 mt-5">
@@ -86,6 +99,7 @@
<p class="boursecu-transactions__empty-text">{{ emptyText }}</p>
</div>
</div>
</div>
</div>
</template>
@@ -93,6 +107,7 @@
import { ref } from 'vue'
import { loading, loadingMore } from '@/utils'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
@@ -105,8 +120,10 @@ const listRef = ref<HTMLElement | null>(null)
const {
items,
dateFilter,
dateFromFilter,
dateToFilter,
statusFilter,
statusOptions,
activeTabId,
tradeSide,
activeTab,
@@ -171,10 +188,33 @@ const {
color: #1f2937;
}
.boursecu-transactions__filter-select,
.boursecu-transactions__filters {
display: flex;
flex-direction: column;
gap: 10px;
}
.boursecu-transactions__filters-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.875rem;
color: #6b7280;
}
.boursecu-transactions__filter-select {
width: 100%;
}
.boursecu-transactions__filter-dates {
display: flex;
gap: 10px;
width: 100%;
}
.boursecu-transactions__filter-date {
flex: 1;
max-width: 9rem;
min-width: 0;
}
.boursecu-transactions__empty {
@@ -1,14 +1,13 @@
<template>
<div>
<div class="absolute inset-0 bg-cover bg-center opacity-60"></div>
<div
class="bg-white w-full transaction-list py-3 px-3 font-bold drop-shadow flex justify-between z-10"
>
<div>لیست تراکنش ها</div>
<BasicIcon icon="vuesax-linear:filter" @click="showFilter = true" />
</div>
<MobilePageHeader title="تراکنش ها">
<template #trailing>
<BasicIcon icon="vuesax-linear:filter" @click="showFilter = true" />
</template>
</MobilePageHeader>
<div ref="el" :class="loading ? 'h-screen' : ''" class="px-3 pt-16 pb-10">
<div ref="el" :class="loading ? 'h-screen' : ''" class="px-3 mpage-content pb-10">
<list-anim v-if="!loading">
<Item v-for="item in items" :key="item.id" :item="item" />
<div :ref="bindMobileObserver" v-show="!loading" class="w-full opacity-0 relative">
@@ -45,6 +44,7 @@ import { ref } from 'vue'
import { loading, loadingMore } from '@/utils'
import Item from './components/Item.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
@@ -64,16 +64,3 @@ const {
bindMobileObserver,
} = useTransactionsPageContext()
</script>
<style scoped lang="less">
.transaction-list {
position: fixed;
right: 0;
left: 0;
@media screen and (min-width: 426px) {
width: 426px;
left: auto;
right: auto;
}
}
</style>
@@ -4,6 +4,8 @@ import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { loading, loadingMore } from '@/utils'
import {
BOURSECU_TRANSACTION_TABS,
getBoursecuStatusFilterGroup,
getBoursecuStatusOptionsForTab,
type BoursecuTradeSide,
type BoursecuTransactionTab,
type BoursecuTransactionTabId,
@@ -16,6 +18,8 @@ import {
resetTransactionsPagination,
transactionActiveTabId,
transactionDateFilter,
transactionDateFromFilter,
transactionDateToFilter,
transactionItems,
transactionPagination,
transactionStatusFilter,
@@ -30,12 +34,15 @@ export interface TransactionsPageContext {
tableOutput: typeof transactionTableOutput
showFilter: ReturnType<typeof ref<boolean>>
dateFilter: typeof transactionDateFilter
dateFromFilter: typeof transactionDateFromFilter
dateToFilter: typeof transactionDateToFilter
statusFilter: typeof transactionStatusFilter
mobileObserverEl: ReturnType<typeof ref<HTMLElement | null>>
desktopObserverEl: ReturnType<typeof ref<HTMLElement | null>>
activeTabId: typeof transactionActiveTabId
tradeSide: typeof transactionTradeSide
activeTab: ReturnType<typeof computed<BoursecuTransactionTab>>
statusOptions: ReturnType<typeof computed<ReturnType<typeof getBoursecuStatusOptionsForTab>>>
emptyText: ReturnType<typeof computed<string>>
resetPagination: typeof resetTransactionsPagination
initTable: () => Promise<void>
@@ -68,6 +75,8 @@ export function useTransactionsPageProvider(options?: { boursecu?: boolean }) {
BOURSECU_TRANSACTION_TABS[0],
)
const statusOptions = computed(() => getBoursecuStatusOptionsForTab(activeTab.value))
const emptyText = computed(() => {
if (!transactionsBoursecuMode.value) {
return 'تراکنشی موجود نیست'
@@ -94,6 +103,14 @@ export function useTransactionsPageProvider(options?: { boursecu?: boolean }) {
return
}
const currentTab = activeTab.value
const nextTab =
BOURSECU_TRANSACTION_TABS.find((tab) => tab.id === tabId) ?? BOURSECU_TRANSACTION_TABS[0]
if (getBoursecuStatusFilterGroup(currentTab) !== getBoursecuStatusFilterGroup(nextTab)) {
transactionStatusFilter.value = undefined
}
transactionActiveTabId.value = tabId
await reloadTransactionsTable()
}
@@ -152,12 +169,15 @@ export function useTransactionsPageProvider(options?: { boursecu?: boolean }) {
tableOutput: transactionTableOutput,
showFilter,
dateFilter: transactionDateFilter,
dateFromFilter: transactionDateFromFilter,
dateToFilter: transactionDateToFilter,
statusFilter: transactionStatusFilter,
mobileObserverEl,
desktopObserverEl,
activeTabId: transactionActiveTabId,
tradeSide: transactionTradeSide,
activeTab,
statusOptions,
emptyText,
resetPagination: resetTransactionsPagination,
initTable,
@@ -20,6 +20,8 @@ export const transactionPagination = ref<TableEngineInput>({ filters: [], limit:
export const transactionItems = ref<Transaction[]>([])
export const transactionTableOutput = ref<TableEngineOutput<Transaction> | null>(null)
export const transactionDateFilter = ref<string>()
export const transactionDateFromFilter = ref<string>()
export const transactionDateToFilter = ref<string>()
export const transactionStatusFilter = ref<string>()
export const transactionActiveTabId = ref<BoursecuTransactionTabId>('trades')
export const transactionTradeSide = ref<BoursecuTradeSide>('buy')
@@ -40,7 +42,8 @@ export function resetTransactionsPagination() {
transactionPagination.value = buildBoursecuTransactionPagination(
getActiveBoursecuTab(),
transactionTradeSide.value,
transactionDateFilter.value,
transactionDateFromFilter.value,
transactionDateToFilter.value,
transactionStatusFilter.value,
)
return
@@ -151,6 +154,8 @@ export function resetTransactionsPageData() {
transactionItems.value = []
transactionTableOutput.value = null
transactionDateFilter.value = undefined
transactionDateFromFilter.value = undefined
transactionDateToFilter.value = undefined
transactionStatusFilter.value = undefined
transactionActiveTabId.value = 'trades'
transactionTradeSide.value = 'buy'
@@ -10,21 +10,50 @@ export type BoursecuTradeSide = 'buy' | 'sell'
export type BoursecuTransactionFetchMode = 'transaction' | 'deposit'
export type BoursecuTradeStatusFilter = 'completed' | 'pending' | 'cancelled'
export type BoursecuFulfillmentStatusFilter = 'delivered' | 'registered' | 'cancelled'
export type BoursecuTransactionStatusFilter = BoursecuTradeStatusFilter | BoursecuFulfillmentStatusFilter
export type BoursecuStatusFilterGroup = 'trade' | 'fulfillment'
export interface BoursecuTransactionStatusOption {
value: string
label: string
sts: number
}
export interface BoursecuTransactionTab {
id: BoursecuTransactionTabId
label: string
emptyText: string
showTradeToggle?: boolean
showAdvancedFilters?: boolean
fetchMode?: BoursecuTransactionFetchMode
typeFilters?: number[]
}
export const BOURSECU_TRADE_STATUS_OPTIONS: BoursecuTransactionStatusOption[] = [
{ value: 'completed', label: 'تکمیل شده', sts: 2 },
{ value: 'pending', label: 'در انتظار تایید', sts: 1 },
{ value: 'cancelled', label: 'لغو شده', sts: 3 },
]
export const BOURSECU_FULFILLMENT_STATUS_OPTIONS: BoursecuTransactionStatusOption[] = [
{ value: 'delivered', label: 'تحویل شده', sts: 2 },
{ value: 'registered', label: 'ثبت درخواست', sts: 1 },
{ value: 'cancelled', label: 'لغو شده', sts: 3 },
]
/** @deprecated Use getBoursecuStatusOptionsForTab instead */
export const BOURSECU_TRANSACTION_STATUS_OPTIONS = BOURSECU_TRADE_STATUS_OPTIONS
export const BOURSECU_TRANSACTION_TABS: BoursecuTransactionTab[] = [
{
id: 'trades',
label: 'همه',
emptyText: 'معامله‌ای برای شما به ثبت نرسیده است',
showTradeToggle: true,
showAdvancedFilters: true,
typeFilters: [1, 2],
},
{
@@ -32,17 +61,20 @@ export const BOURSECU_TRANSACTION_TABS: BoursecuTransactionTab[] = [
label: 'واریز',
emptyText: 'واریزی برای شما به ثبت نرسیده است',
fetchMode: 'deposit',
showAdvancedFilters: true,
},
{
id: 'withdrawal',
label: 'برداشت',
emptyText: 'برداشتی برای شما به ثبت نرسیده است',
showAdvancedFilters: true,
typeFilters: [4],
},
{
id: 'physical',
label: 'تحویل فیزیکی',
emptyText: 'درخواست تحویل فیزیکی برای شما ثبت نشده است',
showAdvancedFilters: true,
typeFilters: [9],
},
]
@@ -52,11 +84,43 @@ const TRADE_TYPE_BY_SIDE: Record<BoursecuTradeSide, number> = {
sell: 2,
}
export function getBoursecuStatusFilterGroup(tab: BoursecuTransactionTab): BoursecuStatusFilterGroup {
return tab.id === 'withdrawal' || tab.id === 'physical' ? 'fulfillment' : 'trade'
}
export function getBoursecuStatusOptionsForTab(
tab: BoursecuTransactionTab,
): BoursecuTransactionStatusOption[] {
return getBoursecuStatusFilterGroup(tab) === 'fulfillment'
? BOURSECU_FULFILLMENT_STATUS_OPTIONS
: BOURSECU_TRADE_STATUS_OPTIONS
}
function getBoursecuStatusFilterField(tab: BoursecuTransactionTab): string {
if (tab.fetchMode === 'deposit') {
return 'sts'
}
if (tab.id === 'withdrawal') {
return 'withdraw.sts'
}
return 'order.sts'
}
function resolveBoursecuStatusSts(
tab: BoursecuTransactionTab,
statusFilter?: BoursecuTransactionStatusFilter | string,
): number | undefined {
return getBoursecuStatusOptionsForTab(tab).find((option) => option.value === statusFilter)?.sts
}
export function buildBoursecuTransactionPagination(
tab: BoursecuTransactionTab,
tradeSide: BoursecuTradeSide,
dateFilter?: string,
statusFilter?: string
dateFrom?: string,
dateTo?: string,
statusFilter?: BoursecuTransactionStatusFilter | string,
): TableEngineInput {
const filters: TableEngineInput['filters'] = []
@@ -75,24 +139,37 @@ export function buildBoursecuTransactionPagination(
})
}
if (dateFilter) {
filters.push({
field: 'created_at',
cr: 'and',
type: 'date',
op: 'eq',
value: dateFilter,
})
}
if (tab.showAdvancedFilters) {
if (dateFrom) {
filters.push({
field: 'created_at',
cr: 'and',
type: 'date',
op: 'gte',
value: dateFrom,
})
}
if (statusFilter === 'active' || statusFilter === 'inactive') {
filters.push({
field: 'active',
cr: 'and',
type: 'boolean',
op: 'eq',
value: statusFilter === 'active',
})
if (dateTo) {
filters.push({
field: 'created_at',
cr: 'and',
type: 'date',
op: 'lte',
value: dateTo,
})
}
const statusSts = resolveBoursecuStatusSts(tab, statusFilter)
if (statusSts != null) {
filters.push({
field: getBoursecuStatusFilterField(tab),
cr: 'and',
type: 'number',
op: 'eq',
value: statusSts,
})
}
}
return {
@@ -1,10 +1,7 @@
<template>
<div class="dtxn">
<div class="dtxn__container">
<header class="dtxn-topbar">
<p class="dtxn-topbar__eyebrow">گزارش‌ها</p>
<h1 class="dtxn-topbar__title">تراکنش‌ها</h1>
</header>
<DesktopPageTopbar title="تراکنش‌ها" />
<div class="dtxn-toolbar">
<div class="dtxn-tabs">
@@ -40,7 +37,7 @@
</div>
</div>
<div class="dtxn-filters">
<div v-if="activeTab.showAdvancedFilters" class="dtxn-filters">
<div class="dtxn-filters__label">
<BasicIcon icon="vuesax-linear:filter" size="16" />
<span>فیلترها:</span>
@@ -53,16 +50,30 @@
size="large"
@change="applyInlineFilters"
>
<a-select-option value="active">فعال</a-select-option>
<a-select-option value="inactive">غیرفعال</a-select-option>
<a-select-option
v-for="option in statusOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-select-option>
</a-select>
<BaseDatePicker
v-model:value="dateFilter"
class="dtxn-filters__date"
placeholder="تاریخ"
size="large"
@onchange="applyInlineFilters"
/>
<div class="dtxn-filters__dates">
<BaseDatePicker
v-model:value="dateFromFilter"
class="dtxn-filters__date"
placeholder="تاریخ از"
size="large"
@onchange="applyInlineFilters"
/>
<BaseDatePicker
v-model:value="dateToFilter"
class="dtxn-filters__date"
placeholder="تاریخ تا"
size="large"
@onchange="applyInlineFilters"
/>
</div>
</div>
<div ref="listRef" :class="loading ? 'min-h-[40vh]' : ''" class="mt-5">
@@ -98,6 +109,7 @@
import { ref } from 'vue'
import { loading, loadingMore } from '@/utils'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
@@ -110,8 +122,10 @@ const listRef = ref<HTMLElement | null>(null)
const {
items,
dateFilter,
dateFromFilter,
dateToFilter,
statusFilter,
statusOptions,
activeTabId,
tradeSide,
activeTab,
@@ -1,10 +1,7 @@
<template>
<div class="dtxn">
<div class="dtxn__container">
<header class="dtxn-topbar">
<p class="dtxn-topbar__eyebrow">گزارش‌ها</p>
<h1 class="dtxn-topbar__title">لیست تراکنش‌ها</h1>
</header>
<DesktopPageTopbar title="تراکنش‌ها" />
<div class="dtxn-filters">
<div class="dtxn-filters__label">
@@ -58,6 +55,7 @@
import { ref } from 'vue'
import { loading, loadingMore } from '@/utils'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
import ListAnim from '@/components/ListAnim.vue'
@@ -12,28 +12,6 @@
padding: 28px 36px 40px;
}
.dtxn-topbar {
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
&__eyebrow {
font-size: 12px;
font-weight: 500;
color: #9ca3af;
margin: 0 0 4px;
}
&__title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.03em;
color: #111827;
margin: 0;
line-height: 1.2;
}
}
.dtxn-toolbar {
display: flex;
flex-wrap: wrap;
@@ -119,6 +97,12 @@
&__date {
min-width: 140px;
}
&__dates {
display: flex;
align-items: center;
gap: 10px;
}
}
.dtxn-list {