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'
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
|
|
||||||
export interface TahesabRecord {
|
export interface TahesabRecord {
|
||||||
id: number
|
id?: number | null
|
||||||
customer_id?: number
|
customer_id?: number
|
||||||
amount?: number
|
amount?: number
|
||||||
desc?: string
|
desc?: string
|
||||||
type?: string
|
type?: string
|
||||||
sync_status?: 'pending' | 'success' | 'failed'
|
sync_status?: 'pending' | 'success' | 'failed' | 'missing'
|
||||||
sync_log?: string | null
|
sync_log?: string | null
|
||||||
tahesab_id?: string | null
|
tahesab_id?: string | null
|
||||||
data?: string | null
|
data?: string | null
|
||||||
res?: string | null
|
res?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
withdraw_id?: number | null
|
||||||
|
order_id?: number | null
|
||||||
|
issue_kind?: 'failed' | 'missing'
|
||||||
|
sync_target?: { type: 'row' | 'withdraw' | 'order'; id: number }
|
||||||
customer?: {
|
customer?: {
|
||||||
id?: number
|
id?: number
|
||||||
name?: string
|
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)
|
await useAxios.post<TahesabRecord[]>('/admin/tahesab/table', params)
|
||||||
|
|
||||||
export const getTahesabDetail = async (id: number) =>
|
export const getTahesabDetail = async (id: number) =>
|
||||||
@@ -30,6 +34,43 @@ export const getTahesabDetail = async (id: number) =>
|
|||||||
export const retryTahesabSanad = async (id: number) =>
|
export const retryTahesabSanad = async (id: number) =>
|
||||||
await useAxios.post<{ ok: boolean; message: string; tahesab?: TahesabRecord }>(`/admin/tahesab/${id}/retry`)
|
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) =>
|
export const registerCustomerInTahesab = async (customerId: number) =>
|
||||||
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
||||||
`/admin/customer/${customerId}/tahesab/register`
|
`/admin/customer/${customerId}/tahesab/register`
|
||||||
|
|||||||
+44
-3
@@ -2,17 +2,21 @@ import useAxios from '@core/utils/useAxios'
|
|||||||
import type { TableInput } from '@core/type/baseTable'
|
import type { TableInput } from '@core/type/baseTable'
|
||||||
|
|
||||||
export interface ZarinRecord {
|
export interface ZarinRecord {
|
||||||
id: number
|
id?: number | null
|
||||||
customer_id?: number
|
customer_id?: number
|
||||||
amount?: number
|
amount?: number
|
||||||
desc?: string
|
desc?: string
|
||||||
type?: string
|
type?: string
|
||||||
sync_status?: 'pending' | 'success' | 'failed'
|
sync_status?: 'pending' | 'success' | 'failed' | 'missing'
|
||||||
sync_log?: string | null
|
sync_log?: string | null
|
||||||
zarin_id?: string | null
|
zarin_id?: string | null
|
||||||
data?: string | null
|
data?: string | null
|
||||||
res?: string | null
|
res?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
|
withdraw_id?: number | null
|
||||||
|
order_id?: number | null
|
||||||
|
issue_kind?: 'failed' | 'missing'
|
||||||
|
sync_target?: { type: 'row' | 'withdraw' | 'order'; id: number }
|
||||||
customer?: {
|
customer?: {
|
||||||
id?: number
|
id?: number
|
||||||
name?: string
|
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)
|
await useAxios.post<ZarinRecord[]>('/admin/zarin/table', params)
|
||||||
|
|
||||||
export const getZarinDetail = async (id: number) =>
|
export const getZarinDetail = async (id: number) =>
|
||||||
@@ -30,6 +34,43 @@ export const getZarinDetail = async (id: number) =>
|
|||||||
export const retryZarinSanad = async (id: number) =>
|
export const retryZarinSanad = async (id: number) =>
|
||||||
await useAxios.post<{ ok: boolean; message: string; zarin?: ZarinRecord }>(`/admin/zarin/${id}/retry`)
|
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) =>
|
export const registerCustomerInZarin = async (customerId: number) =>
|
||||||
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
await useAxios.post<{ ok: boolean; message: string; moshtari_code?: string | number }>(
|
||||||
`/admin/customer/${customerId}/zarin/register`
|
`/admin/customer/${customerId}/zarin/register`
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export interface Product{
|
|||||||
original_image?: File
|
original_image?: File
|
||||||
inventory?: number
|
inventory?: number
|
||||||
description?: string | null
|
description?: string | null
|
||||||
|
karat?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MethodePrices {
|
export interface MethodePrices {
|
||||||
|
|||||||
@@ -1,18 +1,116 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||||
<BasicShowContext title="اسناد تهحساب">
|
<BasicShowContext :title="customerId ? 'اسناد تهحساب این مشتری' : 'اسناد تهحساب'">
|
||||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
<template #extra>
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<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 }">
|
<template #action="{ record }">
|
||||||
<div class="flex justify-center gap-2">
|
<div class="flex justify-center gap-2">
|
||||||
|
|
||||||
<!-- دکمه ۱: اطلاعات کلی (همیشه نمایان) -->
|
|
||||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
||||||
<template #title><span>اطلاعات سند</span></template>
|
<template #title><span>اطلاعات سند</span></template>
|
||||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
<!-- دکمه ۲: ثبت مشتری در تهحساب (فقط اگر سند هنوز ثبت نشده و مشتری کد ندارد) -->
|
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
v-if="!record.tahesab_id && !record.customer?.moshtari_code"
|
v-if="!record.tahesab_id && !record.customer?.moshtari_code"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
@@ -23,7 +121,6 @@
|
|||||||
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
<!-- دکمه ۳: ارسال به تهحساب (فقط اگر هنوز در تهحساب ثبت نشده) -->
|
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
v-if="!record.tahesab_id"
|
v-if="!record.tahesab_id"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
@@ -33,16 +130,14 @@
|
|||||||
<template #title><span>ارسال به تهحساب</span></template>
|
<template #title><span>ارسال به تهحساب</span></template>
|
||||||
<Icon icon="vuesax-linear:send-2" color="#CA8A04" />
|
<Icon icon="vuesax-linear:send-2" color="#CA8A04" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BaseTable>
|
</BaseTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TahesabMobileCardList v-else ref="mobileListRef" />
|
<TahesabMobileCardList v-else-if="docsView !== 'failed'" ref="mobileListRef" />
|
||||||
</BasicShowContext>
|
</BasicShowContext>
|
||||||
|
|
||||||
<!-- مودال اطلاعات کلی -->
|
|
||||||
<BasicModal v-model:visible="infoVisible" title="اطلاعات سند" width="36rem" :footer="null">
|
<BasicModal v-model:visible="infoVisible" title="اطلاعات سند" width="36rem" :footer="null">
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div
|
<div
|
||||||
@@ -53,134 +148,222 @@
|
|||||||
<span class="text-gray-500">{{ item.label }}</span>
|
<span class="text-gray-500">{{ item.label }}</span>
|
||||||
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
|
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
|
||||||
</div>
|
</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 }}
|
{{ selected.res }}
|
||||||
</div>
|
</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 }}
|
{{ selected.sync_log }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="canSyncSelected" class="pt-2">
|
||||||
|
<a-button type="primary" block :loading="!!syncingRowKey" @click="runSyncOne(selected!)">
|
||||||
|
ثبت در تهحساب
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
|
|
||||||
<!-- مودال ارسال به تهحساب -->
|
|
||||||
<TahesabRetryModal
|
<TahesabRetryModal
|
||||||
v-model="sendVisible"
|
v-model="sendVisible"
|
||||||
mode="retry-sanad"
|
mode="retry-sanad"
|
||||||
:tahesab-id="selected?.id"
|
:tahesab-id="selected?.id ?? undefined"
|
||||||
@success="fetch"
|
@success="afterRowSync"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- مودال ثبت مشتری در تهحساب -->
|
|
||||||
<TahesabRetryModal
|
<TahesabRetryModal
|
||||||
v-model="registerVisible"
|
v-model="registerVisible"
|
||||||
mode="register-customer"
|
mode="register-customer"
|
||||||
:customer-id="selected?.customer_id"
|
:customer-id="selected?.customer_id"
|
||||||
:customer-name="selected?.customer?.name"
|
:customer-name="selected?.customer?.name"
|
||||||
@success="fetch"
|
@success="afterRowSync"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
import type { TableInput } from '@core/type/baseTable'
|
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 Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
||||||
import TahesabMobileCardList from '@/pages/menuPages/accounting/components/TahesabMobileCardList.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 {
|
import {
|
||||||
metalAmountLabel,
|
metalAmountLabel,
|
||||||
rialAmountLabel,
|
rialAmountLabel
|
||||||
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
customerId?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
const infoVisible = ref(false)
|
const infoVisible = ref(false)
|
||||||
const sendVisible = ref(false)
|
const sendVisible = ref(false)
|
||||||
const registerVisible = ref(false)
|
const registerVisible = ref(false)
|
||||||
const selected = ref<TahesabRecord | null>(null)
|
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 = [
|
const columns = computed(() => {
|
||||||
{
|
const cols: any[] = [
|
||||||
title: 'ردیف',
|
{
|
||||||
dataIndex: 'id',
|
title: 'ردیف',
|
||||||
key: 'id',
|
dataIndex: 'id',
|
||||||
filterable: false,
|
key: 'id',
|
||||||
sortable: false,
|
filterable: false,
|
||||||
customRender: (r: any) => r.index + 1
|
sortable: false,
|
||||||
},
|
customRender: (r: any) => r.index + 1
|
||||||
{ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' },
|
}
|
||||||
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
]
|
||||||
{
|
if (!props.customerId) {
|
||||||
title: 'مقدار',
|
cols.push({ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' })
|
||||||
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
|
|
||||||
}
|
}
|
||||||
]
|
cols.push(
|
||||||
|
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
||||||
const tblProps = ref({
|
{
|
||||||
columns,
|
title: 'مقدار',
|
||||||
api: (params: TableInput) => getTahesabTable(params)
|
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 infoItems = computed(() => {
|
||||||
const row = selected.value
|
const row = selected.value
|
||||||
if (!row) return []
|
if (!row) return []
|
||||||
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?.name },
|
||||||
{ label: 'موبایل', value: row.customer?.mobile },
|
{ label: 'موبایل', value: row.customer?.mobile },
|
||||||
{ label: 'نوع', value: row.type },
|
{ label: 'نوع', value: row.type },
|
||||||
{ label: 'مقدار', value: metalAmountLabel(row) },
|
{ label: 'مقدار', value: metalAmountLabel(row) },
|
||||||
{ label: 'مبلغ ریالی', value: rialAmountLabel(row) },
|
{ label: 'مبلغ ریالی', value: rialAmountLabel(row) },
|
||||||
{ label: 'شرح', value: row.desc },
|
{ 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: row.created_at ? toJalali(row.created_at) : '-' },
|
||||||
{ label: 'وضعیت', value: tahesabStatusLabel(row.sync_status, row.tahesab_id) },
|
{ label: 'وضعیت', value: tahesabStatusLabel(row.sync_status, row.tahesab_id) },
|
||||||
{ label: 'کد تهحساب', value: 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) {
|
function openInfo(record: TahesabRecord) {
|
||||||
selected.value = record
|
selected.value = record
|
||||||
infoVisible.value = true
|
infoVisible.value = true
|
||||||
@@ -196,13 +379,130 @@ function openRegister(record: TahesabRecord) {
|
|||||||
registerVisible.value = true
|
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() {
|
async function fetch() {
|
||||||
|
if (docsView.value === 'failed') {
|
||||||
|
await loadFailedDocs()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
|
await nextTick()
|
||||||
await mobileListRef.value?.fetch?.()
|
await mobileListRef.value?.fetch?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await nextTick()
|
||||||
await reload()
|
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 })
|
defineExpose({ fetch })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,18 +1,115 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||||
<BasicShowContext title="اسناد زرین">
|
<BasicShowContext :title="customerId ? 'اسناد زرین این مشتری' : 'اسناد زرین'">
|
||||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
<template #extra>
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<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 }">
|
<template #action="{ record }">
|
||||||
<div class="flex justify-center gap-2">
|
<div class="flex justify-center gap-2">
|
||||||
|
|
||||||
<!-- دکمه ۱: اطلاعات کلی (همیشه نمایان) -->
|
|
||||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
||||||
<template #title><span>اطلاعات سند</span></template>
|
<template #title><span>اطلاعات سند</span></template>
|
||||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
<!-- دکمه ۲: ثبت مشتری در زرین (فقط اگر سند هنوز ثبت نشده و مشتری کد ندارد) -->
|
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
v-if="!record.zarin_id && !record.customer?.moshtari_code"
|
v-if="!record.zarin_id && !record.customer?.moshtari_code"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
@@ -23,7 +120,6 @@
|
|||||||
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
<!-- دکمه ۳: ارسال به زرین (فقط اگر هنوز در زرین ثبت نشده) -->
|
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
v-if="!record.zarin_id"
|
v-if="!record.zarin_id"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
@@ -33,16 +129,14 @@
|
|||||||
<template #title><span>ارسال به زرین</span></template>
|
<template #title><span>ارسال به زرین</span></template>
|
||||||
<Icon icon="vuesax-linear:send-2" color="#CA8A04" />
|
<Icon icon="vuesax-linear:send-2" color="#CA8A04" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BaseTable>
|
</BaseTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ZarinMobileCardList v-else ref="mobileListRef" />
|
<ZarinMobileCardList v-else-if="docsView !== 'failed'" ref="mobileListRef" />
|
||||||
</BasicShowContext>
|
</BasicShowContext>
|
||||||
|
|
||||||
<!-- مودال اطلاعات کلی -->
|
|
||||||
<BasicModal v-model:visible="infoVisible" title="اطلاعات سند" width="36rem" :footer="null">
|
<BasicModal v-model:visible="infoVisible" title="اطلاعات سند" width="36rem" :footer="null">
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div
|
<div
|
||||||
@@ -53,134 +147,222 @@
|
|||||||
<span class="text-gray-500">{{ item.label }}</span>
|
<span class="text-gray-500">{{ item.label }}</span>
|
||||||
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
|
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
|
||||||
</div>
|
</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 }}
|
{{ selected.res }}
|
||||||
</div>
|
</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 }}
|
{{ selected.sync_log }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="canSyncSelected" class="pt-2">
|
||||||
|
<a-button type="primary" block :loading="!!syncingRowKey" @click="runSyncOne(selected!)">
|
||||||
|
ثبت در زرین
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
|
|
||||||
<!-- مودال ارسال به زرین -->
|
|
||||||
<ZarinRetryModal
|
<ZarinRetryModal
|
||||||
v-model="sendVisible"
|
v-model="sendVisible"
|
||||||
mode="retry-sanad"
|
mode="retry-sanad"
|
||||||
:zarin-id="selected?.id"
|
:zarin-id="selected?.id ?? undefined"
|
||||||
@success="fetch"
|
@success="afterRowSync"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- مودال ثبت مشتری در زرین -->
|
|
||||||
<ZarinRetryModal
|
<ZarinRetryModal
|
||||||
v-model="registerVisible"
|
v-model="registerVisible"
|
||||||
mode="register-customer"
|
mode="register-customer"
|
||||||
:customer-id="selected?.customer_id"
|
:customer-id="selected?.customer_id"
|
||||||
:customer-name="selected?.customer?.name"
|
:customer-name="selected?.customer?.name"
|
||||||
@success="fetch"
|
@success="afterRowSync"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||||
import type { TableInput } from '@core/type/baseTable'
|
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 Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
|
import ZarinRetryModal from '@/components/ZarinRetryModal.vue'
|
||||||
import ZarinMobileCardList from '@/pages/menuPages/accounting/components/ZarinMobileCardList.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 {
|
import {
|
||||||
metalAmountLabel,
|
metalAmountLabel,
|
||||||
rialAmountLabel,
|
rialAmountLabel
|
||||||
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
} from '@/pages/menuPages/accounting/utils/accountingDocAmounts'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
customerId?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
const infoVisible = ref(false)
|
const infoVisible = ref(false)
|
||||||
const sendVisible = ref(false)
|
const sendVisible = ref(false)
|
||||||
const registerVisible = ref(false)
|
const registerVisible = ref(false)
|
||||||
const selected = ref<ZarinRecord | null>(null)
|
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 = [
|
const columns = computed(() => {
|
||||||
{
|
const cols: any[] = [
|
||||||
title: 'ردیف',
|
{
|
||||||
dataIndex: 'id',
|
title: 'ردیف',
|
||||||
key: 'id',
|
dataIndex: 'id',
|
||||||
filterable: false,
|
key: 'id',
|
||||||
sortable: false,
|
filterable: false,
|
||||||
customRender: (r: any) => r.index + 1
|
sortable: false,
|
||||||
},
|
customRender: (r: any) => r.index + 1
|
||||||
{ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' },
|
}
|
||||||
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
]
|
||||||
{
|
if (!props.customerId) {
|
||||||
title: 'مقدار',
|
cols.push({ title: 'مشتری', dataIndex: 'customer.name', key: 'customer.name', type: 'Text' })
|
||||||
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
|
|
||||||
}
|
}
|
||||||
]
|
cols.push(
|
||||||
|
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
||||||
const tblProps = ref({
|
{
|
||||||
columns,
|
title: 'مقدار',
|
||||||
api: (params: TableInput) => getZarinTable(params)
|
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 infoItems = computed(() => {
|
||||||
const row = selected.value
|
const row = selected.value
|
||||||
if (!row) return []
|
if (!row) return []
|
||||||
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?.name },
|
||||||
{ label: 'موبایل', value: row.customer?.mobile },
|
{ label: 'موبایل', value: row.customer?.mobile },
|
||||||
{ label: 'نوع', value: row.type },
|
{ label: 'نوع', value: row.type },
|
||||||
{ label: 'مقدار', value: metalAmountLabel(row) },
|
{ label: 'مقدار', value: metalAmountLabel(row) },
|
||||||
{ label: 'مبلغ ریالی', value: rialAmountLabel(row) },
|
{ label: 'مبلغ ریالی', value: rialAmountLabel(row) },
|
||||||
{ label: 'شرح', value: row.desc },
|
{ 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: row.created_at ? toJalali(row.created_at) : '-' },
|
||||||
{ label: 'وضعیت', value: zarinStatusLabel(row.sync_status, row.zarin_id) },
|
{ label: 'وضعیت', value: zarinStatusLabel(row.sync_status, row.zarin_id) },
|
||||||
{ label: 'کد زرین', value: 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) {
|
function openInfo(record: ZarinRecord) {
|
||||||
selected.value = record
|
selected.value = record
|
||||||
infoVisible.value = true
|
infoVisible.value = true
|
||||||
@@ -196,13 +378,130 @@ function openRegister(record: ZarinRecord) {
|
|||||||
registerVisible.value = true
|
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() {
|
async function fetch() {
|
||||||
|
if (docsView.value === 'failed') {
|
||||||
|
await loadFailedDocs()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
|
await nextTick()
|
||||||
await mobileListRef.value?.fetch?.()
|
await mobileListRef.value?.fetch?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await nextTick()
|
||||||
await reload()
|
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 })
|
defineExpose({ fetch })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
export function tahesabStatusLabel(status?: string, tahesabId?: string | null): string {
|
export function tahesabStatusLabel(status?: string, tahesabId?: string | null): string {
|
||||||
if (tahesabId) return 'موفق'
|
if (tahesabId) return 'موفق'
|
||||||
|
if (status === 'missing') return 'جاافتاده'
|
||||||
if (status === 'failed') return 'ناموفق'
|
if (status === 'failed') return 'ناموفق'
|
||||||
if (status === 'success') return 'موفق'
|
if (status === 'success') return 'موفق'
|
||||||
// pending = ردیف محلی ساخته شده ولی هنوز در نرمافزار تهحساب ثبت نشده (نه «در صف»)
|
// pending = ردیف محلی ساخته شده ولی هنوز در نرمافزار تهحساب ثبت نشده (نه «در صف»)
|
||||||
return 'ارسال نشده'
|
return 'ارسال نشده'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canRetryTahesab(record: { tahesab_id?: string | null; sync_status?: string }): boolean {
|
export function tahesabIssueKindLabel(kind?: string): string {
|
||||||
return !record.tahesab_id || record.sync_status === 'failed'
|
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 {
|
export function zarinStatusLabel(status?: string, zarinId?: string | null): string {
|
||||||
if (zarinId) return 'موفق'
|
if (zarinId) return 'موفق'
|
||||||
|
if (status === 'missing') return 'جاافتاده'
|
||||||
if (status === 'failed') return 'ناموفق'
|
if (status === 'failed') return 'ناموفق'
|
||||||
if (status === 'success') return 'موفق'
|
if (status === 'success') return 'موفق'
|
||||||
// pending = ردیف محلی ساخته شده ولی هنوز در نرمافزار زرین ثبت نشده (نه «در صف»)
|
// pending = ردیف محلی ساخته شده ولی هنوز در نرمافزار زرین ثبت نشده (نه «در صف»)
|
||||||
return 'ارسال نشده'
|
return 'ارسال نشده'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canRetryZarin(record: { zarin_id?: string | null; sync_status?: string }): boolean {
|
export function zarinIssueKindLabel(kind?: string): string {
|
||||||
return !record.zarin_id || record.sync_status === 'failed'
|
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="لاگ پرگار">
|
<a-tab-pane v-if="pargarWalletSyncVisible" key="لاگ پرگار" tab="لاگ پرگار">
|
||||||
<CustomerPargarLogsTable ref="pargarLogsTable" :customer-id="userInfo?.id" />
|
<CustomerPargarLogsTable ref="pargarLogsTable" :customer-id="userInfo?.id" />
|
||||||
</a-tab-pane>
|
</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
|
<a-tab-pane
|
||||||
v-if="conditionalOrderEnabled"
|
v-if="conditionalOrderEnabled"
|
||||||
key="سفارش شرطی"
|
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 PiggyFundsTable from '@/pages/menuPages/customers/base/components/piggyFundsTable.vue'
|
||||||
import ReferralsTable from '@/pages/menuPages/customers/base/components/referralsTable.vue'
|
import ReferralsTable from '@/pages/menuPages/customers/base/components/referralsTable.vue'
|
||||||
import CustomerPargarLogsTable from '@/pages/menuPages/customers/base/components/CustomerPargarLogsTable.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 CustomerConditionalOrderTab from '@/pages/menuPages/customers/base/components/CustomerConditionalOrderTab.vue'
|
||||||
import type { Order } from '@/model/order'
|
import type { Order } from '@/model/order'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
@@ -384,6 +392,8 @@ const referralTable = ref(null)
|
|||||||
const fundTable = ref(null)
|
const fundTable = ref(null)
|
||||||
const piggyFundTable = ref(null)
|
const piggyFundTable = ref(null)
|
||||||
const pargarLogsTable = ref<{ fetch?: () => void } | null>(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 refreshTable = () => table?.value?.fetch()
|
||||||
const refreshOrderTable = () => orderTable?.value?.fetch()
|
const refreshOrderTable = () => orderTable?.value?.fetch()
|
||||||
const refreshTransactionTable = () => transactionTable?.value?.fetch()
|
const refreshTransactionTable = () => transactionTable?.value?.fetch()
|
||||||
@@ -392,6 +402,8 @@ const refreshReferralTable = () => referralTable?.value?.fetch()
|
|||||||
const refreshFundTable = () => fundTable?.value?.fetch()
|
const refreshFundTable = () => fundTable?.value?.fetch()
|
||||||
const refreshPiggyFundTable = () => piggyFundTable?.value?.fetch()
|
const refreshPiggyFundTable = () => piggyFundTable?.value?.fetch()
|
||||||
const refreshPargarLogsTable = () => pargarLogsTable.value?.fetch?.()
|
const refreshPargarLogsTable = () => pargarLogsTable.value?.fetch?.()
|
||||||
|
const refreshTahesabDocsTable = () => tahesabDocsTable.value?.fetch?.()
|
||||||
|
const refreshZarinDocsTable = () => zarinDocsTable.value?.fetch?.()
|
||||||
|
|
||||||
let updateKey = ref<number>(0)
|
let updateKey = ref<number>(0)
|
||||||
|
|
||||||
@@ -560,6 +572,14 @@ async function refreshInfoTab(key: string) {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
refreshPargarLogsTable()
|
refreshPargarLogsTable()
|
||||||
break
|
break
|
||||||
|
case 'اسناد تهحساب':
|
||||||
|
await nextTick()
|
||||||
|
refreshTahesabDocsTable()
|
||||||
|
break
|
||||||
|
case 'اسناد زرین':
|
||||||
|
await nextTick()
|
||||||
|
refreshZarinDocsTable()
|
||||||
|
break
|
||||||
case 'سفارش شرطی':
|
case 'سفارش شرطی':
|
||||||
await refetchCustomerProfile()
|
await refetchCustomerProfile()
|
||||||
break
|
break
|
||||||
@@ -723,6 +743,9 @@ const pargarWalletSyncVisible = computed(
|
|||||||
() => accountingProviderSetting.value === 'pargar'
|
() => accountingProviderSetting.value === 'pargar'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tahesabDocsVisible = computed(() => accountingProviderSetting.value === 'tahesab')
|
||||||
|
const zarinDocsVisible = computed(() => accountingProviderSetting.value === 'zarin')
|
||||||
|
|
||||||
const pargarWalletCanSync = computed(() =>
|
const pargarWalletCanSync = computed(() =>
|
||||||
isValidAccountingCode(userInfo.value?.pargar_account_id)
|
isValidAccountingCode(userInfo.value?.pargar_account_id)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -351,6 +351,16 @@
|
|||||||
show-count
|
show-count
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- وزن و واحد -->
|
<!-- وزن و واحد -->
|
||||||
@@ -684,6 +694,11 @@ const productDescriptionHelp: DashboardHelp = {
|
|||||||
'متن توضیح محصول که در اپ مشتری، بعد از انتخاب محصول در صفحه خرید/فروش نمایش داده میشود (اگر سوییچ «نمایش جزئیات اطلاعات محصول» در تنظیمات روشن باشد).',
|
'متن توضیح محصول که در اپ مشتری، بعد از انتخاب محصول در صفحه خرید/فروش نمایش داده میشود (اگر سوییچ «نمایش جزئیات اطلاعات محصول» در تنظیمات روشن باشد).',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const productKaratHelp: DashboardHelp = {
|
||||||
|
description:
|
||||||
|
'عیار نمایشی در مشخصات محصول اپ مشتری (مثلاً ۲۴ یا ۹۹۹). اگر خالی باشد، ردیف عیار در اپ نشان داده نمیشود.',
|
||||||
|
}
|
||||||
|
|
||||||
const categoryHelp: DashboardHelp = {
|
const categoryHelp: DashboardHelp = {
|
||||||
description: 'دستهبندی محصول در تبهای اپ (سکه، شمش، آبشده و …).',
|
description: 'دستهبندی محصول در تبهای اپ (سکه، شمش، آبشده و …).',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ function resetData() {
|
|||||||
min_physical_delivery_quantity: undefined,
|
min_physical_delivery_quantity: undefined,
|
||||||
inventory: 0,
|
inventory: 0,
|
||||||
description: '',
|
description: '',
|
||||||
|
karat: '',
|
||||||
min_order_cash: undefined,
|
min_order_cash: undefined,
|
||||||
max_order_cash: undefined,
|
max_order_cash: undefined,
|
||||||
auto_open_at: dayjs('00:00:00', 'HH:mm:ss'),
|
auto_open_at: dayjs('00:00:00', 'HH:mm:ss'),
|
||||||
@@ -482,6 +483,7 @@ const PRICING_FIELDS = new Set([
|
|||||||
const DETAILS_FIELDS = new Set([
|
const DETAILS_FIELDS = new Set([
|
||||||
'title',
|
'title',
|
||||||
'description',
|
'description',
|
||||||
|
'karat',
|
||||||
'group_type',
|
'group_type',
|
||||||
'metal_id',
|
'metal_id',
|
||||||
'amount',
|
'amount',
|
||||||
|
|||||||
Reference in New Issue
Block a user