fix accounting fails
This commit is contained in:
+44
-3
@@ -2,17 +2,21 @@ import useAxios from '@core/utils/useAxios'
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
|
||||
export interface TahesabRecord {
|
||||
id: number
|
||||
id?: number | null
|
||||
customer_id?: number
|
||||
amount?: number
|
||||
desc?: string
|
||||
type?: string
|
||||
sync_status?: 'pending' | 'success' | 'failed'
|
||||
sync_status?: 'pending' | 'success' | 'failed' | 'missing'
|
||||
sync_log?: string | null
|
||||
tahesab_id?: string | null
|
||||
data?: string | null
|
||||
res?: string | null
|
||||
created_at?: string
|
||||
withdraw_id?: number | null
|
||||
order_id?: number | null
|
||||
issue_kind?: 'failed' | 'missing'
|
||||
sync_target?: { type: 'row' | 'withdraw' | 'order'; id: number }
|
||||
customer?: {
|
||||
id?: number
|
||||
name?: string
|
||||
@@ -21,7 +25,7 @@ export interface TahesabRecord {
|
||||
}
|
||||
}
|
||||
|
||||
export const getTahesabTable = async (params: TableInput) =>
|
||||
export const getTahesabTable = async (params: TableInput & { view?: string }) =>
|
||||
await useAxios.post<TahesabRecord[]>('/admin/tahesab/table', params)
|
||||
|
||||
export const getTahesabDetail = async (id: number) =>
|
||||
@@ -30,6 +34,43 @@ export const getTahesabDetail = async (id: number) =>
|
||||
export const retryTahesabSanad = async (id: number) =>
|
||||
await useAxios.post<{ ok: boolean; message: string; tahesab?: TahesabRecord }>(`/admin/tahesab/${id}/retry`)
|
||||
|
||||
export interface TahesabPendingSummary {
|
||||
failed_rows: number
|
||||
missing_withdraws: number
|
||||
missing_orders: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface TahesabSyncAllResult {
|
||||
ok: boolean
|
||||
retried: number
|
||||
backfilled_withdraws: number
|
||||
backfilled_orders: number
|
||||
failed: number
|
||||
errors: string[]
|
||||
summary: TahesabPendingSummary
|
||||
}
|
||||
|
||||
export const getTahesabPendingSummary = async (customerId?: number) =>
|
||||
await useAxios.get<TahesabPendingSummary>('/admin/tahesab/pending-summary', {
|
||||
params: customerId ? { customer_id: customerId } : undefined
|
||||
})
|
||||
|
||||
export const getTahesabPendingDocs = async (customerId?: number, limit = 200) =>
|
||||
await useAxios.get<{ rows: TahesabRecord[]; summary: TahesabPendingSummary }>(
|
||||
'/admin/tahesab/pending-docs',
|
||||
{ params: { ...(customerId ? { customer_id: customerId } : {}), limit } }
|
||||
)
|
||||
|
||||
export const syncOneTahesab = async (type: 'row' | 'withdraw' | 'order', id: number) =>
|
||||
await useAxios.post<{ ok: boolean; message: string }>('/admin/tahesab/sync-one', { type, id })
|
||||
|
||||
export const syncAllTahesabPending = async (limit = 80, customerId?: number) =>
|
||||
await useAxios.post<TahesabSyncAllResult>('/admin/tahesab/sync-all-pending', {
|
||||
limit,
|
||||
...(customerId ? { customer_id: customerId } : {})
|
||||
})
|
||||
|
||||
export const registerCustomerInTahesab = async (customerId: number) =>
|
||||
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
||||
`/admin/customer/${customerId}/tahesab/register`
|
||||
|
||||
+44
-3
@@ -2,17 +2,21 @@ import useAxios from '@core/utils/useAxios'
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
|
||||
export interface ZarinRecord {
|
||||
id: number
|
||||
id?: number | null
|
||||
customer_id?: number
|
||||
amount?: number
|
||||
desc?: string
|
||||
type?: string
|
||||
sync_status?: 'pending' | 'success' | 'failed'
|
||||
sync_status?: 'pending' | 'success' | 'failed' | 'missing'
|
||||
sync_log?: string | null
|
||||
zarin_id?: string | null
|
||||
data?: string | null
|
||||
res?: string | null
|
||||
created_at?: string
|
||||
withdraw_id?: number | null
|
||||
order_id?: number | null
|
||||
issue_kind?: 'failed' | 'missing'
|
||||
sync_target?: { type: 'row' | 'withdraw' | 'order'; id: number }
|
||||
customer?: {
|
||||
id?: number
|
||||
name?: string
|
||||
@@ -21,7 +25,7 @@ export interface ZarinRecord {
|
||||
}
|
||||
}
|
||||
|
||||
export const getZarinTable = async (params: TableInput) =>
|
||||
export const getZarinTable = async (params: TableInput & { view?: string }) =>
|
||||
await useAxios.post<ZarinRecord[]>('/admin/zarin/table', params)
|
||||
|
||||
export const getZarinDetail = async (id: number) =>
|
||||
@@ -30,6 +34,43 @@ export const getZarinDetail = async (id: number) =>
|
||||
export const retryZarinSanad = async (id: number) =>
|
||||
await useAxios.post<{ ok: boolean; message: string; zarin?: ZarinRecord }>(`/admin/zarin/${id}/retry`)
|
||||
|
||||
export interface ZarinPendingSummary {
|
||||
failed_rows: number
|
||||
missing_withdraws: number
|
||||
missing_orders: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface ZarinSyncAllResult {
|
||||
ok: boolean
|
||||
retried: number
|
||||
backfilled_withdraws: number
|
||||
backfilled_orders: number
|
||||
failed: number
|
||||
errors: string[]
|
||||
summary: ZarinPendingSummary
|
||||
}
|
||||
|
||||
export const getZarinPendingSummary = async (customerId?: number) =>
|
||||
await useAxios.get<ZarinPendingSummary>('/admin/zarin/pending-summary', {
|
||||
params: customerId ? { customer_id: customerId } : undefined
|
||||
})
|
||||
|
||||
export const getZarinPendingDocs = async (customerId?: number, limit = 200) =>
|
||||
await useAxios.get<{ rows: ZarinRecord[]; summary: ZarinPendingSummary }>(
|
||||
'/admin/zarin/pending-docs',
|
||||
{ params: { ...(customerId ? { customer_id: customerId } : {}), limit } }
|
||||
)
|
||||
|
||||
export const syncOneZarin = async (type: 'row' | 'withdraw' | 'order', id: number) =>
|
||||
await useAxios.post<{ ok: boolean; message: string }>('/admin/zarin/sync-one', { type, id })
|
||||
|
||||
export const syncAllZarinPending = async (limit = 80, customerId?: number) =>
|
||||
await useAxios.post<ZarinSyncAllResult>('/admin/zarin/sync-all-pending', {
|
||||
limit,
|
||||
...(customerId ? { customer_id: customerId } : {})
|
||||
})
|
||||
|
||||
export const registerCustomerInZarin = async (customerId: number) =>
|
||||
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
||||
`/admin/customer/${customerId}/zarin/register`
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface Product{
|
||||
original_image?: File
|
||||
inventory?: number
|
||||
description?: string | null
|
||||
karat?: string | null
|
||||
}
|
||||
|
||||
export interface MethodePrices {
|
||||
|
||||
@@ -1,18 +1,116 @@
|
||||
<template>
|
||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||
<BasicShowContext title="اسناد تهحساب">
|
||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<BasicShowContext :title="customerId ? 'اسناد تهحساب این مشتری' : 'اسناد تهحساب'">
|
||||
<template #extra>
|
||||
<a-button
|
||||
v-if="(pending?.total ?? 0) > 0"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="syncing"
|
||||
@click="runSyncAll"
|
||||
>
|
||||
ثبت همه ({{ pending?.total }})
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="(pending?.total ?? 0) > 0"
|
||||
class="mb-3 rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900"
|
||||
>
|
||||
<div class="font-700">
|
||||
{{ pending?.total }} سند جاافتاده / ناموفق برای تهحساب
|
||||
<span v-if="customerId">این مشتری</span>
|
||||
</div>
|
||||
<div class="mt-1 opacity-90">
|
||||
خطا خورده در جدول: {{ pending?.failed_rows }} —
|
||||
شارژ جاافتاده: {{ pending?.missing_withdraws }} —
|
||||
سفارش جاافتاده: {{ pending?.missing_orders }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:activeKey="docsView" class="mb-2" @change="onViewChange">
|
||||
<a-tab-pane key="failed">
|
||||
<template #tab>
|
||||
خطا خورده / جاافتاده
|
||||
<span v-if="(pending?.total ?? 0) > 0">({{ pending?.total }})</span>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="ok" tab="سندهای سالم" />
|
||||
<a-tab-pane key="all" tab="کل اسناد" />
|
||||
</a-tabs>
|
||||
|
||||
<!-- تب خطا: شامل جاافتاده + خطا خورده -->
|
||||
<div v-if="docsView === 'failed'" class="min-w-0 w-full overflow-x-auto">
|
||||
<a-spin :spinning="failedLoading">
|
||||
<a-table
|
||||
:columns="failedColumns"
|
||||
:data-source="failedRows"
|
||||
:pagination="{ pageSize: 20, showSizeChanger: true }"
|
||||
:row-key="failedRowKey"
|
||||
size="small"
|
||||
:locale="{ emptyText: 'مورد خطا خورده یا جاافتادهای نیست' }"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'row'">{{ index + 1 }}</template>
|
||||
<template v-else-if="column.key === 'issue'">
|
||||
<a-tag :color="record.issue_kind === 'missing' ? 'orange' : 'red'">
|
||||
{{ tahesabIssueKindLabel(record.issue_kind) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'customer'">
|
||||
{{ record.customer?.name || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'amount'">
|
||||
{{ metalAmountLabel(record) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'rial'">
|
||||
{{ rialAmountLabel(record) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
{{ tahesabStatusLabel(record.sync_status, record.tahesab_id) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'time'">
|
||||
{{ record.created_at ? toJalali(record.created_at) : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<div class="flex justify-center gap-2">
|
||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
||||
<template #title><span>جزئیات</span></template>
|
||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="!record.tahesab_id && !record.customer?.moshtari_code && record.customer_id"
|
||||
placement="bottom"
|
||||
class="cursor-pointer"
|
||||
@click="openRegister(record)"
|
||||
>
|
||||
<template #title><span>ثبت مشتری</span></template>
|
||||
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||
</a-tooltip>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
:loading="syncingRowKey === failedRowKey(record)"
|
||||
@click="runSyncOne(record)"
|
||||
>
|
||||
ثبت
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||
<BaseTable :key="docsView" v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<template #action="{ record }">
|
||||
<div class="flex justify-center gap-2">
|
||||
|
||||
<!-- دکمه ۱: اطلاعات کلی (همیشه نمایان) -->
|
||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
||||
<template #title><span>اطلاعات سند</span></template>
|
||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</a-tooltip>
|
||||
|
||||
<!-- دکمه ۲: ثبت مشتری در تهحساب (فقط اگر سند هنوز ثبت نشده و مشتری کد ندارد) -->
|
||||
<a-tooltip
|
||||
v-if="!record.tahesab_id && !record.customer?.moshtari_code"
|
||||
placement="bottom"
|
||||
@@ -23,7 +121,6 @@
|
||||
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||
</a-tooltip>
|
||||
|
||||
<!-- دکمه ۳: ارسال به تهحساب (فقط اگر هنوز در تهحساب ثبت نشده) -->
|
||||
<a-tooltip
|
||||
v-if="!record.tahesab_id"
|
||||
placement="bottom"
|
||||
@@ -33,16 +130,14 @@
|
||||
<template #title><span>ارسال به تهحساب</span></template>
|
||||
<Icon icon="vuesax-linear:send-2" color="#CA8A04" />
|
||||
</a-tooltip>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
|
||||
<TahesabMobileCardList v-else ref="mobileListRef" />
|
||||
<TahesabMobileCardList v-else-if="docsView !== 'failed'" ref="mobileListRef" />
|
||||
</BasicShowContext>
|
||||
|
||||
<!-- مودال اطلاعات کلی -->
|
||||
<BasicModal v-model:visible="infoVisible" title="اطلاعات سند" width="36rem" :footer="null">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div
|
||||
@@ -53,134 +148,222 @@
|
||||
<span class="text-gray-500">{{ item.label }}</span>
|
||||
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
|
||||
</div>
|
||||
<div v-if="selected?.res" class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap">
|
||||
<div
|
||||
v-if="selected?.res"
|
||||
class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap"
|
||||
>
|
||||
{{ selected.res }}
|
||||
</div>
|
||||
<div v-if="selected?.sync_log" class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap">
|
||||
<div
|
||||
v-if="selected?.sync_log"
|
||||
class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap"
|
||||
>
|
||||
{{ selected.sync_log }}
|
||||
</div>
|
||||
<div v-if="canSyncSelected" class="pt-2">
|
||||
<a-button type="primary" block :loading="!!syncingRowKey" @click="runSyncOne(selected!)">
|
||||
ثبت در تهحساب
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
|
||||
<!-- مودال ارسال به تهحساب -->
|
||||
<TahesabRetryModal
|
||||
v-model="sendVisible"
|
||||
mode="retry-sanad"
|
||||
:tahesab-id="selected?.id"
|
||||
@success="fetch"
|
||||
:tahesab-id="selected?.id ?? undefined"
|
||||
@success="afterRowSync"
|
||||
/>
|
||||
|
||||
<!-- مودال ثبت مشتری در تهحساب -->
|
||||
<TahesabRetryModal
|
||||
v-model="registerVisible"
|
||||
mode="register-customer"
|
||||
:customer-id="selected?.customer_id"
|
||||
:customer-name="selected?.customer?.name"
|
||||
@success="fetch"
|
||||
@success="afterRowSync"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import { getTahesabTable, type TahesabRecord } from '@/api/tahesab'
|
||||
import {
|
||||
getTahesabPendingDocs,
|
||||
getTahesabPendingSummary,
|
||||
getTahesabTable,
|
||||
syncAllTahesabPending,
|
||||
syncOneTahesab,
|
||||
type TahesabPendingSummary,
|
||||
type TahesabRecord
|
||||
} from '@/api/tahesab'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import BasicModal from '@/components/BasicModal.vue'
|
||||
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
||||
import TahesabMobileCardList from '@/pages/menuPages/accounting/components/TahesabMobileCardList.vue'
|
||||
import { tahesabStatusLabel } from '@/pages/menuPages/accounting/utils/tahesabStatus'
|
||||
import {
|
||||
tahesabIssueKindLabel,
|
||||
tahesabStatusLabel
|
||||
} from '@/pages/menuPages/accounting/utils/tahesabStatus'
|
||||
import {
|
||||
metalAmountLabel,
|
||||
rialAmountLabel,
|
||||
rialAmountLabel
|
||||
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
|
||||
const props = defineProps<{
|
||||
customerId?: number
|
||||
}>()
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
const infoVisible = ref(false)
|
||||
const sendVisible = ref(false)
|
||||
const registerVisible = ref(false)
|
||||
const selected = ref<TahesabRecord | null>(null)
|
||||
const pending = ref<TahesabPendingSummary | null>(null)
|
||||
const syncing = ref(false)
|
||||
const docsView = ref<'failed' | 'ok' | 'all'>('failed')
|
||||
const failedRows = ref<TahesabRecord[]>([])
|
||||
const failedLoading = ref(false)
|
||||
const syncingRowKey = ref<string | null>(null)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => r.index + 1
|
||||
},
|
||||
{ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' },
|
||||
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
||||
{
|
||||
title: 'مقدار',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
type: 'Number',
|
||||
filterable: false,
|
||||
customRender: (r: any) => metalAmountLabel(r.record),
|
||||
},
|
||||
{
|
||||
title: 'مبلغ ریالی',
|
||||
dataIndex: 'rial_amount',
|
||||
key: 'rial_amount',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => rialAmountLabel(r.record),
|
||||
},
|
||||
{ title: 'شرح', dataIndex: 'desc', key: 'desc', type: 'Text' },
|
||||
{
|
||||
title: 'وضعیت',
|
||||
dataIndex: 'sync_status',
|
||||
key: 'sync_status',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
customRender: (r: any) => tahesabStatusLabel(r.record.sync_status, r.record.tahesab_id)
|
||||
},
|
||||
{
|
||||
title: 'زمان',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'Text',
|
||||
customRender: (r: any) => toJalali(r.record.created_at)
|
||||
},
|
||||
{
|
||||
title: 'عملیات',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
slot: 'action',
|
||||
filterable: false
|
||||
const columns = computed(() => {
|
||||
const cols: any[] = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => r.index + 1
|
||||
}
|
||||
]
|
||||
if (!props.customerId) {
|
||||
cols.push({ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' })
|
||||
}
|
||||
]
|
||||
|
||||
const tblProps = ref({
|
||||
columns,
|
||||
api: (params: TableInput) => getTahesabTable(params)
|
||||
cols.push(
|
||||
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
||||
{
|
||||
title: 'مقدار',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
type: 'Number',
|
||||
filterable: false,
|
||||
customRender: (r: any) => metalAmountLabel(r.record)
|
||||
},
|
||||
{
|
||||
title: 'مبلغ ریالی',
|
||||
dataIndex: 'rial_amount',
|
||||
key: 'rial_amount',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => rialAmountLabel(r.record)
|
||||
},
|
||||
{ title: 'شرح', dataIndex: 'desc', key: 'desc', type: 'Text' },
|
||||
{
|
||||
title: 'وضعیت',
|
||||
dataIndex: 'sync_status',
|
||||
key: 'sync_status',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
customRender: (r: any) => tahesabStatusLabel(r.record.sync_status, r.record.tahesab_id)
|
||||
},
|
||||
{
|
||||
title: 'زمان',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'Text',
|
||||
customRender: (r: any) => toJalali(r.record.created_at)
|
||||
},
|
||||
{
|
||||
title: 'عملیات',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
slot: 'action',
|
||||
filterable: false
|
||||
}
|
||||
)
|
||||
return cols
|
||||
})
|
||||
|
||||
/** اطلاعاتی که برای هر سند حسابداری نیاز است (بدون جزئیات تهحسابمحور) */
|
||||
const failedColumns = computed(() => {
|
||||
const cols: any[] = [
|
||||
{ title: 'ردیف', key: 'row', width: 60 },
|
||||
{ title: 'نوع مشکل', key: 'issue', width: 110 }
|
||||
]
|
||||
if (!props.customerId) {
|
||||
cols.push({
|
||||
title: 'مشتری',
|
||||
key: 'customer'
|
||||
})
|
||||
}
|
||||
cols.push(
|
||||
{ title: 'نوع', dataIndex: 'type', key: 'type' },
|
||||
{ title: 'مقدار', key: 'amount' },
|
||||
{ title: 'مبلغ ریالی', key: 'rial' },
|
||||
{ title: 'شرح', dataIndex: 'desc', key: 'desc', ellipsis: true },
|
||||
{ title: 'وضعیت', key: 'status' },
|
||||
{ title: 'زمان', key: 'time', width: 140 },
|
||||
{ title: 'عملیات', key: 'action', width: 140 }
|
||||
)
|
||||
return cols
|
||||
})
|
||||
|
||||
const tblProps = computed(() => ({
|
||||
columns: columns.value,
|
||||
api: (params: TableInput) => {
|
||||
if (props.customerId) {
|
||||
params.filters = params.filters || []
|
||||
params.filters.push({
|
||||
field: 'customer_id',
|
||||
type: 'text',
|
||||
value: props.customerId,
|
||||
cr: 'and',
|
||||
op: 'eq',
|
||||
hidden: false
|
||||
})
|
||||
}
|
||||
return getTahesabTable({ ...params, view: docsView.value })
|
||||
}
|
||||
}))
|
||||
|
||||
const infoItems = computed(() => {
|
||||
const row = selected.value
|
||||
if (!row) return []
|
||||
return [
|
||||
{ label: 'شناسه سند', value: row.id },
|
||||
{ label: 'شناسه سند', value: row.id ?? '—' },
|
||||
{ label: 'نوع مشکل', value: row.issue_kind ? tahesabIssueKindLabel(row.issue_kind) : '—' },
|
||||
{ label: 'مشتری', value: row.customer?.name },
|
||||
{ label: 'موبایل', value: row.customer?.mobile },
|
||||
{ label: 'نوع', value: row.type },
|
||||
{ label: 'مقدار', value: metalAmountLabel(row) },
|
||||
{ label: 'مبلغ ریالی', value: rialAmountLabel(row) },
|
||||
{ label: 'شرح', value: row.desc },
|
||||
{ label: 'سفارش', value: row.order_id || '-' },
|
||||
{ label: 'برداشت', value: row.withdraw_id || '-' },
|
||||
{ label: 'زمان ثبت', value: row.created_at ? toJalali(row.created_at) : '-' },
|
||||
{ label: 'وضعیت', value: tahesabStatusLabel(row.sync_status, row.tahesab_id) },
|
||||
{ label: 'کد تهحساب', value: row.tahesab_id || 'ثبت نشده' }
|
||||
]
|
||||
})
|
||||
|
||||
const canSyncSelected = computed(() => {
|
||||
const row = selected.value
|
||||
if (!row?.sync_target) return false
|
||||
return !row.tahesab_id
|
||||
})
|
||||
|
||||
function failedRowKey(record: TahesabRecord) {
|
||||
if (record.sync_target) return `${record.sync_target.type}-${record.sync_target.id}`
|
||||
if (record.id) return `row-${record.id}`
|
||||
return `x-${record.withdraw_id || record.order_id || Math.random()}`
|
||||
}
|
||||
|
||||
function openInfo(record: TahesabRecord) {
|
||||
selected.value = record
|
||||
infoVisible.value = true
|
||||
@@ -196,13 +379,130 @@ function openRegister(record: TahesabRecord) {
|
||||
registerVisible.value = true
|
||||
}
|
||||
|
||||
async function loadPending() {
|
||||
try {
|
||||
pending.value = await getTahesabPendingSummary(props.customerId)
|
||||
} catch {
|
||||
pending.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFailedDocs() {
|
||||
failedLoading.value = true
|
||||
try {
|
||||
const res = await getTahesabPendingDocs(props.customerId, 200)
|
||||
failedRows.value = res.rows || []
|
||||
pending.value = res.summary
|
||||
} catch {
|
||||
failedRows.value = []
|
||||
} finally {
|
||||
failedLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runSyncOne(record: TahesabRecord) {
|
||||
const target = record.sync_target
|
||||
if (!target) {
|
||||
if (record.id) {
|
||||
openSend(record)
|
||||
return
|
||||
}
|
||||
message.warning('هدف ثبت مشخص نیست')
|
||||
return
|
||||
}
|
||||
const key = failedRowKey(record)
|
||||
syncingRowKey.value = key
|
||||
try {
|
||||
const result = await syncOneTahesab(target.type, target.id)
|
||||
if (result.ok) {
|
||||
message.success(result.message || 'ثبت شد')
|
||||
infoVisible.value = false
|
||||
await afterRowSync()
|
||||
} else {
|
||||
message.error(result.message || 'ثبت ناموفق بود')
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.message || e?.message || 'خطا در ثبت')
|
||||
} finally {
|
||||
syncingRowKey.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function runSyncAll() {
|
||||
Modal.confirm({
|
||||
title: 'ثبت همه در تهحساب',
|
||||
content: props.customerId
|
||||
? 'همهٔ سندهای ناموفق/جاافتادهٔ این مشتری ارسال میشود (حداکثر ۸۰). ادامه؟'
|
||||
: 'حداکثر ۸۰ سند ناموفق/جاافتاده به تهحساب ارسال میشود. ادامه؟',
|
||||
okText: 'ثبت همه',
|
||||
cancelText: 'انصراف',
|
||||
async onOk() {
|
||||
syncing.value = true
|
||||
try {
|
||||
const result = await syncAllTahesabPending(80, props.customerId)
|
||||
const parts = [
|
||||
result.retried ? `${result.retried} ارسال مجدد` : '',
|
||||
result.backfilled_withdraws ? `${result.backfilled_withdraws} شارژ` : '',
|
||||
result.backfilled_orders ? `${result.backfilled_orders} سفارش` : '',
|
||||
result.failed ? `${result.failed} ناموفق` : ''
|
||||
].filter(Boolean)
|
||||
if (result.ok) {
|
||||
message.success(parts.join(' — ') || 'همگامسازی انجام شد')
|
||||
} else {
|
||||
message.warning(parts.join(' — ') || 'بخشی از موارد ناموفق بود')
|
||||
}
|
||||
pending.value = result.summary
|
||||
await afterRowSync()
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.message || e?.message || 'خطا در همگامسازی')
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function afterRowSync() {
|
||||
await loadPending()
|
||||
await fetch()
|
||||
}
|
||||
|
||||
async function onViewChange() {
|
||||
await fetch()
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
if (docsView.value === 'failed') {
|
||||
await loadFailedDocs()
|
||||
return
|
||||
}
|
||||
if (isMobile.value) {
|
||||
await nextTick()
|
||||
await mobileListRef.value?.fetch?.()
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
await reload()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.customerId,
|
||||
async () => {
|
||||
docsView.value = 'failed'
|
||||
await loadPending()
|
||||
await fetch()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadPending()
|
||||
if ((pending.value?.total ?? 0) > 0) {
|
||||
docsView.value = 'failed'
|
||||
} else {
|
||||
docsView.value = 'all'
|
||||
}
|
||||
await fetch()
|
||||
})
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,115 @@
|
||||
<template>
|
||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||
<BasicShowContext title="اسناد زرین">
|
||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<BasicShowContext :title="customerId ? 'اسناد زرین این مشتری' : 'اسناد زرین'">
|
||||
<template #extra>
|
||||
<a-button
|
||||
v-if="(pending?.total ?? 0) > 0"
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="syncing"
|
||||
@click="runSyncAll"
|
||||
>
|
||||
ثبت همه ({{ pending?.total }})
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="(pending?.total ?? 0) > 0"
|
||||
class="mb-3 rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900"
|
||||
>
|
||||
<div class="font-700">
|
||||
{{ pending?.total }} سند جاافتاده / ناموفق برای زرین
|
||||
<span v-if="customerId">این مشتری</span>
|
||||
</div>
|
||||
<div class="mt-1 opacity-90">
|
||||
خطا خورده در جدول: {{ pending?.failed_rows }} —
|
||||
شارژ جاافتاده: {{ pending?.missing_withdraws }} —
|
||||
سفارش جاافتاده: {{ pending?.missing_orders }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:activeKey="docsView" class="mb-2" @change="onViewChange">
|
||||
<a-tab-pane key="failed">
|
||||
<template #tab>
|
||||
خطا خورده / جاافتاده
|
||||
<span v-if="(pending?.total ?? 0) > 0">({{ pending?.total }})</span>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="ok" tab="سندهای سالم" />
|
||||
<a-tab-pane key="all" tab="کل اسناد" />
|
||||
</a-tabs>
|
||||
|
||||
<div v-if="docsView === 'failed'" class="min-w-0 w-full overflow-x-auto">
|
||||
<a-spin :spinning="failedLoading">
|
||||
<a-table
|
||||
:columns="failedColumns"
|
||||
:data-source="failedRows"
|
||||
:pagination="{ pageSize: 20, showSizeChanger: true }"
|
||||
:row-key="failedRowKey"
|
||||
size="small"
|
||||
:locale="{ emptyText: 'مورد خطا خورده یا جاافتادهای نیست' }"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'row'">{{ index + 1 }}</template>
|
||||
<template v-else-if="column.key === 'issue'">
|
||||
<a-tag :color="record.issue_kind === 'missing' ? 'orange' : 'red'">
|
||||
{{ zarinIssueKindLabel(record.issue_kind) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'customer'">
|
||||
{{ record.customer?.name || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'amount'">
|
||||
{{ metalAmountLabel(record) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'rial'">
|
||||
{{ rialAmountLabel(record) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
{{ zarinStatusLabel(record.sync_status, record.zarin_id) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'time'">
|
||||
{{ record.created_at ? toJalali(record.created_at) : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<div class="flex justify-center gap-2">
|
||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
||||
<template #title><span>جزئیات</span></template>
|
||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</a-tooltip>
|
||||
<a-tooltip
|
||||
v-if="!record.zarin_id && !record.customer?.moshtari_code && record.customer_id"
|
||||
placement="bottom"
|
||||
class="cursor-pointer"
|
||||
@click="openRegister(record)"
|
||||
>
|
||||
<template #title><span>ثبت مشتری</span></template>
|
||||
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||
</a-tooltip>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
:loading="syncingRowKey === failedRowKey(record)"
|
||||
@click="runSyncOne(record)"
|
||||
>
|
||||
ثبت
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||
<BaseTable :key="docsView" v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<template #action="{ record }">
|
||||
<div class="flex justify-center gap-2">
|
||||
|
||||
<!-- دکمه ۱: اطلاعات کلی (همیشه نمایان) -->
|
||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
||||
<template #title><span>اطلاعات سند</span></template>
|
||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</a-tooltip>
|
||||
|
||||
<!-- دکمه ۲: ثبت مشتری در زرین (فقط اگر سند هنوز ثبت نشده و مشتری کد ندارد) -->
|
||||
<a-tooltip
|
||||
v-if="!record.zarin_id && !record.customer?.moshtari_code"
|
||||
placement="bottom"
|
||||
@@ -23,7 +120,6 @@
|
||||
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||
</a-tooltip>
|
||||
|
||||
<!-- دکمه ۳: ارسال به زرین (فقط اگر هنوز در زرین ثبت نشده) -->
|
||||
<a-tooltip
|
||||
v-if="!record.zarin_id"
|
||||
placement="bottom"
|
||||
@@ -33,16 +129,14 @@
|
||||
<template #title><span>ارسال به زرین</span></template>
|
||||
<Icon icon="vuesax-linear:send-2" color="#CA8A04" />
|
||||
</a-tooltip>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
|
||||
<ZarinMobileCardList v-else ref="mobileListRef" />
|
||||
<ZarinMobileCardList v-else-if="docsView !== 'failed'" ref="mobileListRef" />
|
||||
</BasicShowContext>
|
||||
|
||||
<!-- مودال اطلاعات کلی -->
|
||||
<BasicModal v-model:visible="infoVisible" title="اطلاعات سند" width="36rem" :footer="null">
|
||||
<div class="space-y-2 text-sm">
|
||||
<div
|
||||
@@ -53,134 +147,222 @@
|
||||
<span class="text-gray-500">{{ item.label }}</span>
|
||||
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
|
||||
</div>
|
||||
<div v-if="selected?.res" class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap">
|
||||
<div
|
||||
v-if="selected?.res"
|
||||
class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap"
|
||||
>
|
||||
{{ selected.res }}
|
||||
</div>
|
||||
<div v-if="selected?.sync_log" class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap">
|
||||
<div
|
||||
v-if="selected?.sync_log"
|
||||
class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap"
|
||||
>
|
||||
{{ selected.sync_log }}
|
||||
</div>
|
||||
<div v-if="canSyncSelected" class="pt-2">
|
||||
<a-button type="primary" block :loading="!!syncingRowKey" @click="runSyncOne(selected!)">
|
||||
ثبت در زرین
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
|
||||
<!-- مودال ارسال به زرین -->
|
||||
<ZarinRetryModal
|
||||
v-model="sendVisible"
|
||||
mode="retry-sanad"
|
||||
:zarin-id="selected?.id"
|
||||
@success="fetch"
|
||||
:zarin-id="selected?.id ?? undefined"
|
||||
@success="afterRowSync"
|
||||
/>
|
||||
|
||||
<!-- مودال ثبت مشتری در زرین -->
|
||||
<ZarinRetryModal
|
||||
v-model="registerVisible"
|
||||
mode="register-customer"
|
||||
:customer-id="selected?.customer_id"
|
||||
:customer-name="selected?.customer?.name"
|
||||
@success="fetch"
|
||||
@success="afterRowSync"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
import { getZarinTable, type ZarinRecord } from '@/api/zarin'
|
||||
import {
|
||||
getZarinPendingDocs,
|
||||
getZarinPendingSummary,
|
||||
getZarinTable,
|
||||
syncAllZarinPending,
|
||||
syncOneZarin,
|
||||
type ZarinPendingSummary,
|
||||
type ZarinRecord
|
||||
} from '@/api/zarin'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import BasicModal from '@/components/BasicModal.vue'
|
||||
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
|
||||
import ZarinMobileCardList from '@/pages/menuPages/accounting/components/ZarinMobileCardList.vue'
|
||||
import { zarinStatusLabel } from '@/pages/menuPages/accounting/utils/zarinStatus'
|
||||
import {
|
||||
zarinIssueKindLabel,
|
||||
zarinStatusLabel
|
||||
} from '@/pages/menuPages/accounting/utils/zarinStatus'
|
||||
import {
|
||||
metalAmountLabel,
|
||||
rialAmountLabel,
|
||||
rialAmountLabel
|
||||
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
|
||||
const props = defineProps<{
|
||||
customerId?: number
|
||||
}>()
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
const infoVisible = ref(false)
|
||||
const sendVisible = ref(false)
|
||||
const registerVisible = ref(false)
|
||||
const selected = ref<ZarinRecord | null>(null)
|
||||
const pending = ref<ZarinPendingSummary | null>(null)
|
||||
const syncing = ref(false)
|
||||
const docsView = ref<'failed' | 'ok' | 'all'>('failed')
|
||||
const failedRows = ref<ZarinRecord[]>([])
|
||||
const failedLoading = ref(false)
|
||||
const syncingRowKey = ref<string | null>(null)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => r.index + 1
|
||||
},
|
||||
{ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' },
|
||||
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
||||
{
|
||||
title: 'مقدار',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
type: 'Number',
|
||||
filterable: false,
|
||||
customRender: (r: any) => metalAmountLabel(r.record),
|
||||
},
|
||||
{
|
||||
title: 'مبلغ ریالی',
|
||||
dataIndex: 'rial_amount',
|
||||
key: 'rial_amount',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => rialAmountLabel(r.record),
|
||||
},
|
||||
{ title: 'شرح', dataIndex: 'desc', key: 'desc', type: 'Text' },
|
||||
{
|
||||
title: 'وضعیت',
|
||||
dataIndex: 'sync_status',
|
||||
key: 'sync_status',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
customRender: (r: any) => zarinStatusLabel(r.record.sync_status, r.record.zarin_id)
|
||||
},
|
||||
{
|
||||
title: 'زمان',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'Text',
|
||||
customRender: (r: any) => toJalali(r.record.created_at)
|
||||
},
|
||||
{
|
||||
title: 'عملیات',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
slot: 'action',
|
||||
filterable: false
|
||||
const columns = computed(() => {
|
||||
const cols: any[] = [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => r.index + 1
|
||||
}
|
||||
]
|
||||
if (!props.customerId) {
|
||||
cols.push({ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' })
|
||||
}
|
||||
]
|
||||
|
||||
const tblProps = ref({
|
||||
columns,
|
||||
api: (params: TableInput) => getZarinTable(params)
|
||||
cols.push(
|
||||
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
||||
{
|
||||
title: 'مقدار',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
type: 'Number',
|
||||
filterable: false,
|
||||
customRender: (r: any) => metalAmountLabel(r.record)
|
||||
},
|
||||
{
|
||||
title: 'مبلغ ریالی',
|
||||
dataIndex: 'rial_amount',
|
||||
key: 'rial_amount',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (r: any) => rialAmountLabel(r.record)
|
||||
},
|
||||
{ title: 'شرح', dataIndex: 'desc', key: 'desc', type: 'Text' },
|
||||
{
|
||||
title: 'وضعیت',
|
||||
dataIndex: 'sync_status',
|
||||
key: 'sync_status',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
customRender: (r: any) => zarinStatusLabel(r.record.sync_status, r.record.zarin_id)
|
||||
},
|
||||
{
|
||||
title: 'زمان',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'Text',
|
||||
customRender: (r: any) => toJalali(r.record.created_at)
|
||||
},
|
||||
{
|
||||
title: 'عملیات',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
slot: 'action',
|
||||
filterable: false
|
||||
}
|
||||
)
|
||||
return cols
|
||||
})
|
||||
|
||||
/** اطلاعاتی که برای هر سند حسابداری نیاز است (بدون جزئیات زرینمحور) */
|
||||
const failedColumns = computed(() => {
|
||||
const cols: any[] = [
|
||||
{ title: 'ردیف', key: 'row', width: 60 },
|
||||
{ title: 'نوع مشکل', key: 'issue', width: 110 }
|
||||
]
|
||||
if (!props.customerId) {
|
||||
cols.push({
|
||||
title: 'مشتری',
|
||||
key: 'customer'
|
||||
})
|
||||
}
|
||||
cols.push(
|
||||
{ title: 'نوع', dataIndex: 'type', key: 'type' },
|
||||
{ title: 'مقدار', key: 'amount' },
|
||||
{ title: 'مبلغ ریالی', key: 'rial' },
|
||||
{ title: 'شرح', dataIndex: 'desc', key: 'desc', ellipsis: true },
|
||||
{ title: 'وضعیت', key: 'status' },
|
||||
{ title: 'زمان', key: 'time', width: 140 },
|
||||
{ title: 'عملیات', key: 'action', width: 140 }
|
||||
)
|
||||
return cols
|
||||
})
|
||||
|
||||
const tblProps = computed(() => ({
|
||||
columns: columns.value,
|
||||
api: (params: TableInput) => {
|
||||
if (props.customerId) {
|
||||
params.filters = params.filters || []
|
||||
params.filters.push({
|
||||
field: 'customer_id',
|
||||
type: 'text',
|
||||
value: props.customerId,
|
||||
cr: 'and',
|
||||
op: 'eq',
|
||||
hidden: false
|
||||
})
|
||||
}
|
||||
return getZarinTable({ ...params, view: docsView.value })
|
||||
}
|
||||
}))
|
||||
|
||||
const infoItems = computed(() => {
|
||||
const row = selected.value
|
||||
if (!row) return []
|
||||
return [
|
||||
{ label: 'شناسه سند', value: row.id },
|
||||
{ label: 'شناسه سند', value: row.id ?? '—' },
|
||||
{ label: 'نوع مشکل', value: row.issue_kind ? zarinIssueKindLabel(row.issue_kind) : '—' },
|
||||
{ label: 'مشتری', value: row.customer?.name },
|
||||
{ label: 'موبایل', value: row.customer?.mobile },
|
||||
{ label: 'نوع', value: row.type },
|
||||
{ label: 'مقدار', value: metalAmountLabel(row) },
|
||||
{ label: 'مبلغ ریالی', value: rialAmountLabel(row) },
|
||||
{ label: 'شرح', value: row.desc },
|
||||
{ label: 'سفارش', value: row.order_id || '-' },
|
||||
{ label: 'برداشت', value: row.withdraw_id || '-' },
|
||||
{ label: 'زمان ثبت', value: row.created_at ? toJalali(row.created_at) : '-' },
|
||||
{ label: 'وضعیت', value: zarinStatusLabel(row.sync_status, row.zarin_id) },
|
||||
{ label: 'کد زرین', value: row.zarin_id || 'ثبت نشده' }
|
||||
]
|
||||
})
|
||||
|
||||
const canSyncSelected = computed(() => {
|
||||
const row = selected.value
|
||||
if (!row?.sync_target) return false
|
||||
return !row.zarin_id
|
||||
})
|
||||
|
||||
function failedRowKey(record: ZarinRecord) {
|
||||
if (record.sync_target) return `${record.sync_target.type}-${record.sync_target.id}`
|
||||
if (record.id) return `row-${record.id}`
|
||||
return `x-${record.withdraw_id || record.order_id || Math.random()}`
|
||||
}
|
||||
|
||||
function openInfo(record: ZarinRecord) {
|
||||
selected.value = record
|
||||
infoVisible.value = true
|
||||
@@ -196,13 +378,130 @@ function openRegister(record: ZarinRecord) {
|
||||
registerVisible.value = true
|
||||
}
|
||||
|
||||
async function loadPending() {
|
||||
try {
|
||||
pending.value = await getZarinPendingSummary(props.customerId)
|
||||
} catch {
|
||||
pending.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFailedDocs() {
|
||||
failedLoading.value = true
|
||||
try {
|
||||
const res = await getZarinPendingDocs(props.customerId, 200)
|
||||
failedRows.value = res.rows || []
|
||||
pending.value = res.summary
|
||||
} catch {
|
||||
failedRows.value = []
|
||||
} finally {
|
||||
failedLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runSyncOne(record: ZarinRecord) {
|
||||
const target = record.sync_target
|
||||
if (!target) {
|
||||
if (record.id) {
|
||||
openSend(record)
|
||||
return
|
||||
}
|
||||
message.warning('هدف ثبت مشخص نیست')
|
||||
return
|
||||
}
|
||||
const key = failedRowKey(record)
|
||||
syncingRowKey.value = key
|
||||
try {
|
||||
const result = await syncOneZarin(target.type, target.id)
|
||||
if (result.ok) {
|
||||
message.success(result.message || 'ثبت شد')
|
||||
infoVisible.value = false
|
||||
await afterRowSync()
|
||||
} else {
|
||||
message.error(result.message || 'ثبت ناموفق بود')
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.message || e?.message || 'خطا در ثبت')
|
||||
} finally {
|
||||
syncingRowKey.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function runSyncAll() {
|
||||
Modal.confirm({
|
||||
title: 'ثبت همه در زرین',
|
||||
content: props.customerId
|
||||
? 'همهٔ سندهای ناموفق/جاافتادهٔ این مشتری ارسال میشود (حداکثر ۸۰). ادامه؟'
|
||||
: 'حداکثر ۸۰ سند ناموفق/جاافتاده به زرین ارسال میشود. ادامه؟',
|
||||
okText: 'ثبت همه',
|
||||
cancelText: 'انصراف',
|
||||
async onOk() {
|
||||
syncing.value = true
|
||||
try {
|
||||
const result = await syncAllZarinPending(80, props.customerId)
|
||||
const parts = [
|
||||
result.retried ? `${result.retried} ارسال مجدد` : '',
|
||||
result.backfilled_withdraws ? `${result.backfilled_withdraws} شارژ` : '',
|
||||
result.backfilled_orders ? `${result.backfilled_orders} سفارش` : '',
|
||||
result.failed ? `${result.failed} ناموفق` : ''
|
||||
].filter(Boolean)
|
||||
if (result.ok) {
|
||||
message.success(parts.join(' — ') || 'همگامسازی انجام شد')
|
||||
} else {
|
||||
message.warning(parts.join(' — ') || 'بخشی از موارد ناموفق بود')
|
||||
}
|
||||
pending.value = result.summary
|
||||
await afterRowSync()
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.message || e?.message || 'خطا در همگامسازی')
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function afterRowSync() {
|
||||
await loadPending()
|
||||
await fetch()
|
||||
}
|
||||
|
||||
async function onViewChange() {
|
||||
await fetch()
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
if (docsView.value === 'failed') {
|
||||
await loadFailedDocs()
|
||||
return
|
||||
}
|
||||
if (isMobile.value) {
|
||||
await nextTick()
|
||||
await mobileListRef.value?.fetch?.()
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
await reload()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.customerId,
|
||||
async () => {
|
||||
docsView.value = 'failed'
|
||||
await loadPending()
|
||||
await fetch()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadPending()
|
||||
if ((pending.value?.total ?? 0) > 0) {
|
||||
docsView.value = 'failed'
|
||||
} else {
|
||||
docsView.value = 'all'
|
||||
}
|
||||
await fetch()
|
||||
})
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
export function tahesabStatusLabel(status?: string, tahesabId?: string | null): string {
|
||||
if (tahesabId) return 'موفق'
|
||||
if (status === 'missing') return 'جاافتاده'
|
||||
if (status === 'failed') return 'ناموفق'
|
||||
if (status === 'success') return 'موفق'
|
||||
// pending = ردیف محلی ساخته شده ولی هنوز در نرمافزار تهحساب ثبت نشده (نه «در صف»)
|
||||
return 'ارسال نشده'
|
||||
}
|
||||
|
||||
export function canRetryTahesab(record: { tahesab_id?: string | null; sync_status?: string }): boolean {
|
||||
return !record.tahesab_id || record.sync_status === 'failed'
|
||||
export function tahesabIssueKindLabel(kind?: string): string {
|
||||
if (kind === 'missing') return 'جاافتاده'
|
||||
return 'خطا خورده'
|
||||
}
|
||||
|
||||
export function canRetryTahesab(record: {
|
||||
tahesab_id?: string | null
|
||||
sync_status?: string
|
||||
}): boolean {
|
||||
return !record.tahesab_id || record.sync_status === 'failed' || record.sync_status === 'missing'
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
export function zarinStatusLabel(status?: string, zarinId?: string | null): string {
|
||||
if (zarinId) return 'موفق'
|
||||
if (status === 'missing') return 'جاافتاده'
|
||||
if (status === 'failed') return 'ناموفق'
|
||||
if (status === 'success') return 'موفق'
|
||||
// pending = ردیف محلی ساخته شده ولی هنوز در نرمافزار زرین ثبت نشده (نه «در صف»)
|
||||
return 'ارسال نشده'
|
||||
}
|
||||
|
||||
export function canRetryZarin(record: { zarin_id?: string | null; sync_status?: string }): boolean {
|
||||
return !record.zarin_id || record.sync_status === 'failed'
|
||||
export function zarinIssueKindLabel(kind?: string): string {
|
||||
if (kind === 'missing') return 'جاافتاده'
|
||||
return 'خطا خورده'
|
||||
}
|
||||
|
||||
export function canRetryZarin(record: { zarin_id?: string | null; sync_status?: string }): boolean {
|
||||
return !record.zarin_id || record.sync_status === 'failed' || record.sync_status === 'missing'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<tahesabTbl ref="inner" :customer-id="customerId" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import tahesabTbl from '@/pages/menuPages/accounting/components/tahesabTable.vue'
|
||||
|
||||
defineProps<{
|
||||
customerId?: number
|
||||
}>()
|
||||
|
||||
const inner = ref<{ fetch?: () => void | Promise<void> } | null>(null)
|
||||
|
||||
defineExpose({
|
||||
fetch: () => inner.value?.fetch?.()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<zarinTbl ref="inner" :customer-id="customerId" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import zarinTbl from '@/pages/menuPages/accounting/components/zarinTable.vue'
|
||||
|
||||
defineProps<{
|
||||
customerId?: number
|
||||
}>()
|
||||
|
||||
const inner = ref<{ fetch?: () => void | Promise<void> } | null>(null)
|
||||
|
||||
defineExpose({
|
||||
fetch: () => inner.value?.fetch?.()
|
||||
})
|
||||
</script>
|
||||
@@ -249,6 +249,12 @@
|
||||
<a-tab-pane v-if="pargarWalletSyncVisible" key="لاگ پرگار" tab="لاگ پرگار">
|
||||
<CustomerPargarLogsTable ref="pargarLogsTable" :customer-id="userInfo?.id" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane v-if="tahesabDocsVisible" key="اسناد تهحساب" tab="اسناد تهحساب">
|
||||
<CustomerTahesabDocsTable ref="tahesabDocsTable" :customer-id="userInfo?.id" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane v-if="zarinDocsVisible" key="اسناد زرین" tab="اسناد زرین">
|
||||
<CustomerZarinDocsTable ref="zarinDocsTable" :customer-id="userInfo?.id" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
v-if="conditionalOrderEnabled"
|
||||
key="سفارش شرطی"
|
||||
@@ -310,6 +316,8 @@ import FundsTable from '@/pages/menuPages/customers/base/components/fundsTable.v
|
||||
import PiggyFundsTable from '@/pages/menuPages/customers/base/components/piggyFundsTable.vue'
|
||||
import ReferralsTable from '@/pages/menuPages/customers/base/components/referralsTable.vue'
|
||||
import CustomerPargarLogsTable from '@/pages/menuPages/customers/base/components/CustomerPargarLogsTable.vue'
|
||||
import CustomerTahesabDocsTable from '@/pages/menuPages/customers/base/components/CustomerTahesabDocsTable.vue'
|
||||
import CustomerZarinDocsTable from '@/pages/menuPages/customers/base/components/CustomerZarinDocsTable.vue'
|
||||
import CustomerConditionalOrderTab from '@/pages/menuPages/customers/base/components/CustomerConditionalOrderTab.vue'
|
||||
import type { Order } from '@/model/order'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
@@ -384,6 +392,8 @@ const referralTable = ref(null)
|
||||
const fundTable = ref(null)
|
||||
const piggyFundTable = ref(null)
|
||||
const pargarLogsTable = ref<{ fetch?: () => void } | null>(null)
|
||||
const tahesabDocsTable = ref<{ fetch?: () => void } | null>(null)
|
||||
const zarinDocsTable = ref<{ fetch?: () => void } | null>(null)
|
||||
const refreshTable = () => table?.value?.fetch()
|
||||
const refreshOrderTable = () => orderTable?.value?.fetch()
|
||||
const refreshTransactionTable = () => transactionTable?.value?.fetch()
|
||||
@@ -392,6 +402,8 @@ const refreshReferralTable = () => referralTable?.value?.fetch()
|
||||
const refreshFundTable = () => fundTable?.value?.fetch()
|
||||
const refreshPiggyFundTable = () => piggyFundTable?.value?.fetch()
|
||||
const refreshPargarLogsTable = () => pargarLogsTable.value?.fetch?.()
|
||||
const refreshTahesabDocsTable = () => tahesabDocsTable.value?.fetch?.()
|
||||
const refreshZarinDocsTable = () => zarinDocsTable.value?.fetch?.()
|
||||
|
||||
let updateKey = ref<number>(0)
|
||||
|
||||
@@ -560,6 +572,14 @@ async function refreshInfoTab(key: string) {
|
||||
await nextTick()
|
||||
refreshPargarLogsTable()
|
||||
break
|
||||
case 'اسناد تهحساب':
|
||||
await nextTick()
|
||||
refreshTahesabDocsTable()
|
||||
break
|
||||
case 'اسناد زرین':
|
||||
await nextTick()
|
||||
refreshZarinDocsTable()
|
||||
break
|
||||
case 'سفارش شرطی':
|
||||
await refetchCustomerProfile()
|
||||
break
|
||||
@@ -723,6 +743,9 @@ const pargarWalletSyncVisible = computed(
|
||||
() => accountingProviderSetting.value === 'pargar'
|
||||
)
|
||||
|
||||
const tahesabDocsVisible = computed(() => accountingProviderSetting.value === 'tahesab')
|
||||
const zarinDocsVisible = computed(() => accountingProviderSetting.value === 'zarin')
|
||||
|
||||
const pargarWalletCanSync = computed(() =>
|
||||
isValidAccountingCode(userInfo.value?.pargar_account_id)
|
||||
)
|
||||
|
||||
@@ -351,6 +351,16 @@
|
||||
show-count
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item name="karat" class="!mb-0 max-w-xs">
|
||||
<template #label>
|
||||
<DashboardLabelWithHelp label="عیار" :help="productKaratHelp" size="sm" />
|
||||
</template>
|
||||
<BasicInput
|
||||
v-model:value="form.karat"
|
||||
placeholder="مثلاً ۲۴ (اختیاری)"
|
||||
:maxlength="32"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<!-- وزن و واحد -->
|
||||
@@ -684,6 +694,11 @@ const productDescriptionHelp: DashboardHelp = {
|
||||
'متن توضیح محصول که در اپ مشتری، بعد از انتخاب محصول در صفحه خرید/فروش نمایش داده میشود (اگر سوییچ «نمایش جزئیات اطلاعات محصول» در تنظیمات روشن باشد).',
|
||||
}
|
||||
|
||||
const productKaratHelp: DashboardHelp = {
|
||||
description:
|
||||
'عیار نمایشی در مشخصات محصول اپ مشتری (مثلاً ۲۴ یا ۹۹۹). اگر خالی باشد، ردیف عیار در اپ نشان داده نمیشود.',
|
||||
}
|
||||
|
||||
const categoryHelp: DashboardHelp = {
|
||||
description: 'دستهبندی محصول در تبهای اپ (سکه، شمش، آبشده و …).',
|
||||
}
|
||||
|
||||
@@ -457,6 +457,7 @@ function resetData() {
|
||||
min_physical_delivery_quantity: undefined,
|
||||
inventory: 0,
|
||||
description: '',
|
||||
karat: '',
|
||||
min_order_cash: undefined,
|
||||
max_order_cash: undefined,
|
||||
auto_open_at: dayjs('00:00:00', 'HH:mm:ss'),
|
||||
@@ -482,6 +483,7 @@ const PRICING_FIELDS = new Set([
|
||||
const DETAILS_FIELDS = new Set([
|
||||
'title',
|
||||
'description',
|
||||
'karat',
|
||||
'group_type',
|
||||
'metal_id',
|
||||
'amount',
|
||||
|
||||
Reference in New Issue
Block a user