feat(accounting): Shayegan docs tab and fixed provider UI

Show Shayegan sync table/health in admin and always treat boursecu accounting as Shayegan.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-05 17:12:55 +03:30
co-authored by Cursor
parent 7e869902d6
commit 8a4f95da7d
5 changed files with 193 additions and 17 deletions
+17
View File
@@ -0,0 +1,17 @@
import request from '@/utils/http'
export function shayeganHealth() {
return request.get({ url: '/admin/shayegan/health' })
}
export function shayeganTable(data: Record<string, unknown>) {
return request.post({ url: '/admin/shayegan/table', data })
}
export function shayeganShow(id: number | string) {
return request.get({ url: `/admin/shayegan/${id}` })
}
export function shayeganRetry(id: number | string, force = false) {
return request.post({ url: `/admin/shayegan/${id}/retry`, data: { force } })
}
@@ -0,0 +1,145 @@
<template>
<div class="flex h-full min-h-0 w-full flex-col gap-2">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between shrink-0">
<div class="min-w-0">
<div class="font-medium">اسناد شایگان</div>
<div class="text-xs opacity-70">
وضعیت ارسال به حسابگر. تست اتصال فقط خواندنی است و سند آزمایشی در CYPRSD04 نمی‌سازد.
</div>
</div>
<div class="grid grid-cols-2 gap-2 sm:flex sm:w-auto">
<a-button :loading="healthLoading" @click="runHealth">تست اتصال</a-button>
<a-button type="primary" :loading="loading" @click="fetch">بروزرسانی</a-button>
</div>
</div>
<a-alert
v-if="healthMessage"
:type="healthOk ? 'success' : 'error'"
:message="healthMessage"
show-icon
closable
class="shrink-0"
@close="healthMessage = ''"
/>
<a-table
class="min-h-0 flex-1"
size="small"
:loading="loading"
:columns="columns"
:data-source="rows"
:pagination="pagination"
row-key="id"
@change="onTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'sync_status'">
<a-tag :color="statusColor(record.sync_status)">{{ record.sync_status }}</a-tag>
</template>
<template v-else-if="column.key === 'actions'">
<a-button
size="small"
:disabled="record.sync_status === 'success'"
@click="retry(record)"
>
ارسال مجدد
</a-button>
</template>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Modal, message } from 'ant-design-vue'
import { shayeganHealth, shayeganRetry, shayeganTable } from '@/api/shayegan'
const loading = ref(false)
const healthLoading = ref(false)
const healthOk = ref(false)
const healthMessage = ref('')
const rows = ref<any[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true
})
const columns = [
{ title: 'شناسه', dataIndex: 'id', key: 'id', width: 70 },
{ title: 'نوع', dataIndex: 'type', key: 'type', width: 110 },
{ title: 'مبلغ/مقدار', dataIndex: 'amount', key: 'amount', width: 110 },
{ title: 'وضعیت', dataIndex: 'sync_status', key: 'sync_status', width: 100 },
{ title: 'شماره شایگان', dataIndex: 'shayegan_number', key: 'shayegan_number', width: 120 },
{ title: 'توضیح', dataIndex: 'desc', key: 'desc' },
{ title: 'عملیات', key: 'actions', width: 120 }
]
function statusColor(status: string) {
if (status === 'success') return 'green'
if (status === 'failed') return 'red'
if (status === 'timeout') return 'orange'
return 'blue'
}
async function fetch() {
loading.value = true
try {
const res: any = await shayeganTable({
page: pagination.current,
per_page: pagination.pageSize
})
rows.value = res?.data ?? []
pagination.total = res?.total ?? 0
} catch (e: any) {
message.error(e?.message || 'خطا در دریافت اسناد شایگان')
} finally {
loading.value = false
}
}
function onTableChange(pag: any) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetch()
}
async function runHealth() {
healthLoading.value = true
healthMessage.value = ''
try {
const res: any = await shayeganHealth()
healthOk.value = !!res?.ok
healthMessage.value = res?.message || (res?.ok ? 'اتصال برقرار است' : 'اتصال ناموفق')
} catch (e: any) {
healthOk.value = false
healthMessage.value = e?.response?.data?.message || e?.message || 'خطای تست اتصال'
} finally {
healthLoading.value = false
}
}
function retry(record: any) {
const isTimeout = record.sync_status === 'timeout'
Modal.confirm({
title: 'ارسال مجدد به شایگان',
content: isTimeout
? 'این سند timeout شده. فقط اگر در حسابگر ثبت نشده، تأیید کنید.'
: 'سند دوباره به شایگان ارسال شود؟',
onOk: async () => {
try {
await shayeganRetry(record.id, isTimeout)
message.success('انجام شد')
fetch()
} catch (e: any) {
message.error(e?.response?.data?.message || e?.message || 'خطا')
}
}
})
}
onMounted(fetch)
defineExpose({ fetch })
</script>
+6 -2
View File
@@ -17,6 +17,7 @@
<ATabPane v-if="pargarEnabled" key="pargar" tab="اسناد پرگار" /> <ATabPane v-if="pargarEnabled" key="pargar" tab="اسناد پرگار" />
<ATabPane v-if="pargarEnabled" key="pargar-customer-sync" tab="همگام سازی مشتریان در پرگار" /> <ATabPane v-if="pargarEnabled" key="pargar-customer-sync" tab="همگام سازی مشتریان در پرگار" />
<ATabPane v-if="pargarEnabled" key="pargar-wallet-sync" tab="مانده کیف پرگار" /> <ATabPane v-if="pargarEnabled" key="pargar-wallet-sync" tab="مانده کیف پرگار" />
<ATabPane v-if="shayeganEnabled" key="shayegan" tab="اسناد شایگان" />
</ATabs> </ATabs>
<tbl v-if="activeTab === 'reports'" ref="table" class="min-h-0 flex-1" /> <tbl v-if="activeTab === 'reports'" ref="table" class="min-h-0 flex-1" />
<tahesabTbl v-else-if="tahesabEnabled && activeTab === 'tahesab'" ref="tahesabTable" class="min-h-0 flex-1" /> <tahesabTbl v-else-if="tahesabEnabled && activeTab === 'tahesab'" ref="tahesabTable" class="min-h-0 flex-1" />
@@ -28,6 +29,7 @@
<pargarTbl v-else-if="pargarEnabled && activeTab === 'pargar'" ref="pargarTable" class="min-h-0 flex-1" /> <pargarTbl v-else-if="pargarEnabled && activeTab === 'pargar'" ref="pargarTable" class="min-h-0 flex-1" />
<pargarCustomerSync v-else-if="pargarEnabled && activeTab === 'pargar-customer-sync'" class="shrink-0" /> <pargarCustomerSync v-else-if="pargarEnabled && activeTab === 'pargar-customer-sync'" class="shrink-0" />
<pargarWalletSync v-else-if="pargarEnabled && activeTab === 'pargar-wallet-sync'" class="shrink-0" /> <pargarWalletSync v-else-if="pargarEnabled && activeTab === 'pargar-wallet-sync'" class="shrink-0" />
<shayeganTbl v-else-if="shayeganEnabled && activeTab === 'shayegan'" ref="shayeganTable" class="min-h-0 flex-1" />
</div> </div>
</BasicMainCard> </BasicMainCard>
<BasicModal <BasicModal
@@ -162,6 +164,7 @@ import tahesabWalletSync from './components/TahesabWalletBulkSync.vue'
import zarinTbl from './components/zarinTable.vue' import zarinTbl from './components/zarinTable.vue'
import zarinCustomerSync from './components/ZarinCustomerSync.vue' import zarinCustomerSync from './components/ZarinCustomerSync.vue'
import zarinWalletSync from './components/ZarinWalletBulkSync.vue' import zarinWalletSync from './components/ZarinWalletBulkSync.vue'
import shayeganTbl from './components/shayeganTable.vue'
import pargarTbl from './components/pargarTable.vue' import pargarTbl from './components/pargarTable.vue'
import pargarCustomerSync from './components/PargarCustomerSync.vue' import pargarCustomerSync from './components/PargarCustomerSync.vue'
import pargarWalletSync from './components/PargarWalletBulkSync.vue' import pargarWalletSync from './components/PargarWalletBulkSync.vue'
@@ -183,15 +186,16 @@ import type { last } from '@/model/price'
import { createOrder } from '@/api/order' import { createOrder } from '@/api/order'
import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet' import { getCashBalance, getGoldBalance } from '@/components/mobile/utils/wallet'
import { metalLabel } from '@/utils/adminMetalLabels' import { metalLabel } from '@/utils/adminMetalLabels'
import { initAccountingSettingsFromApi, tahesabEnabledSetting, zarinEnabledSetting, pargarEnabledSetting } from '@/utils/config' import { initAccountingSettingsFromApi, tahesabEnabledSetting, zarinEnabledSetting, pargarEnabledSetting, shayeganEnabledSetting } from '@/utils/config'
//loading //loading
const loading = ref<boolean>(false) const loading = ref<boolean>(false)
const submitError = ref('') const submitError = ref('')
const activeTab = ref<'reports' | 'tahesab' | 'customer-sync' | 'tahesab-wallet-sync' | 'zarin' | 'zarin-customer-sync' | 'zarin-wallet-sync' | 'pargar' | 'pargar-customer-sync' | 'pargar-wallet-sync'>('reports') const activeTab = ref<'reports' | 'tahesab' | 'customer-sync' | 'tahesab-wallet-sync' | 'zarin' | 'zarin-customer-sync' | 'zarin-wallet-sync' | 'pargar' | 'pargar-customer-sync' | 'pargar-wallet-sync' | 'shayegan'>('reports')
const tahesabEnabled = tahesabEnabledSetting const tahesabEnabled = tahesabEnabledSetting
const zarinEnabled = zarinEnabledSetting const zarinEnabled = zarinEnabledSetting
const pargarEnabled = pargarEnabledSetting const pargarEnabled = pargarEnabledSetting
const shayeganEnabled = shayeganEnabledSetting
//define store //define store
const store = useStaticApiListsStore() const store = useStaticApiListsStore()
@@ -265,6 +265,12 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
icon: 'vuesax-linear:document-text', icon: 'vuesax-linear:document-text',
match: (k) => k.startsWith('pargar_') || k === 'send_to_pargar' match: (k) => k.startsWith('pargar_') || k === 'send_to_pargar'
}, },
{
id: 'accounting_shayegan',
title: 'حسابداری شایگان',
icon: 'vuesax-linear:receipt-item',
match: (k) => k.startsWith('shayegan_')
},
{ {
id: 'price_channel', id: 'price_channel',
title: 'ارسال قیمت به کانال', title: 'ارسال قیمت به کانال',
+19 -15
View File
@@ -1,7 +1,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { setCachedMainMetal } from '@/utils/metalLabels' import { setCachedMainMetal } from '@/utils/metalLabels'
export type AccountingProvider = 'none' | 'pargar' | 'tahesab' | 'zarin' export type AccountingProvider = 'none' | 'pargar' | 'tahesab' | 'zarin' | 'shayegan'
export function getConfigApp(key: string) { export function getConfigApp(key: string) {
const config = window.__APP_CONFIG__ ? window.__APP_CONFIG__.user : {} const config = window.__APP_CONFIG__ ? window.__APP_CONFIG__.user : {}
@@ -47,6 +47,9 @@ export const zarinEnabledSetting = ref(false)
/** reactive — نمایش UI پرگار در حسابداری و ... */ /** reactive — نمایش UI پرگار در حسابداری و ... */
export const pargarEnabledSetting = ref(false) export const pargarEnabledSetting = ref(false)
/** reactive — نمایش UI شایگان در حسابداری و ... */
export const shayeganEnabledSetting = ref(false)
/** reactive — دکمه پیامک تکی در لیست مشتریان */ /** reactive — دکمه پیامک تکی در لیست مشتریان */
export const customerDirectSmsEnabledSetting = ref(false) export const customerDirectSmsEnabledSetting = ref(false)
@@ -100,6 +103,7 @@ export const accountingProviderSetting = ref<AccountingProvider>('none')
let tahesabEnabledFromSettings: boolean | null = null let tahesabEnabledFromSettings: boolean | null = null
let zarinEnabledFromSettings: boolean | null = null let zarinEnabledFromSettings: boolean | null = null
let pargarEnabledFromSettings: boolean | null = null let pargarEnabledFromSettings: boolean | null = null
let shayeganEnabledFromSettings: boolean | null = null
let customerDirectSmsFromSettings: boolean | null = null let customerDirectSmsFromSettings: boolean | null = null
let accountingProviderFromSettings: AccountingProvider | null = null let accountingProviderFromSettings: AccountingProvider | null = null
@@ -113,25 +117,15 @@ function isSwitchOn(value: string | number | boolean | undefined | null): boolea
function normalizeAccountingProvider(value: string | undefined | null): AccountingProvider { function normalizeAccountingProvider(value: string | undefined | null): AccountingProvider {
const v = (value ?? '').trim().toLowerCase() const v = (value ?? '').trim().toLowerCase()
if (v === 'pargar' || v === 'tahesab' || v === 'zarin') return v if (v === 'pargar' || v === 'tahesab' || v === 'zarin' || v === 'shayegan') return v
return 'none' return 'none'
} }
/** بورس مس: UI حسابداری همیشه شایگان — settings/legacy نادیده گرفته می‌شود. */
function resolveAccountingProviderFromRows( function resolveAccountingProviderFromRows(
list: Array<{ key?: string; value?: string | null }> _list: Array<{ key?: string; value?: string | null }>
): AccountingProvider { ): AccountingProvider {
const row = list.find((item) => item.key === 'accounting_provider') return 'shayegan'
if (row?.value !== undefined && row?.value !== null && String(row.value).trim() !== '') {
return normalizeAccountingProvider(row.value)
}
const pargarLegacy = list.find((item) => item.key === 'send_to_pargar')
if (pargarLegacy && isSwitchOn(pargarLegacy.value)) return 'pargar'
const tahesabLegacy = list.find((item) => item.key === 'send_to_tahesab')
if (tahesabLegacy && isSwitchOn(tahesabLegacy.value)) return 'tahesab'
return 'none'
} }
export function applyAccountingProviderFromRows( export function applyAccountingProviderFromRows(
@@ -143,6 +137,7 @@ export function applyAccountingProviderFromRows(
setTahesabEnabledFromSettings(provider === 'tahesab') setTahesabEnabledFromSettings(provider === 'tahesab')
setZarinEnabledFromSettings(provider === 'zarin') setZarinEnabledFromSettings(provider === 'zarin')
setPargarEnabledFromSettings(provider === 'pargar') setPargarEnabledFromSettings(provider === 'pargar')
setShayeganEnabledFromSettings(provider === 'shayegan')
} }
export function setStoreModeFromSettings(value: boolean): void { export function setStoreModeFromSettings(value: boolean): void {
@@ -172,6 +167,11 @@ export function setPargarEnabledFromSettings(value: boolean): void {
pargarEnabledSetting.value = value pargarEnabledSetting.value = value
} }
export function setShayeganEnabledFromSettings(value: boolean): void {
shayeganEnabledFromSettings = value
shayeganEnabledSetting.value = value
}
export function setCustomerDirectSmsEnabledFromSettings(value: boolean): void { export function setCustomerDirectSmsEnabledFromSettings(value: boolean): void {
customerDirectSmsFromSettings = value customerDirectSmsFromSettings = value
customerDirectSmsEnabledSetting.value = value customerDirectSmsEnabledSetting.value = value
@@ -491,6 +491,10 @@ export function isPargarEnabled(): boolean {
return pargarEnabledFromSettings === true return pargarEnabledFromSettings === true
} }
export function isShayeganEnabled(): boolean {
return shayeganEnabledFromSettings === true
}
export function getAccountingProvider(): AccountingProvider { export function getAccountingProvider(): AccountingProvider {
return accountingProviderFromSettings ?? 'none' return accountingProviderFromSettings ?? 'none'
} }