fixed ui and some bugs
This commit is contained in:
Vendored
+1
@@ -53,6 +53,7 @@ declare module '@vue/runtime-core' {
|
|||||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||||
|
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
BaseButton: typeof import('./src/core/components/BaseButton/index.ts')['default']
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ import type { Accounting, TotalAccounting } from '@/model/accounting'
|
|||||||
export const getAccountingTable = async (start: string,end: string) =>
|
export const getAccountingTable = async (start: string,end: string) =>
|
||||||
await useAxios.post<Accounting>('/admin/dashboard/finance', {startDate: start, endDate: end});
|
await useAxios.post<Accounting>('/admin/dashboard/finance', {startDate: start, endDate: end});
|
||||||
|
|
||||||
//Accounting Tab
|
//Accounting Tab — دفترکل داخلی، مستقل از هر سیستم حسابداری بیرونی
|
||||||
export const getTotalAccountingTable = async (params: TableInput) =>
|
export const getTotalAccountingTable = async (params: TableInput) =>
|
||||||
await useAxios.post<TotalAccounting[]>('/admin/finance', params)
|
await useAxios.post<TotalAccounting[]>('/admin/finance', params)
|
||||||
@@ -34,3 +34,59 @@ 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`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export interface PanelOnlyCustomer {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
mobile: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TahesabOnlyCustomer {
|
||||||
|
code: number
|
||||||
|
name: string
|
||||||
|
tel: string
|
||||||
|
gid: number | null
|
||||||
|
group: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerSyncStatus {
|
||||||
|
panel_total: number
|
||||||
|
panel_registered: number
|
||||||
|
panel_only: number
|
||||||
|
tahesab_total: number
|
||||||
|
tahesab_only_count: number
|
||||||
|
panel_only_list: {
|
||||||
|
data: PanelOnlyCustomer[]
|
||||||
|
current_page: number
|
||||||
|
last_page: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
tahesab_only_list: {
|
||||||
|
data: TahesabOnlyCustomer[]
|
||||||
|
current_page: number
|
||||||
|
last_page: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCustomerSyncStatus = async (params?: {
|
||||||
|
search?: string
|
||||||
|
page?: number
|
||||||
|
tahesab_page?: number
|
||||||
|
}) => await useAxios.get<CustomerSyncStatus>('/admin/tahesab/customer-sync/status', { params })
|
||||||
|
|
||||||
|
export interface SyncPanelResult {
|
||||||
|
ok: boolean
|
||||||
|
success: number
|
||||||
|
failed: number
|
||||||
|
errors: { name: string; mobile: string; error: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const syncPanelToTahesab = async () =>
|
||||||
|
await useAxios.post<SyncPanelResult>('/admin/tahesab/customer-sync/panel-to-tahesab')
|
||||||
|
|
||||||
|
export const importFromTahesab = async (customers: TahesabOnlyCustomer[]) =>
|
||||||
|
await useAxios.post<SyncPanelResult>('/admin/tahesab/customer-sync/import-from-tahesab', {
|
||||||
|
customers,
|
||||||
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<span class="text-gray-500">{{ item.label }}</span>
|
<span class="text-gray-500">{{ item.label }}</span>
|
||||||
<span class="text-left break-all">{{ item.value ?? '-' }}</span>
|
<span class="text-left break-all">{{ item.value ?? '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="detail?.res" class="rounded-lg bg-gray-100 p-3 text-xs font-mono text-gray-700 whitespace-pre-wrap">{{ detail.res }}</div>
|
||||||
<div v-if="logText" class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap">{{ logText }}</div>
|
<div v-if="logText" class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap">{{ logText }}</div>
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
@@ -23,6 +24,7 @@ import { computed, ref, watch } from 'vue'
|
|||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import { message } from 'ant-design-vue/es'
|
import { message } from 'ant-design-vue/es'
|
||||||
import { getTahesabDetail, retryTahesabSanad, registerCustomerInTahesab, type TahesabRecord } from '@/api/tahesab'
|
import { getTahesabDetail, retryTahesabSanad, registerCustomerInTahesab, type TahesabRecord } from '@/api/tahesab'
|
||||||
|
import { tahesabStatusLabel } from '@/pages/menuPages/accounting/utils/tahesabStatus'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -49,7 +51,9 @@ const title = computed(() =>
|
|||||||
props.mode === 'register-customer' ? 'ثبت مشتری در تهحساب' : 'ارسال مجدد سند به تهحساب'
|
props.mode === 'register-customer' ? 'ثبت مشتری در تهحساب' : 'ارسال مجدد سند به تهحساب'
|
||||||
)
|
)
|
||||||
|
|
||||||
const confirmText = computed(() => (props.mode === 'register-customer' ? 'ثبت در تهحساب' : 'ارسال به تهحساب'))
|
const confirmText = computed(() =>
|
||||||
|
props.mode === 'register-customer' ? 'ثبت در تهحساب' : 'ارسال مجدد'
|
||||||
|
)
|
||||||
|
|
||||||
const logText = computed(() => detail.value?.sync_log || '')
|
const logText = computed(() => detail.value?.sync_log || '')
|
||||||
|
|
||||||
@@ -69,17 +73,11 @@ const items = computed(() => {
|
|||||||
{ label: 'نوع', value: detail.value?.type },
|
{ label: 'نوع', value: detail.value?.type },
|
||||||
{ label: 'مقدار', value: detail.value?.amount },
|
{ label: 'مقدار', value: detail.value?.amount },
|
||||||
{ label: 'شرح', value: detail.value?.desc },
|
{ label: 'شرح', value: detail.value?.desc },
|
||||||
{ label: 'وضعیت', value: statusLabel(detail.value?.sync_status) },
|
{ label: 'وضعیت', value: tahesabStatusLabel(detail.value?.sync_status, detail.value?.tahesab_id) },
|
||||||
{ label: 'کد تهحساب', value: detail.value?.tahesab_id }
|
{ label: 'کد تهحساب', value: detail.value?.tahesab_id }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
function statusLabel(status?: string) {
|
|
||||||
if (status === 'success') return 'موفق'
|
|
||||||
if (status === 'failed') return 'ناموفق'
|
|
||||||
return 'در انتظار'
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.modelValue, props.tahesabId] as const,
|
() => [props.modelValue, props.tahesabId] as const,
|
||||||
async ([open, id]) => {
|
async ([open, id]) => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface AccountingCustomerRef {
|
|||||||
export interface TotalAccounting {
|
export interface TotalAccounting {
|
||||||
id: number
|
id: number
|
||||||
customer_id: number
|
customer_id: number
|
||||||
order_id: null
|
order_id: number | null
|
||||||
customer_to_id: number
|
customer_to_id: number
|
||||||
user_id: number
|
user_id: number
|
||||||
wallet: null
|
wallet: null
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface Product{
|
|||||||
show: boolean
|
show: boolean
|
||||||
show_in_assets?: boolean
|
show_in_assets?: boolean
|
||||||
trade_by_quantity?: boolean
|
trade_by_quantity?: boolean
|
||||||
|
override_trading_hours?: boolean
|
||||||
use_last_price_if_zero?: boolean
|
use_last_price_if_zero?: boolean
|
||||||
title: string
|
title: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
|||||||
@@ -16,12 +16,7 @@
|
|||||||
/>
|
/>
|
||||||
</MobileCardList>
|
</MobileCardList>
|
||||||
|
|
||||||
<AccountingMobileDetailSheet
|
<AccountingMobileDetailSheet v-model:visible="sheetVisible" :item="selectedItem" />
|
||||||
v-model:visible="sheetVisible"
|
|
||||||
:item="selectedItem"
|
|
||||||
@info="$emit('info', $event)"
|
|
||||||
@orders="$emit('orders', $event)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -33,11 +28,6 @@ import { useMobileTableList } from '@/components/mobile/composables/useMobileTab
|
|||||||
import AccountingMobileDetailSheet from './AccountingMobileDetailSheet.vue'
|
import AccountingMobileDetailSheet from './AccountingMobileDetailSheet.vue'
|
||||||
import AccountingMobileListItem from './AccountingMobileListItem.vue'
|
import AccountingMobileListItem from './AccountingMobileListItem.vue'
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
(e: 'info', item: TotalAccounting): void
|
|
||||||
(e: 'orders', item: TotalAccounting): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<TotalAccounting>()
|
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<TotalAccounting>()
|
||||||
|
|
||||||
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<TotalAccounting>({
|
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<TotalAccounting>({
|
||||||
@@ -47,5 +37,5 @@ const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTa
|
|||||||
|
|
||||||
onMounted(() => reload())
|
onMounted(() => reload())
|
||||||
|
|
||||||
defineExpose({ fetch, openSheet })
|
defineExpose({ fetch })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,32 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
|
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
|
||||||
<template #default="{ item: row }">
|
<template #default="{ item: row }">
|
||||||
<MobileSheetHeader :title="sheetTitle" :show-title="isMobile">
|
<MobileSheetHeader :title="sheetTitle" :show-title="isMobile" />
|
||||||
<template #actions>
|
|
||||||
<MobileIconButton plain icon="vuesax-linear:stickynote" color="#10913A" :size="22" @click="$emit('info', row)" />
|
|
||||||
<MobileIconButton plain icon="vuesax-linear:receipt-item" color="#10913A" :size="22" @click="$emit('orders', row)" />
|
|
||||||
</template>
|
|
||||||
</MobileSheetHeader>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<MobileStatCard bordered variant="success" label="موجودی ریالی" :value="usePrice(row.balance_cash ?? 0, true, '')">
|
|
||||||
<template #prefix>
|
|
||||||
<BaseIcon icon="vuesax-linear:empty-wallet" size="18" color="#15803d" />
|
|
||||||
</template>
|
|
||||||
</MobileStatCard>
|
|
||||||
<MobileStatCard bordered variant="success" :label="metalLabel.goldInventory()" :value="`${row.balance_gold ?? 0} گرم`">
|
|
||||||
<template #prefix>
|
|
||||||
<BaseIcon icon="vuesax-linear:coin" size="18" color="#15803d" />
|
|
||||||
</template>
|
|
||||||
</MobileStatCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MobileInfoGrid>
|
<MobileInfoGrid>
|
||||||
<MobileDetailRow label="نام مشتری" :value="row.customer_to?.name || '-'" />
|
<MobileDetailRow label="نام مشتری" :value="row.customer_to?.name || '-'" />
|
||||||
<MobileDetailRow label="نام مشتری دعوت شده" :value="row.customer?.name || '-'" />
|
|
||||||
<MobileDetailRow label="زمان ثبت" :value="row.created_at ? toJalali(row.created_at) : '-'" />
|
<MobileDetailRow label="زمان ثبت" :value="row.created_at ? toJalali(row.created_at) : '-'" />
|
||||||
<MobileDetailRow label="نقد (ریال)" :value="usePrice(row.cash ?? 0, true, '')" />
|
<MobileDetailRow label="نقد (ریال)" :value="usePrice(row.cash ?? 0, true, '')" />
|
||||||
<MobileDetailRow :label="m('طلا (گرم)')" :value="`${row.gold ?? 0}`" />
|
<MobileDetailRow :label="metalLabel.goldUnit()" :value="`${row.gold ?? 0}`" />
|
||||||
<MobileDetailRow label="توضیحات" full-width :value="row.note || '-'" />
|
<MobileDetailRow label="توضیحات" full-width :value="row.note || '-'" />
|
||||||
</MobileInfoGrid>
|
</MobileInfoGrid>
|
||||||
</template>
|
</template>
|
||||||
@@ -36,32 +17,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TotalAccounting } from '@/model/accounting'
|
import type { TotalAccounting } from '@/model/accounting'
|
||||||
import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
|
import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
|
||||||
import MobileStatCard from '@/components/mobile/MobileStatCard.vue'
|
|
||||||
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
|
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
|
||||||
import MobileIconButton from '@/components/mobile/MobileIconButton.vue'
|
|
||||||
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
|
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
|
||||||
import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
|
import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
|
||||||
import { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
import { usePrice } from '@core/utils/usePrice'
|
import { usePrice } from '@core/utils/usePrice'
|
||||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||||
import { m } from '@/utils/metalLabels'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{ visible: boolean; item: TotalAccounting | null }>()
|
||||||
visible: boolean
|
|
||||||
item: TotalAccounting | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{ (e: 'update:visible', value: boolean): void }>()
|
||||||
(e: 'update:visible', value: boolean): void
|
|
||||||
(e: 'info', item: TotalAccounting): void
|
|
||||||
(e: 'orders', item: TotalAccounting): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const visibleProxy = computed({
|
const visibleProxy = computed({
|
||||||
get: () => props.visible,
|
get: () => props.visible,
|
||||||
set: (value: boolean) => emit('update:visible', value)
|
set: (v) => emit('update:visible', v)
|
||||||
})
|
})
|
||||||
|
|
||||||
const sheetTitle = computed(() => props.item?.customer_to?.name || props.item?.customer?.name || 'حسابداری')
|
const sheetTitle = computed(() => props.item?.customer_to?.name || 'گزارش')
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<MobileCard clickable @click="$emit('open', item)">
|
<MobileCard @click="$emit('open', item)">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div>
|
<div class="flex justify-between items-center gap-x-3 text-xs">
|
||||||
<div class="flex justify-between items-center gap-x-3 gap-y-1 text-xs">
|
<div class="truncate text-sm font-700 text-gray-900">{{ customerName }}</div>
|
||||||
<div class="truncate text-sm font-700 text-gray-900">{{ customerToName }}</div>
|
<div class="text-xs text-gray-400">{{ createdAtLabel }}</div>
|
||||||
<div class="text-xs text-gray-400">{{ createdAtLabel }}</div>
|
</div>
|
||||||
</div>
|
<div v-if="item.note" class="text-xs text-gray-600">{{ item.note }}</div>
|
||||||
<span class="text-xs text-gray-600">نقد: <span class="font-600 text-gray-900">{{ cashLabel }}</span></span>
|
<div class="flex gap-3 text-xs">
|
||||||
<span class="text-xs text-gray-600">{{ metalLabel.goldUnit() }}: <span class="font-600 text-gray-900">{{ goldLabel }}</span></span>
|
<span class="text-gray-600">نقد: <span class="font-600 text-gray-900">{{ cashLabel }}</span></span>
|
||||||
|
<span class="text-gray-600">{{ metalLabel.goldUnit() }}: <span class="font-600 text-gray-900">{{ goldLabel }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -23,15 +21,11 @@ import { toJalali } from '@core/utils/useDataTime'
|
|||||||
import { usePrice } from '@core/utils/usePrice'
|
import { usePrice } from '@core/utils/usePrice'
|
||||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{ item: TotalAccounting }>()
|
||||||
item: TotalAccounting
|
|
||||||
}>()
|
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{ (e: 'open', item: TotalAccounting): void }>()
|
||||||
(e: 'open', item: TotalAccounting): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const customerToName = computed(() => props.item.customer_to?.name || '-')
|
const customerName = computed(() => props.item.customer_to?.name || '-')
|
||||||
const createdAtLabel = computed(() => (props.item.created_at ? toJalali(props.item.created_at) : '-'))
|
const createdAtLabel = computed(() => (props.item.created_at ? toJalali(props.item.created_at) : '-'))
|
||||||
const cashLabel = computed(() => usePrice(props.item.cash ?? 0, true, ''))
|
const cashLabel = computed(() => usePrice(props.item.cash ?? 0, true, ''))
|
||||||
const goldLabel = computed(() => `${props.item.gold ?? 0} گرم`)
|
const goldLabel = computed(() => `${props.item.gold ?? 0} گرم`)
|
||||||
|
|||||||
@@ -0,0 +1,823 @@
|
|||||||
|
<template>
|
||||||
|
<div class="cs-wrap">
|
||||||
|
|
||||||
|
<!-- ─── بنر آمار ─── -->
|
||||||
|
<div class="cs-banner">
|
||||||
|
<div class="cs-banner__title">همگامسازی مشتریان</div>
|
||||||
|
|
||||||
|
<!-- آمار اصلی: ردیف بالا -->
|
||||||
|
<div class="cs-stats-main">
|
||||||
|
<div class="cs-stat cs-stat--neutral">
|
||||||
|
<div class="cs-stat__num">
|
||||||
|
<template v-if="status">{{ status.panel_total }}</template>
|
||||||
|
<a-spin v-else size="small" />
|
||||||
|
</div>
|
||||||
|
<div class="cs-stat__label">مشتریان پنل</div>
|
||||||
|
</div>
|
||||||
|
<div class="cs-sep-arrows">
|
||||||
|
<Icon icon="vuesax-linear:arrow-left" :size="16" />
|
||||||
|
<Icon icon="vuesax-linear:arrow-right" :size="16" />
|
||||||
|
</div>
|
||||||
|
<div class="cs-stat cs-stat--tahesab">
|
||||||
|
<div class="cs-stat__num">
|
||||||
|
<template v-if="status?.tahesab_total != null">{{ status.tahesab_total }}</template>
|
||||||
|
<a-spin v-else-if="loadingTahesab" size="small" />
|
||||||
|
<span v-else class="cs-stat__hint">تب دوم</span>
|
||||||
|
</div>
|
||||||
|
<div class="cs-stat__label">مشتریان تهحساب</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- آمار تفاوت: ردیف پایین -->
|
||||||
|
<div class="cs-stats-diff">
|
||||||
|
<div class="cs-stat cs-stat--amber">
|
||||||
|
<div class="cs-stat__num">
|
||||||
|
<template v-if="status">{{ status.panel_only }}</template>
|
||||||
|
<a-spin v-else-if="loadingPanel" size="small" />
|
||||||
|
<span v-else>—</span>
|
||||||
|
</div>
|
||||||
|
<div class="cs-stat__label">پنل — بدون تهحساب</div>
|
||||||
|
</div>
|
||||||
|
<div class="cs-stat cs-stat--sky">
|
||||||
|
<div class="cs-stat__num">
|
||||||
|
<template v-if="status?.tahesab_only_count != null">{{ status.tahesab_only_count }}</template>
|
||||||
|
<a-spin v-else-if="loadingTahesab" size="small" />
|
||||||
|
<span v-else class="cs-stat__hint">تب دوم</span>
|
||||||
|
</div>
|
||||||
|
<div class="cs-stat__label">تهحساب — بدون پنل</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- progress -->
|
||||||
|
<div v-if="status" class="cs-progress">
|
||||||
|
<div class="cs-progress__labels">
|
||||||
|
<span>{{ registeredPercent }}٪ پنل در تهحساب</span>
|
||||||
|
<span>{{ status.panel_registered }} / {{ status.panel_total }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="cs-track"><div class="cs-track__fill" :style="{ width: registeredPercent + '%' }" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── تبها ─── -->
|
||||||
|
<div class="cs-tabs">
|
||||||
|
<button
|
||||||
|
class="cs-tab"
|
||||||
|
:class="{ 'cs-tab--active': activeTab === 'panel' }"
|
||||||
|
@click="onTabChange('panel')"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:arrow-left" :size="14" />
|
||||||
|
پنل ← تهحساب
|
||||||
|
<span v-if="status?.panel_only" class="cs-tab__badge cs-tab__badge--amber">{{ status.panel_only }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="cs-tab"
|
||||||
|
:class="{ 'cs-tab--active': activeTab === 'tahesab' }"
|
||||||
|
@click="onTabChange('tahesab')"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:arrow-right" :size="14" />
|
||||||
|
تهحساب ← پنل
|
||||||
|
<span v-if="status?.tahesab_only_count" class="cs-tab__badge cs-tab__badge--sky">{{ status.tahesab_only_count }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── تب ۱: پنل → تهحساب ─── -->
|
||||||
|
<div v-show="activeTab === 'panel'" class="cs-card">
|
||||||
|
<div class="cs-card__head">
|
||||||
|
<div class="cs-card__head-right">
|
||||||
|
<span class="cs-dot cs-dot--amber" />
|
||||||
|
<span class="cs-card__head-title">مشتریان پنل بدون کد تهحساب</span>
|
||||||
|
</div>
|
||||||
|
<div class="cs-card__head-actions">
|
||||||
|
<a-input
|
||||||
|
v-model:value="panelSearch"
|
||||||
|
placeholder="جستجو..."
|
||||||
|
allow-clear
|
||||||
|
size="small"
|
||||||
|
class="cs-search"
|
||||||
|
@change="onPanelSearch"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="syncingPanel"
|
||||||
|
:disabled="!status?.panel_only"
|
||||||
|
@click="syncAllPanel"
|
||||||
|
>
|
||||||
|
<template #icon><Icon icon="vuesax-linear:send-2" :size="13" /></template>
|
||||||
|
ثبت همه در تهحساب
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="syncingPanel || panelSyncResult" class="cs-result">
|
||||||
|
<template v-if="syncingPanel">
|
||||||
|
<a-spin size="small" />
|
||||||
|
<span>در حال ثبت مشتریان پنل در تهحساب...</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="panelSyncResult">
|
||||||
|
<span class="cs-result__ok">✓ {{ panelSyncResult.success }} موفق</span>
|
||||||
|
<span v-if="panelSyncResult.failed" class="cs-result__fail">✗ {{ panelSyncResult.failed }} ناموفق</span>
|
||||||
|
<a-button v-if="panelSyncResult.failed" type="link" size="small" @click="showPanelErrors = !showPanelErrors">
|
||||||
|
{{ showPanelErrors ? 'پنهان' : 'جزئیات' }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" @click="panelSyncResult = null">بستن</a-button>
|
||||||
|
</template>
|
||||||
|
<div v-if="showPanelErrors && panelSyncResult?.errors.length" class="cs-result__errors">
|
||||||
|
<div v-for="e in panelSyncResult.errors" :key="e.mobile"><b>{{ e.name }}</b> — {{ e.error }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loadingPanel" class="cs-loading"><a-spin /></div>
|
||||||
|
<template v-else-if="panelList.length">
|
||||||
|
<!-- موبایل -->
|
||||||
|
<div class="cs-mobile-list">
|
||||||
|
<div
|
||||||
|
v-for="c in panelList" :key="c.id"
|
||||||
|
class="cs-mobile-row"
|
||||||
|
:class="{ 'cs-mobile-row--done': panelDone.has(c.id) }"
|
||||||
|
>
|
||||||
|
<div class="cs-mobile-row__info">
|
||||||
|
<div class="cs-mobile-row__name">{{ c.name }}</div>
|
||||||
|
<div class="cs-mobile-row__mobile">{{ c.mobile }}</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="panelDone.has(c.id)" class="cs-done-badge">✓ ثبت شد</span>
|
||||||
|
<a-button v-else type="primary" ghost size="small"
|
||||||
|
:loading="panelRegisteringId === c.id" @click="registerOnePanel(c)">
|
||||||
|
ارسال به تهحساب
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- دسکتاپ -->
|
||||||
|
<table class="cs-table">
|
||||||
|
<thead><tr>
|
||||||
|
<th>#</th><th>نام مشتری</th><th>موبایل</th><th>تاریخ عضویت</th><th>عملیات</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(c, idx) in panelList" :key="c.id"
|
||||||
|
:class="{ 'cs-table__row--done': panelDone.has(c.id) }">
|
||||||
|
<td class="cs-table__num">{{ (panelPagination.page - 1) * 20 + idx + 1 }}</td>
|
||||||
|
<td class="cs-table__name">{{ c.name }}</td>
|
||||||
|
<td class="cs-table__mobile">{{ c.mobile }}</td>
|
||||||
|
<td class="cs-table__date">{{ toJalali(c.created_at) }}</td>
|
||||||
|
<td class="cs-table__action">
|
||||||
|
<span v-if="panelDone.has(c.id)" class="cs-done-badge">✓ ثبت شد</span>
|
||||||
|
<a-button v-else type="primary" ghost size="small"
|
||||||
|
:loading="panelRegisteringId === c.id" @click="registerOnePanel(c)">
|
||||||
|
<template #icon><Icon icon="vuesax-linear:send-2" :size="13" /></template>
|
||||||
|
ارسال به تهحساب
|
||||||
|
</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-if="panelPagination.lastPage > 1" class="cs-pagination">
|
||||||
|
<a-pagination
|
||||||
|
v-model:current="panelPagination.page"
|
||||||
|
:total="panelPagination.total"
|
||||||
|
:page-size="20" size="small" show-less-items
|
||||||
|
@change="p => loadPanel(p)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="cs-empty">
|
||||||
|
<div class="cs-empty__icon"><Icon icon="vuesax-linear:tick-circle" :size="28" /></div>
|
||||||
|
<div class="cs-empty__title">همه مشتریان پنل در تهحساب ثبت شدهاند</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── تب ۲: تهحساب → پنل ─── -->
|
||||||
|
<div v-show="activeTab === 'tahesab'" class="cs-card">
|
||||||
|
<div class="cs-card__head">
|
||||||
|
<div class="cs-card__head-right">
|
||||||
|
<span class="cs-dot cs-dot--sky" />
|
||||||
|
<span class="cs-card__head-title">مشتریان تهحساب بدون حساب پنل</span>
|
||||||
|
</div>
|
||||||
|
<div class="cs-card__head-actions">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="syncingTahesab"
|
||||||
|
:disabled="!status?.tahesab_only_count || selectedTahesab.size === 0"
|
||||||
|
@click="importSelected"
|
||||||
|
>
|
||||||
|
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
||||||
|
وارد کردن انتخابشدهها ({{ selectedTahesab.size }})
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
:loading="syncingTahesab"
|
||||||
|
:disabled="!tahesabOnlyList.length"
|
||||||
|
@click="importAll"
|
||||||
|
>
|
||||||
|
وارد کردن همه ({{ status?.tahesab_only_count ?? 0 }})
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="syncingTahesab || tahesabSyncResult" class="cs-result">
|
||||||
|
<template v-if="syncingTahesab">
|
||||||
|
<a-spin size="small" />
|
||||||
|
<span>در حال وارد کردن مشتریان تهحساب به پنل...</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="tahesabSyncResult">
|
||||||
|
<span class="cs-result__ok">✓ {{ tahesabSyncResult.success }} موفق</span>
|
||||||
|
<span v-if="tahesabSyncResult.failed" class="cs-result__fail">✗ {{ tahesabSyncResult.failed }} ناموفق</span>
|
||||||
|
<a-button v-if="tahesabSyncResult.failed" type="link" size="small" @click="showTahesabErrors = !showTahesabErrors">
|
||||||
|
{{ showTahesabErrors ? 'پنهان' : 'جزئیات' }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="link" size="small" @click="tahesabSyncResult = null">بستن</a-button>
|
||||||
|
</template>
|
||||||
|
<div v-if="showTahesabErrors && tahesabSyncResult?.errors.length" class="cs-result__errors">
|
||||||
|
<div v-for="e in tahesabSyncResult.errors" :key="e.name"><b>{{ e.name }}</b> — {{ e.error }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loadingTahesab" class="cs-loading"><a-spin /></div>
|
||||||
|
<template v-else-if="tahesabOnlyList.length">
|
||||||
|
<!-- موبایل -->
|
||||||
|
<div class="cs-mobile-list">
|
||||||
|
<div
|
||||||
|
v-for="c in pagedTahesab" :key="c.code"
|
||||||
|
class="cs-mobile-row"
|
||||||
|
:class="{ 'cs-mobile-row--done': tahesabDone.has(c.code) }"
|
||||||
|
>
|
||||||
|
<a-checkbox
|
||||||
|
:checked="selectedTahesab.has(c.code)"
|
||||||
|
:disabled="tahesabDone.has(c.code)"
|
||||||
|
class="cs-mobile-row__check"
|
||||||
|
@change="toggleTahesab(c.code)"
|
||||||
|
/>
|
||||||
|
<div class="cs-mobile-row__info">
|
||||||
|
<div class="cs-mobile-row__name">{{ c.name }}</div>
|
||||||
|
<div class="cs-mobile-row__mobile">{{ c.tel || '—' }} · کد {{ c.code }}</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="tahesabDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
||||||
|
<a-button v-else type="primary" ghost size="small"
|
||||||
|
@click="importOne(c)">
|
||||||
|
وارد کردن
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- دسکتاپ -->
|
||||||
|
<table class="cs-table">
|
||||||
|
<thead><tr>
|
||||||
|
<th><a-checkbox :checked="allPageSelected" :indeterminate="someSelected" @change="toggleAll" /></th>
|
||||||
|
<th>#</th><th>نام مشتری</th><th>شماره تلفن</th><th>کد تهحساب</th><th>گروه</th><th>عملیات</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(c, idx) in pagedTahesab" :key="c.code"
|
||||||
|
:class="{ 'cs-table__row--done': tahesabDone.has(c.code) }">
|
||||||
|
<td>
|
||||||
|
<a-checkbox
|
||||||
|
:checked="selectedTahesab.has(c.code)"
|
||||||
|
:disabled="tahesabDone.has(c.code)"
|
||||||
|
@change="toggleTahesab(c.code)"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="cs-table__num">{{ (tahesabPage - 1) * 20 + idx + 1 }}</td>
|
||||||
|
<td class="cs-table__name">{{ c.name }}</td>
|
||||||
|
<td class="cs-table__mobile">{{ c.tel || '—' }}</td>
|
||||||
|
<td class="cs-table__mobile">{{ c.code }}</td>
|
||||||
|
<td class="cs-table__date">{{ c.group || '—' }}</td>
|
||||||
|
<td class="cs-table__action">
|
||||||
|
<span v-if="tahesabDone.has(c.code)" class="cs-done-badge">✓ وارد شد</span>
|
||||||
|
<a-button v-else type="primary" ghost size="small" @click="importOne(c)">
|
||||||
|
<template #icon><Icon icon="vuesax-linear:import" :size="13" /></template>
|
||||||
|
وارد کردن
|
||||||
|
</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div v-if="tahesabLastPage > 1" class="cs-pagination">
|
||||||
|
<a-pagination
|
||||||
|
v-model:current="tahesabPage"
|
||||||
|
:total="tahesabOnlyList.length"
|
||||||
|
:page-size="20" size="small" show-less-items
|
||||||
|
@change="p => { tahesabPage = p; selectedTahesab.clear() }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="cs-empty">
|
||||||
|
<div class="cs-empty__icon"><Icon icon="vuesax-linear:tick-circle" :size="28" /></div>
|
||||||
|
<div class="cs-empty__title">همه مشتریان تهحساب در پنل ثبت شدهاند</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, reactive } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue/es'
|
||||||
|
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||||
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
import {
|
||||||
|
getCustomerSyncStatus,
|
||||||
|
syncPanelToTahesab,
|
||||||
|
importFromTahesab,
|
||||||
|
registerCustomerInTahesab,
|
||||||
|
type CustomerSyncStatus,
|
||||||
|
type PanelOnlyCustomer,
|
||||||
|
type TahesabOnlyCustomer,
|
||||||
|
type SyncPanelResult,
|
||||||
|
} from '@/api/tahesab'
|
||||||
|
|
||||||
|
// ─── state ───
|
||||||
|
const loadingPanel = ref(true) // شروع با true تا اولین load کامل بشه
|
||||||
|
const loadingTahesab = ref(false)
|
||||||
|
const syncingPanel = ref(false)
|
||||||
|
const syncingTahesab = ref(false)
|
||||||
|
const activeTab = ref<'panel' | 'tahesab'>('panel')
|
||||||
|
const tahesabFetched = ref(false) // آیا مشتریان تهحساب قبلاً fetch شده؟
|
||||||
|
|
||||||
|
const status = ref<CustomerSyncStatus | null>(null)
|
||||||
|
|
||||||
|
// تب ۱
|
||||||
|
const panelSearch = ref('')
|
||||||
|
const panelList = ref<PanelOnlyCustomer[]>([])
|
||||||
|
const panelPagination = ref({ page: 1, lastPage: 1, total: 0 })
|
||||||
|
const panelDone = reactive(new Set<number>())
|
||||||
|
const panelRegisteringId = ref<number | null>(null)
|
||||||
|
const panelSyncResult = ref<SyncPanelResult | null>(null)
|
||||||
|
const showPanelErrors = ref(false)
|
||||||
|
|
||||||
|
// تب ۲
|
||||||
|
const tahesabOnlyList = ref<TahesabOnlyCustomer[]>([])
|
||||||
|
const tahesabPage = ref(1)
|
||||||
|
const tahesabDone = reactive(new Set<number>())
|
||||||
|
const selectedTahesab = reactive(new Set<number>())
|
||||||
|
const tahesabSyncResult = ref<SyncPanelResult | null>(null)
|
||||||
|
const showTahesabErrors = ref(false)
|
||||||
|
|
||||||
|
// ─── computed ───
|
||||||
|
const registeredPercent = computed(() => {
|
||||||
|
if (!status.value?.panel_total) return 0
|
||||||
|
return Math.round((status.value.panel_registered / status.value.panel_total) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
const tahesabLastPage = computed(() => Math.max(1, Math.ceil(tahesabOnlyList.value.length / 20)))
|
||||||
|
|
||||||
|
const pagedTahesab = computed(() => {
|
||||||
|
const s = (tahesabPage.value - 1) * 20
|
||||||
|
return tahesabOnlyList.value.slice(s, s + 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
const allPageSelected = computed(() =>
|
||||||
|
pagedTahesab.value.length > 0 &&
|
||||||
|
pagedTahesab.value.every(c => selectedTahesab.has(c.code) || tahesabDone.has(c.code))
|
||||||
|
)
|
||||||
|
const someSelected = computed(() =>
|
||||||
|
pagedTahesab.value.some(c => selectedTahesab.has(c.code)) && !allPageSelected.value
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─── load panel only (سریع — بدون Tahesab API) ───
|
||||||
|
let panelSearchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
async function load(panelPage = 1) {
|
||||||
|
loadingPanel.value = true
|
||||||
|
try {
|
||||||
|
const res = await getCustomerSyncStatus({
|
||||||
|
search: panelSearch.value || undefined,
|
||||||
|
page: panelPage,
|
||||||
|
})
|
||||||
|
status.value = res
|
||||||
|
panelList.value = res.panel_only_list.data
|
||||||
|
panelPagination.value = {
|
||||||
|
page: res.panel_only_list.current_page,
|
||||||
|
lastPage: res.panel_only_list.last_page,
|
||||||
|
total: res.panel_only_list.total,
|
||||||
|
}
|
||||||
|
// دادههای تهحساب فقط اگر قبلاً fetch شده باشه بروز کن
|
||||||
|
if (tahesabFetched.value) {
|
||||||
|
tahesabOnlyList.value = res.tahesab_only_list.data ?? []
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
finally { loadingPanel.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── load tahesab customers (کند — فقط وقتی تب تهحساب باز میشه) ───
|
||||||
|
async function loadTahesab() {
|
||||||
|
if (tahesabFetched.value) return
|
||||||
|
loadingTahesab.value = true
|
||||||
|
try {
|
||||||
|
const res = await getCustomerSyncStatus({ include_tahesab: 1 } as any)
|
||||||
|
if (status.value) {
|
||||||
|
status.value.tahesab_total = res.tahesab_total ?? 0
|
||||||
|
status.value.tahesab_only_count = res.tahesab_only_count ?? 0
|
||||||
|
} else {
|
||||||
|
status.value = res
|
||||||
|
}
|
||||||
|
tahesabOnlyList.value = res.tahesab_only_list?.data ?? []
|
||||||
|
tahesabFetched.value = true
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
finally { loadingTahesab.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTabChange(tab: 'panel' | 'tahesab') {
|
||||||
|
activeTab.value = tab
|
||||||
|
if (tab === 'tahesab') loadTahesab()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPanel(page = 1) {
|
||||||
|
loadingPanel.value = true
|
||||||
|
try {
|
||||||
|
const res = await getCustomerSyncStatus({ search: panelSearch.value || undefined, page })
|
||||||
|
if (status.value) {
|
||||||
|
status.value.panel_total = res.panel_total
|
||||||
|
status.value.panel_registered = res.panel_registered
|
||||||
|
status.value.panel_only = res.panel_only
|
||||||
|
}
|
||||||
|
panelList.value = res.panel_only_list.data
|
||||||
|
panelPagination.value = {
|
||||||
|
page: res.panel_only_list.current_page,
|
||||||
|
lastPage: res.panel_only_list.last_page,
|
||||||
|
total: res.panel_only_list.total,
|
||||||
|
}
|
||||||
|
} catch { } finally { loadingPanel.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPanelSearch() {
|
||||||
|
if (panelSearchTimer) clearTimeout(panelSearchTimer)
|
||||||
|
panelSearchTimer = setTimeout(() => loadPanel(1), 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── تب ۱: عملیات ───
|
||||||
|
async function registerOnePanel(customer: PanelOnlyCustomer) {
|
||||||
|
panelRegisteringId.value = customer.id
|
||||||
|
try {
|
||||||
|
const res = await registerCustomerInTahesab(customer.id)
|
||||||
|
if (res.ok) {
|
||||||
|
panelDone.add(customer.id)
|
||||||
|
if (status.value) { status.value.panel_registered++; status.value.panel_only-- }
|
||||||
|
message.success(`${customer.name} در تهحساب ثبت شد`)
|
||||||
|
} else {
|
||||||
|
message.error((res as any).message || 'ثبت ناموفق بود')
|
||||||
|
}
|
||||||
|
} catch { message.error('خطا در ارتباط با سرور') }
|
||||||
|
finally { panelRegisteringId.value = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncAllPanel() {
|
||||||
|
syncingPanel.value = true
|
||||||
|
panelSyncResult.value = null
|
||||||
|
showPanelErrors.value = false
|
||||||
|
try {
|
||||||
|
const res = await syncPanelToTahesab()
|
||||||
|
panelSyncResult.value = res
|
||||||
|
if (res.success) message.success(`${res.success} مشتری ثبت شد`)
|
||||||
|
if (res.failed) message.warning(`${res.failed} مشتری ثبت نشد`)
|
||||||
|
await load(1)
|
||||||
|
panelDone.clear()
|
||||||
|
} catch { message.error('خطا در ارتباط با سرور') }
|
||||||
|
finally { syncingPanel.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── تب ۲: عملیات ───
|
||||||
|
function toggleTahesab(code: number) {
|
||||||
|
if (selectedTahesab.has(code)) selectedTahesab.delete(code)
|
||||||
|
else selectedTahesab.add(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll() {
|
||||||
|
if (allPageSelected.value) {
|
||||||
|
pagedTahesab.value.forEach(c => selectedTahesab.delete(c.code))
|
||||||
|
} else {
|
||||||
|
pagedTahesab.value.forEach(c => { if (!tahesabDone.has(c.code)) selectedTahesab.add(c.code) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doImport(customers: TahesabOnlyCustomer[]) {
|
||||||
|
syncingTahesab.value = true
|
||||||
|
tahesabSyncResult.value = null
|
||||||
|
showTahesabErrors.value = false
|
||||||
|
try {
|
||||||
|
const res = await importFromTahesab(customers)
|
||||||
|
tahesabSyncResult.value = res
|
||||||
|
if (res.success) {
|
||||||
|
message.success(`${res.success} مشتری وارد پنل شد`)
|
||||||
|
customers.forEach(c => { if (!res.errors.find((e: any) => e.code === c.code)) tahesabDone.add(c.code) })
|
||||||
|
}
|
||||||
|
if (res.failed) message.warning(`${res.failed} مشتری وارد نشد`)
|
||||||
|
await load(panelPagination.value.page)
|
||||||
|
selectedTahesab.clear()
|
||||||
|
} catch { message.error('خطا در ارتباط با سرور') }
|
||||||
|
finally { syncingTahesab.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importOne(customer: TahesabOnlyCustomer) { await doImport([customer]) }
|
||||||
|
async function importSelected() { await doImport(tahesabOnlyList.value.filter(c => selectedTahesab.has(c.code))) }
|
||||||
|
async function importAll() { await doImport(tahesabOnlyList.value) }
|
||||||
|
|
||||||
|
onMounted(() => load(1))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.cs-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 8px 4px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── بنر ─── */
|
||||||
|
.cs-banner {
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 55%, #0ea5e9 100%);
|
||||||
|
padding: 14px 16px;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.35);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media (min-width: 640px) { padding: 20px 24px; gap: 16px; }
|
||||||
|
|
||||||
|
&__title { font-size: 14px; font-weight: 700; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ردیف اصلی: پنل ↔ تهحساب */
|
||||||
|
.cs-stats-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ردیف تفاوت */
|
||||||
|
.cs-stats-diff {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-sep-arrows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-stat {
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&--neutral { background: rgba(255,255,255,0.12); }
|
||||||
|
&--tahesab { background: rgba(14,165,233,0.25); }
|
||||||
|
&--amber { background: rgba(251,191,36,0.2); }
|
||||||
|
&--sky { background: rgba(14,165,233,0.2); }
|
||||||
|
|
||||||
|
&__num {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
min-height: 26px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (min-width: 640px) { font-size: 24px; }
|
||||||
|
}
|
||||||
|
&__label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255,255,255,0.65);
|
||||||
|
margin-top: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
&__hint { font-size: 10px; color: rgba(255,255,255,0.4); font-weight: 400; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* progress */
|
||||||
|
.cs-progress {
|
||||||
|
&__labels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-track {
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: #34d399;
|
||||||
|
transition: width 0.7s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── تبها ─── */
|
||||||
|
.cs-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
border-bottom: 2px solid var(--admin-border);
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--admin-text-secondary);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (min-width: 640px) { padding: 8px 16px; font-size: 13px; }
|
||||||
|
|
||||||
|
&:hover { color: var(--admin-text); }
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: #7c3aed;
|
||||||
|
border-bottom-color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 99px;
|
||||||
|
|
||||||
|
&--amber { background: rgba(245,158,11,0.15); color: #d97706; }
|
||||||
|
&--sky { background: rgba(14,165,233,0.15); color: #0ea5e9; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── کارت ─── */
|
||||||
|
.cs-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--admin-border-strong);
|
||||||
|
background: var(--admin-bg-surface);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
background: var(--admin-bg-muted);
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.ant-btn { flex-shrink: 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* روی موبایل دکمههای head رو کوچیکتر نشون بده */
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.cs-card__head-title { font-size: 12px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&--amber { background: #f59e0b; }
|
||||||
|
&--sky { background: #0ea5e9; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-search { width: 150px; }
|
||||||
|
@media (max-width: 639px) { .cs-search { flex: 1; min-width: 0; } }
|
||||||
|
|
||||||
|
/* ─── نتیجه sync ─── */
|
||||||
|
.cs-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
background: var(--admin-bg-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--admin-text);
|
||||||
|
|
||||||
|
&__ok { background: rgba(52,211,153,0.15); color: #059669; font-weight: 600; border-radius: 99px; padding: 2px 10px; font-size: 12px; }
|
||||||
|
&__fail { background: rgba(239,68,68,0.12); color: #dc2626; font-weight: 600; border-radius: 99px; padding: 2px 10px; font-size: 12px; }
|
||||||
|
&__errors { width: 100%; margin-top: 4px; max-height: 100px; overflow-y: auto; border-radius: 8px; background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.2); padding: 8px 12px; font-size: 11px; color: #dc2626; line-height: 1.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-loading { display: flex; justify-content: center; padding: 48px; }
|
||||||
|
|
||||||
|
/* ─── موبایل ─── */
|
||||||
|
.cs-mobile-list {
|
||||||
|
display: block;
|
||||||
|
@media (min-width: 640px) { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-mobile-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&--done { opacity: 0.45; }
|
||||||
|
|
||||||
|
&__check { flex-shrink: 0; }
|
||||||
|
&__info { flex: 1; min-width: 0; }
|
||||||
|
&__name { font-size: 13px; font-weight: 600; color: var(--admin-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
&__mobile{ font-size: 11px; color: var(--admin-text-secondary); font-family: monospace; margin-top: 2px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── جدول دسکتاپ ─── */
|
||||||
|
.cs-table {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
@media (min-width: 640px) { display: table; }
|
||||||
|
|
||||||
|
thead tr { background: var(--admin-bg-muted); border-bottom: 1px solid var(--admin-border); }
|
||||||
|
th { padding: 10px 14px; text-align: right; font-size: 11px; font-weight: 600; color: var(--admin-text-secondary); white-space: nowrap; }
|
||||||
|
tbody tr { border-bottom: 1px solid var(--admin-border); transition: background 0.15s; }
|
||||||
|
tbody tr:hover { background: var(--admin-bg-hover); }
|
||||||
|
tbody tr.cs-table__row--done { opacity: 0.45; }
|
||||||
|
td { padding: 10px 14px; color: var(--admin-text); }
|
||||||
|
|
||||||
|
&__num { color: var(--admin-text-secondary); font-size: 12px; width: 40px; }
|
||||||
|
&__name { font-weight: 600; }
|
||||||
|
&__mobile { font-family: monospace; font-size: 12px; color: var(--admin-text-muted); }
|
||||||
|
&__date { font-size: 12px; color: var(--admin-text-secondary); }
|
||||||
|
&__action { text-align: center; white-space: nowrap; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-done-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(52,211,153,0.15);
|
||||||
|
color: #059669;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 99px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 52px 16px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&__icon { width: 52px; height: 52px; border-radius: 50%; background: rgba(52,211,153,0.12); color: #059669; display: flex; align-items: center; justify-content: center; }
|
||||||
|
&__title { font-size: 14px; font-weight: 700; color: var(--admin-text); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid var(--admin-border);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<MobileCardList
|
||||||
|
v-model:search="search"
|
||||||
|
:loading="loading"
|
||||||
|
:has-more="hasMore"
|
||||||
|
:show-empty="!loading && items.length === 0"
|
||||||
|
empty-text="سندی یافت نشد"
|
||||||
|
@search="reload"
|
||||||
|
@load-more="loadMore"
|
||||||
|
>
|
||||||
|
<TahesabMobileListItem
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
@open="openSheet"
|
||||||
|
/>
|
||||||
|
</MobileCardList>
|
||||||
|
|
||||||
|
<TahesabMobileDetailSheet
|
||||||
|
v-model:visible="sheetVisible"
|
||||||
|
:item="selectedItem"
|
||||||
|
@retry="openRetry"
|
||||||
|
@register-customer="openRegister"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TahesabRetryModal
|
||||||
|
v-model="retryVisible"
|
||||||
|
mode="retry-sanad"
|
||||||
|
:tahesab-id="selectedItem?.id"
|
||||||
|
@success="onRetrySuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TahesabRetryModal
|
||||||
|
v-model="registerVisible"
|
||||||
|
mode="register-customer"
|
||||||
|
:customer-id="selectedItem?.customer_id"
|
||||||
|
:customer-name="selectedItem?.customer?.name"
|
||||||
|
@success="onRetrySuccess"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getTahesabTable, type TahesabRecord } from '@/api/tahesab'
|
||||||
|
import MobileCardList from '@/components/mobile/MobileCardList.vue'
|
||||||
|
import { useMobileDetailSheet } from '@/components/mobile/composables/useMobileDetailSheet'
|
||||||
|
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
|
||||||
|
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
||||||
|
import TahesabMobileDetailSheet from './TahesabMobileDetailSheet.vue'
|
||||||
|
import TahesabMobileListItem from './TahesabMobileListItem.vue'
|
||||||
|
|
||||||
|
const { sheetVisible, selectedItem, openSheet, syncSelectedItem } = useMobileDetailSheet<TahesabRecord>()
|
||||||
|
const retryVisible = ref(false)
|
||||||
|
const registerVisible = ref(false)
|
||||||
|
|
||||||
|
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList<TahesabRecord>({
|
||||||
|
fetchApi: getTahesabTable,
|
||||||
|
onFetched: (rows) => syncSelectedItem(rows)
|
||||||
|
})
|
||||||
|
|
||||||
|
function openRetry(item: TahesabRecord) {
|
||||||
|
selectedItem.value = item
|
||||||
|
sheetVisible.value = false
|
||||||
|
retryVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRegister(item: TahesabRecord) {
|
||||||
|
selectedItem.value = item
|
||||||
|
sheetVisible.value = false
|
||||||
|
registerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRetrySuccess() {
|
||||||
|
await fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => reload())
|
||||||
|
|
||||||
|
defineExpose({ fetch })
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<MobileDetailSheet v-model:visible="visibleProxy" :item="item" :title="sheetTitle">
|
||||||
|
<template #default="{ item: row }">
|
||||||
|
<MobileSheetHeader :title="sheetTitle" :show-title="isMobile">
|
||||||
|
<template #actions>
|
||||||
|
<MobileIconButton
|
||||||
|
v-if="!row.tahesab_id && !row.customer?.moshtari_code"
|
||||||
|
plain
|
||||||
|
icon="vuesax-linear:profile-add"
|
||||||
|
color="#7C3AED"
|
||||||
|
:size="22"
|
||||||
|
@click="$emit('register-customer', row)"
|
||||||
|
/>
|
||||||
|
<MobileIconButton
|
||||||
|
v-if="canRetry(row)"
|
||||||
|
plain
|
||||||
|
icon="vuesax-linear:refresh-circle"
|
||||||
|
color="#CA8A04"
|
||||||
|
:size="22"
|
||||||
|
@click="$emit('retry', row)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</MobileSheetHeader>
|
||||||
|
|
||||||
|
<MobileInfoGrid>
|
||||||
|
<MobileDetailRow label="شناسه" :value="row.id" />
|
||||||
|
<MobileDetailRow label="مشتری" :value="row.customer?.name || '-'" />
|
||||||
|
<MobileDetailRow label="موبایل" :value="row.customer?.mobile || '-'" />
|
||||||
|
<MobileDetailRow label="نوع" :value="row.type || '-'" />
|
||||||
|
<MobileDetailRow label="مقدار" :value="row.amount ?? '-'" />
|
||||||
|
<MobileDetailRow label="شرح" full-width :value="row.desc || '-'" />
|
||||||
|
<MobileDetailRow label="وضعیت" :value="tahesabStatusLabel(row.sync_status, row.tahesab_id)" />
|
||||||
|
<MobileDetailRow label="کد تهحساب" :value="row.tahesab_id || '-'" />
|
||||||
|
<MobileDetailRow label="زمان" :value="row.created_at ? toJalali(row.created_at) : '-'" />
|
||||||
|
<MobileDetailRow v-if="row.res" label="پاسخ API" full-width :value="row.res" />
|
||||||
|
</MobileInfoGrid>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="row.sync_log"
|
||||||
|
class="mt-3 rounded-lg bg-red-50 p-3 text-sm text-red-700 whitespace-pre-wrap"
|
||||||
|
>
|
||||||
|
{{ row.sync_log }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MobileDetailSheet>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TahesabRecord } from '@/api/tahesab'
|
||||||
|
import MobileDetailRow from '@/components/mobile/MobileDetailRow.vue'
|
||||||
|
import MobileDetailSheet from '@/components/mobile/MobileDetailSheet.vue'
|
||||||
|
import MobileIconButton from '@/components/mobile/MobileIconButton.vue'
|
||||||
|
import MobileInfoGrid from '@/components/mobile/MobileInfoGrid.vue'
|
||||||
|
import MobileSheetHeader from '@/components/mobile/MobileSheetHeader.vue'
|
||||||
|
import { canRetryTahesab, tahesabStatusLabel } from '@/pages/menuPages/accounting/utils/tahesabStatus'
|
||||||
|
import { isMobile } from '@core/utils/is'
|
||||||
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
item: TahesabRecord | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', value: boolean): void
|
||||||
|
(e: 'retry', item: TahesabRecord): void
|
||||||
|
(e: 'register-customer', item: TahesabRecord): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visibleProxy = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (value: boolean) => emit('update:visible', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const sheetTitle = computed(() => props.item?.customer?.name || 'سند تهحساب')
|
||||||
|
|
||||||
|
function canRetry(row: TahesabRecord) {
|
||||||
|
return canRetryTahesab(row)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<MobileCard clickable @click="$emit('open', item)">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<div class="truncate text-sm font-700 text-gray-900">{{ customerName }}</div>
|
||||||
|
<span class="shrink-0 rounded-full px-2 py-0.5 text-[11px] font-600" :class="statusClass">
|
||||||
|
{{ statusText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-gray-600">
|
||||||
|
<span>نوع: <span class="font-600 text-gray-900">{{ item.type || '-' }}</span></span>
|
||||||
|
<span>مقدار: <span class="font-600 text-gray-900">{{ item.amount ?? '-' }}</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">{{ createdAtLabel }}</div>
|
||||||
|
</div>
|
||||||
|
</MobileCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TahesabRecord } from '@/api/tahesab'
|
||||||
|
import MobileCard from '@/components/mobile/MobileCard.vue'
|
||||||
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
import { tahesabStatusLabel } from '@/pages/menuPages/accounting/utils/tahesabStatus'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: TahesabRecord
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'open', item: TahesabRecord): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const customerName = computed(() => props.item.customer?.name || '-')
|
||||||
|
const createdAtLabel = computed(() =>
|
||||||
|
props.item.created_at ? toJalali(props.item.created_at) : '-'
|
||||||
|
)
|
||||||
|
const statusText = computed(() =>
|
||||||
|
tahesabStatusLabel(props.item.sync_status, props.item.tahesab_id)
|
||||||
|
)
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const text = statusText.value
|
||||||
|
if (text === 'موفق') return 'bg-green-100 text-green-700'
|
||||||
|
if (text === 'ناموفق') return 'bg-red-100 text-red-700'
|
||||||
|
return 'bg-amber-100 text-amber-700'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -2,141 +2,31 @@
|
|||||||
<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="حسابداری">
|
||||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable v-bind="tblProps" @register="submit" />
|
||||||
<template #action="{ record }">
|
|
||||||
<div class="flex justify-center gap-2">
|
|
||||||
<a-tooltip
|
|
||||||
v-if="showTahesab && record.tahesab_record_id"
|
|
||||||
placement="bottom"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="openTahesabDetail(record)"
|
|
||||||
>
|
|
||||||
<template #title><span>جزئیات تهحساب</span></template>
|
|
||||||
<Icon icon="vuesax-linear:document-text" color="#10913A" />
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip
|
|
||||||
v-if="showTahesab && record.tahesab_failed_id"
|
|
||||||
placement="bottom"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="openTahesabRetry(record)"
|
|
||||||
>
|
|
||||||
<template #title><span>ارسال به تهحساب</span></template>
|
|
||||||
<Icon icon="vuesax-linear:refresh-circle" color="#CA8A04" />
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
|
|
||||||
<template #title><span>برداشت و واریز</span></template>
|
|
||||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('orders', record)">
|
|
||||||
<template #title><span>سفارشات</span></template>
|
|
||||||
<Icon icon="vuesax-linear:receipt-item" color="#10913A" />
|
|
||||||
</a-tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</BaseTable>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountingMobileCardList
|
<AccountingMobileCardList v-else ref="mobileListRef" />
|
||||||
v-else
|
|
||||||
ref="mobileListRef"
|
|
||||||
@info="emits('info', $event)"
|
|
||||||
@orders="emits('orders', $event)"
|
|
||||||
/>
|
|
||||||
</BasicShowContext>
|
</BasicShowContext>
|
||||||
|
|
||||||
<TahesabRetryModal
|
|
||||||
v-if="showTahesab"
|
|
||||||
v-model="tahesabRetryVisible"
|
|
||||||
mode="retry-sanad"
|
|
||||||
:tahesab-id="selectedTahesabId"
|
|
||||||
@success="fetch"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BasicModal
|
|
||||||
v-if="showTahesab"
|
|
||||||
v-model:visible="tahesabDetailVisible" title="جزئیات سند تهحساب" width="36rem" :footer="null">
|
|
||||||
<div class="space-y-2 text-sm">
|
|
||||||
<div v-for="item in tahesabDetailItems" :key="item.label" class="flex justify-between gap-3 border-b border-gray-100 py-2">
|
|
||||||
<span class="text-gray-500">{{ item.label }}</span>
|
|
||||||
<span class="break-all text-left">{{ item.value ?? '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="tahesabDetail?.sync_log" class="rounded-lg bg-red-50 p-3 text-red-700 whitespace-pre-wrap">
|
|
||||||
{{ tahesabDetail.sync_log }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BasicModal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { getTotalAccountingTable } from '@/api/accounting'
|
import { getTotalAccountingTable } from '@/api/accounting'
|
||||||
import { getTahesabDetail, type TahesabRecord } from '@/api/tahesab'
|
|
||||||
import { getAccountingColumns } from '@/pages/menuPages/accounting/hooks/accountingColumns'
|
import { getAccountingColumns } from '@/pages/menuPages/accounting/hooks/accountingColumns'
|
||||||
import AccountingMobileCardList from '@/pages/menuPages/accounting/components/AccountingMobileCardList.vue'
|
import AccountingMobileCardList from '@/pages/menuPages/accounting/components/AccountingMobileCardList.vue'
|
||||||
import Icon from '@core/components/BaseIcon/src/Icon.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 { isMobile } from '@core/utils/is'
|
import { isMobile } from '@core/utils/is'
|
||||||
import TahesabRetryModal from '@/components/TahesabRetryModal.vue'
|
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
customerId?: number
|
|
||||||
showTahesab?: boolean
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
showTahesab: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(e: 'info', record: unknown): void
|
|
||||||
(e: 'orders', record: unknown): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
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 tahesabRetryVisible = ref(false)
|
|
||||||
const tahesabDetailVisible = ref(false)
|
|
||||||
const selectedTahesabId = ref<number>()
|
|
||||||
const tahesabDetail = ref<TahesabRecord | null>(null)
|
|
||||||
|
|
||||||
const tahesabDetailItems = computed(() => {
|
|
||||||
const row = tahesabDetail.value
|
|
||||||
if (!row) return []
|
|
||||||
return [
|
|
||||||
{ label: 'شناسه', value: row.id },
|
|
||||||
{ label: 'مشتری', value: row.customer?.name },
|
|
||||||
{ label: 'نوع', value: row.type },
|
|
||||||
{ label: 'مقدار', value: row.amount },
|
|
||||||
{ label: 'شرح', value: row.desc },
|
|
||||||
{ label: 'وضعیت', value: tahesabStatusLabel(row) },
|
|
||||||
{ label: 'کد تهحساب', value: row.tahesab_id }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
function tahesabStatusLabel(row?: TahesabRecord | null) {
|
|
||||||
if (!row) return '-'
|
|
||||||
if (row.tahesab_id) return 'موفق'
|
|
||||||
if (row.sync_status === 'failed') return 'ناموفق'
|
|
||||||
if (row.sync_status === 'success') return 'موفق'
|
|
||||||
return 'در انتظار'
|
|
||||||
}
|
|
||||||
|
|
||||||
const tblProps = ref({
|
const tblProps = ref({
|
||||||
columns: getAccountingColumns(props.showTahesab),
|
columns: getAccountingColumns(),
|
||||||
api: (params: TableInput) => getTotalAccountingTable(params)
|
api: (params: TableInput) => getTotalAccountingTable(params)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.showTahesab,
|
|
||||||
(enabled) => {
|
|
||||||
tblProps.value.columns = getAccountingColumns(enabled)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
await mobileListRef.value?.fetch?.()
|
await mobileListRef.value?.fetch?.()
|
||||||
@@ -145,22 +35,5 @@ async function fetch() {
|
|||||||
await reload()
|
await reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTahesabRetry(record: { tahesab_failed_id?: number }) {
|
|
||||||
if (!record.tahesab_failed_id) return
|
|
||||||
selectedTahesabId.value = record.tahesab_failed_id
|
|
||||||
tahesabRetryVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openTahesabDetail(record: { tahesab_record_id?: number }) {
|
|
||||||
if (!record.tahesab_record_id) return
|
|
||||||
selectedTahesabId.value = record.tahesab_record_id
|
|
||||||
try {
|
|
||||||
tahesabDetail.value = await getTahesabDetail(record.tahesab_record_id)
|
|
||||||
tahesabDetailVisible.value = true
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ fetch })
|
defineExpose({ fetch })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,43 +5,79 @@
|
|||||||
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
|
<BaseTable 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="openDetail(record)">
|
|
||||||
<template #title><span>جزئیات</span></template>
|
<!-- دکمه ۱: اطلاعات کلی (همیشه نمایان) -->
|
||||||
|
<a-tooltip placement="bottom" class="cursor-pointer" @click="openInfo(record)">
|
||||||
|
<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="canRetry(record)"
|
v-if="!record.tahesab_id && !record.customer?.moshtari_code"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
@click="openRetry(record)"
|
@click="openRegister(record)"
|
||||||
|
>
|
||||||
|
<template #title><span>ثبت مشتری در تهحساب</span></template>
|
||||||
|
<Icon icon="vuesax-linear:profile-add" color="#7C3AED" />
|
||||||
|
</a-tooltip>
|
||||||
|
|
||||||
|
<!-- دکمه ۳: ارسال به تهحساب (فقط اگر هنوز در تهحساب ثبت نشده) -->
|
||||||
|
<a-tooltip
|
||||||
|
v-if="!record.tahesab_id"
|
||||||
|
placement="bottom"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="openSend(record)"
|
||||||
>
|
>
|
||||||
<template #title><span>ارسال به تهحساب</span></template>
|
<template #title><span>ارسال به تهحساب</span></template>
|
||||||
<Icon icon="vuesax-linear:refresh-circle" 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" />
|
||||||
</BasicShowContext>
|
</BasicShowContext>
|
||||||
|
|
||||||
<BasicModal v-model:visible="detailVisible" 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 v-for="item in detailItems" :key="item.label" class="flex justify-between gap-3 border-b border-gray-100 py-2">
|
<div
|
||||||
|
v-for="item in infoItems"
|
||||||
|
:key="item.label"
|
||||||
|
class="flex justify-between gap-3 border-b border-gray-100 py-2"
|
||||||
|
>
|
||||||
<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">
|
||||||
|
{{ 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 }}
|
{{ selected.sync_log }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
|
|
||||||
|
<!-- مودال ارسال به تهحساب -->
|
||||||
<TahesabRetryModal
|
<TahesabRetryModal
|
||||||
v-model="retryVisible"
|
v-model="sendVisible"
|
||||||
mode="retry-sanad"
|
mode="retry-sanad"
|
||||||
:tahesab-id="selected?.id"
|
:tahesab-id="selected?.id"
|
||||||
@success="fetch"
|
@success="fetch"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- مودال ثبت مشتری در تهحساب -->
|
||||||
|
<TahesabRetryModal
|
||||||
|
v-model="registerVisible"
|
||||||
|
mode="register-customer"
|
||||||
|
:customer-id="selected?.customer_id"
|
||||||
|
:customer-name="selected?.customer?.name"
|
||||||
|
@success="fetch"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -54,15 +90,26 @@ 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 { tahesabStatusLabel } from '@/pages/menuPages/accounting/utils/tahesabStatus'
|
||||||
import { toJalali } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
|
|
||||||
const [submit, reload] = useBaseTable()
|
const [submit, reload] = useBaseTable()
|
||||||
const detailVisible = ref(false)
|
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||||
const retryVisible = ref(false)
|
const infoVisible = ref(false)
|
||||||
|
const sendVisible = ref(false)
|
||||||
|
const registerVisible = ref(false)
|
||||||
const selected = ref<TahesabRecord | null>(null)
|
const selected = ref<TahesabRecord | null>(null)
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: 'ردیف', dataIndex: 'id', key: 'id', filterable: false, sortable: false, customRender: (r: any) => r.index + 1 },
|
{
|
||||||
|
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: 'customer.name', key: 'customer.name', type: 'Text' },
|
||||||
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
{ title: 'نوع', dataIndex: 'type', key: 'type', type: 'Text' },
|
||||||
{ title: 'مقدار', dataIndex: 'amount', key: 'amount', type: 'Number' },
|
{ title: 'مقدار', dataIndex: 'amount', key: 'amount', type: 'Number' },
|
||||||
@@ -72,7 +119,8 @@ const columns = [
|
|||||||
dataIndex: 'sync_status',
|
dataIndex: 'sync_status',
|
||||||
key: 'sync_status',
|
key: 'sync_status',
|
||||||
type: 'Text',
|
type: 'Text',
|
||||||
customRender: (r: any) => statusLabel(r.record.sync_status, r.record.tahesab_id)
|
filterable: false,
|
||||||
|
customRender: (r: any) => tahesabStatusLabel(r.record.sync_status, r.record.tahesab_id)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'زمان',
|
title: 'زمان',
|
||||||
@@ -81,7 +129,13 @@ const columns = [
|
|||||||
type: 'Text',
|
type: 'Text',
|
||||||
customRender: (r: any) => toJalali(r.record.created_at)
|
customRender: (r: any) => toJalali(r.record.created_at)
|
||||||
},
|
},
|
||||||
{ title: 'عملیات', dataIndex: 'action', key: 'action', slot: 'action', filterable: false }
|
{
|
||||||
|
title: 'عملیات',
|
||||||
|
dataIndex: 'action',
|
||||||
|
key: 'action',
|
||||||
|
slot: 'action',
|
||||||
|
filterable: false
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const tblProps = ref({
|
const tblProps = ref({
|
||||||
@@ -89,44 +143,43 @@ const tblProps = ref({
|
|||||||
api: (params: TableInput) => getTahesabTable(params)
|
api: (params: TableInput) => getTahesabTable(params)
|
||||||
})
|
})
|
||||||
|
|
||||||
const detailItems = 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.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: row.amount },
|
{ label: 'مقدار', value: row.amount },
|
||||||
{ label: 'شرح', value: row.desc },
|
{ label: 'شرح', value: row.desc },
|
||||||
{ label: 'وضعیت', value: statusLabel(row.sync_status, row.tahesab_id) },
|
{ label: 'زمان ثبت', value: row.created_at ? toJalali(row.created_at) : '-' },
|
||||||
{ label: 'کد تهحساب', value: row.tahesab_id },
|
{ label: 'وضعیت', value: tahesabStatusLabel(row.sync_status, row.tahesab_id) },
|
||||||
{ label: 'پاسخ API', value: row.res }
|
{ label: 'کد تهحساب', value: row.tahesab_id || 'ثبت نشده' }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
function statusLabel(status?: string, tahesabId?: string | null) {
|
function openInfo(record: TahesabRecord) {
|
||||||
if (tahesabId) return 'موفق'
|
|
||||||
if (status === 'failed') return 'ناموفق'
|
|
||||||
if (status === 'success') return 'موفق'
|
|
||||||
return 'در انتظار'
|
|
||||||
}
|
|
||||||
|
|
||||||
function canRetry(record: TahesabRecord) {
|
|
||||||
return !record.tahesab_id || record.sync_status === 'failed'
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDetail(record: TahesabRecord) {
|
|
||||||
selected.value = record
|
selected.value = record
|
||||||
detailVisible.value = true
|
infoVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRetry(record: TahesabRecord) {
|
function openSend(record: TahesabRecord) {
|
||||||
selected.value = record
|
selected.value = record
|
||||||
retryVisible.value = true
|
sendVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRegister(record: TahesabRecord) {
|
||||||
|
selected.value = record
|
||||||
|
registerVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
|
if (isMobile.value) {
|
||||||
|
await mobileListRef.value?.fetch?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
await reload()
|
await reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,106 +1,51 @@
|
|||||||
import type { BaseTableColumn } from '@core/type/baseTable'
|
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||||
import { toJalali, toJalaliNoTime, toTime } from '@core/utils/useDataTime'
|
import { toJalali } from '@core/utils/useDataTime'
|
||||||
import CustomTag from '@/components/CustomTag.vue'
|
|
||||||
import { usePrice } from '@core/utils/usePrice'
|
|
||||||
import { metalLabel } from '@/utils/adminMetalLabels'
|
import { metalLabel } from '@/utils/adminMetalLabels'
|
||||||
/*import BudgetSelect from '@/components/BudgetSelect.vue'
|
|
||||||
import { useStaticApiListsStore } from '@/store/staticApisStore'
|
|
||||||
import type { FilterSelect } from '@/model/filterSelect'
|
|
||||||
import type { DocItem } from '@/model/docType'*/
|
|
||||||
|
|
||||||
//define store
|
/**
|
||||||
//const store = useStaticApiListsStore()
|
* ستونهای دفترکل (گزارشات) — مستقل از هر سیستم حسابداری بیرونی.
|
||||||
//document types
|
* هیچ ستون مربوط به تهحساب یا سایر حسابداریها اینجا نیست.
|
||||||
/*let docTypes: DocItem[]*/
|
*/
|
||||||
/*onMounted(async () => {
|
export function getAccountingColumns(): BaseTableColumn[] {
|
||||||
docTypes = await store.readyDocTypeList()
|
|
||||||
})
|
|
||||||
const filteredDocTypes: FilterSelect = docTypes?.map((x: DocItem) => {
|
|
||||||
return {
|
|
||||||
label: x.title,
|
|
||||||
value: x.id
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
|
|
||||||
export function getAccountingColumns(showTahesab = false): BaseTableColumn[] {
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: 'ردیف',
|
title: 'ردیف',
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
key: 'id',
|
key: 'id',
|
||||||
filterable: false,
|
filterable: false,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
customRender: (record: any) => {
|
customRender: (record: any) => record.index + 1
|
||||||
return record.index + 1
|
},
|
||||||
|
{
|
||||||
|
title: 'نام مشتری',
|
||||||
|
dataIndex: 'customer_to.name',
|
||||||
|
key: 'customer_id',
|
||||||
|
type: 'Text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'زمان ثبت',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
type: 'Text',
|
||||||
|
customRender: (record: any) => toJalali(record.record.created_at)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'نقد (ریال)',
|
||||||
|
dataIndex: 'cash',
|
||||||
|
key: 'cash',
|
||||||
|
type: 'Number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: metalLabel.goldUnit(),
|
||||||
|
dataIndex: 'gold',
|
||||||
|
key: 'gold',
|
||||||
|
type: 'Number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'توضیحات',
|
||||||
|
dataIndex: 'note',
|
||||||
|
key: 'note',
|
||||||
|
type: 'Text'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'نام مشتری',
|
|
||||||
dataIndex: 'customer_to.name',
|
|
||||||
key: 'customer_id',
|
|
||||||
type: 'Number'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'نام مشتری دعوت شده',
|
|
||||||
dataIndex: 'customer.name',
|
|
||||||
key: 'customer_id',
|
|
||||||
type: 'Number'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'زمان ثبت',
|
|
||||||
dataIndex: 'created_at',
|
|
||||||
key: 'created_at',
|
|
||||||
type: 'Text',
|
|
||||||
customRender: (record: any) => {
|
|
||||||
return toJalali(record.record.created_at)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'نقد',
|
|
||||||
dataIndex: 'cash',
|
|
||||||
key: 'cash',
|
|
||||||
type: 'Number',
|
|
||||||
// customRender(data) {
|
|
||||||
// return usePrice(data.record?.price ?? 0, true, '')
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: metalLabel.goldUnit(),
|
|
||||||
dataIndex: 'gold',
|
|
||||||
key: 'gold',
|
|
||||||
type: 'Number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'توضیحات',
|
|
||||||
dataIndex: 'note',
|
|
||||||
key: 'note',
|
|
||||||
type: 'Text',
|
|
||||||
},
|
|
||||||
...(showTahesab
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: 'تهحساب',
|
|
||||||
dataIndex: 'tahesab_sync_status',
|
|
||||||
key: 'tahesab_sync_status',
|
|
||||||
type: 'Text',
|
|
||||||
filterable: false,
|
|
||||||
customRender: (record: any) => {
|
|
||||||
const row = record.record
|
|
||||||
if (!row.tahesab_record_id && !row.tahesab_sync_status) return '-'
|
|
||||||
if (row.tahesab_failed_id) return 'ناموفق'
|
|
||||||
if (row.tahesab_sync_status === 'success') return 'موفق'
|
|
||||||
if (row.tahesab_sync_status === 'pending') return 'در انتظار'
|
|
||||||
return row.tahesab_sync_status ?? '-'
|
|
||||||
}
|
|
||||||
} as BaseTableColumn
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
title: 'عملیات',
|
|
||||||
dataIndex: 'action',
|
|
||||||
key: 'action',
|
|
||||||
slot: 'action',
|
|
||||||
filterable: false
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicMainCard>
|
<BasicMainCard>
|
||||||
<div class="flex h-full w-full min-w-0 max-w-full flex-col overflow-x-hidden">
|
<div
|
||||||
<ATabs v-if="tahesabEnabled" v-model:activeKey="activeTab" class="mb-3 shrink-0">
|
class="flex h-full w-full min-w-0 max-w-full flex-col overflow-x-hidden"
|
||||||
|
:class="activeTab === 'customer-sync' ? 'overflow-y-auto' : 'overflow-hidden'"
|
||||||
|
>
|
||||||
|
<ATabs v-model:activeKey="activeTab" class="mb-3 shrink-0">
|
||||||
<ATabPane key="reports" tab="گزارشات" />
|
<ATabPane key="reports" tab="گزارشات" />
|
||||||
<ATabPane key="tahesab" tab="اسناد تهحساب" />
|
<ATabPane v-if="tahesabEnabled" key="tahesab" tab="اسناد تهحساب" />
|
||||||
|
<ATabPane v-if="tahesabEnabled" key="customer-sync" tab="همگام سازی مشتریان در تهحساب" />
|
||||||
</ATabs>
|
</ATabs>
|
||||||
<tbl
|
<tbl v-if="activeTab === 'reports'" ref="table" class="min-h-0 flex-1" />
|
||||||
v-if="!tahesabEnabled || activeTab === 'reports'"
|
<tahesabTbl v-else-if="tahesabEnabled && activeTab === 'tahesab'" ref="tahesabTable" class="min-h-0 flex-1" />
|
||||||
ref="table"
|
<customerSync v-else-if="tahesabEnabled && activeTab === 'customer-sync'" class="shrink-0" />
|
||||||
class="min-h-0 flex-1"
|
|
||||||
:show-tahesab="tahesabEnabled"
|
|
||||||
@info="onAccountingWithdraw"
|
|
||||||
@orders="onAccountingOrders"
|
|
||||||
/>
|
|
||||||
<tahesabTbl v-else-if="tahesabEnabled" ref="tahesabTable" class="min-h-0 flex-1" />
|
|
||||||
</div>
|
</div>
|
||||||
</BasicMainCard>
|
</BasicMainCard>
|
||||||
<BasicModal
|
<BasicModal
|
||||||
@@ -135,6 +133,7 @@ import { useStaticApiListsStore } from '@/store/staticApisStore'
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import tbl from './components/table.vue'
|
import tbl from './components/table.vue'
|
||||||
import tahesabTbl from './components/tahesabTable.vue'
|
import tahesabTbl from './components/tahesabTable.vue'
|
||||||
|
import customerSync from './components/TahesabCustomerSync.vue'
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
|
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
|
||||||
import { message } from 'ant-design-vue/es'
|
import { message } from 'ant-design-vue/es'
|
||||||
@@ -145,7 +144,6 @@ import { cloneDeep } from 'lodash'
|
|||||||
import type { Item } from '@/model/Item'
|
import type { Item } from '@/model/Item'
|
||||||
import type { Customer } from '@/model/customer'
|
import type { Customer } from '@/model/customer'
|
||||||
import { createCustomer, deleteCustomer, getCustomersList, updateCustomer } from '@/api/customer'
|
import { createCustomer, deleteCustomer, getCustomersList, updateCustomer } from '@/api/customer'
|
||||||
import type { TotalAccounting } from '@/model/accounting'
|
|
||||||
import type { Order } from '@/model/order'
|
import type { Order } from '@/model/order'
|
||||||
import { usePrice } from '@core/utils/usePrice'
|
import { usePrice } from '@core/utils/usePrice'
|
||||||
import { getLastPrice } from '@/api/price'
|
import { getLastPrice } from '@/api/price'
|
||||||
@@ -157,7 +155,7 @@ import { initTahesabSettingFromApi, tahesabEnabledSetting } from '@/utils/config
|
|||||||
|
|
||||||
//loading
|
//loading
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const activeTab = ref<'reports' | 'tahesab'>('reports')
|
const activeTab = ref<'reports' | 'tahesab' | 'customer-sync'>('reports')
|
||||||
const tahesabEnabled = tahesabEnabledSetting
|
const tahesabEnabled = tahesabEnabledSetting
|
||||||
|
|
||||||
//define store
|
//define store
|
||||||
@@ -242,14 +240,6 @@ function stopGetPrice() {
|
|||||||
//info
|
//info
|
||||||
let infoParams = reactive<Info>({})
|
let infoParams = reactive<Info>({})
|
||||||
|
|
||||||
function onAccountingWithdraw(_record: TotalAccounting) {
|
|
||||||
message.info('برداشت و واریز بهزودی در دسترس است')
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAccountingOrders(_record: TotalAccounting) {
|
|
||||||
message.info('لیست سفارشات بهزودی در دسترس است')
|
|
||||||
}
|
|
||||||
|
|
||||||
function info(val: Order) {
|
function info(val: Order) {
|
||||||
infoParams.name = val.customer.name
|
infoParams.name = val.customer.name
|
||||||
infoParams.type = val.customer.mobile
|
infoParams.type = val.customer.mobile
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export function tahesabStatusLabel(status?: string, tahesabId?: string | null): string {
|
||||||
|
if (tahesabId) 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'
|
||||||
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SettingValueField from './SettingValueField.vue'
|
import SettingValueField from './SettingValueField.vue'
|
||||||
import { helpText, resolveSettingFieldType, type SettingFieldType } from './settingTypes'
|
import {
|
||||||
|
helpText,
|
||||||
|
KEY_LABEL_FALLBACK,
|
||||||
|
resolveSettingFieldType,
|
||||||
|
type SettingFieldType
|
||||||
|
} from './settingTypes'
|
||||||
import type { Const } from '@/model/configuration'
|
import type { Const } from '@/model/configuration'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -58,7 +63,9 @@ const emit = defineEmits<{
|
|||||||
const settingKey = computed(() => props.row.key ?? '')
|
const settingKey = computed(() => props.row.key ?? '')
|
||||||
const settingType = computed(() => props.row.type)
|
const settingType = computed(() => props.row.type)
|
||||||
const description = computed(() => props.row.description)
|
const description = computed(() => props.row.description)
|
||||||
const title = computed(() => props.row.title_fa || props.row.key || '—')
|
const title = computed(
|
||||||
|
() => props.row.title_fa || (props.row.key && KEY_LABEL_FALLBACK[props.row.key]) || props.row.key || '—'
|
||||||
|
)
|
||||||
|
|
||||||
const fieldType = computed<SettingFieldType>(() =>
|
const fieldType = computed<SettingFieldType>(() =>
|
||||||
resolveSettingFieldType({ key: props.row.key, type: props.row.type })
|
resolveSettingFieldType({ key: props.row.key, type: props.row.type })
|
||||||
|
|||||||
@@ -31,6 +31,18 @@
|
|||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
|
||||||
|
<a-time-picker
|
||||||
|
v-else-if="fieldType === 'time'"
|
||||||
|
:value="modelValue || undefined"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="w-full"
|
||||||
|
format="HH:mm"
|
||||||
|
value-format="HH:mm:ss"
|
||||||
|
:minute-step="5"
|
||||||
|
placeholder="انتخاب ساعت"
|
||||||
|
@update:value="onTimeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<a-input-number
|
<a-input-number
|
||||||
v-else-if="fieldType === 'price'"
|
v-else-if="fieldType === 'price'"
|
||||||
:value="priceValue"
|
:value="priceValue"
|
||||||
@@ -130,6 +142,12 @@ function onSelectChange(val: string | undefined) {
|
|||||||
emit('update:value', val ?? '')
|
emit('update:value', val ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onTimeChange(val: string | string[] | undefined) {
|
||||||
|
const v = Array.isArray(val) ? val[0] : val
|
||||||
|
emit('update:value', v ?? '')
|
||||||
|
emit('blur')
|
||||||
|
}
|
||||||
|
|
||||||
const priceValue = computed(() => {
|
const priceValue = computed(() => {
|
||||||
const n = parseFloat(String(props.modelValue || '').replace(/,/g, ''))
|
const n = parseFloat(String(props.modelValue || '').replace(/,/g, ''))
|
||||||
return Number.isFinite(n) ? n : undefined
|
return Number.isFinite(n) ? n : undefined
|
||||||
|
|||||||
@@ -16,12 +16,18 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
|||||||
k
|
k
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'trading_hours',
|
||||||
|
title: 'ساعت کاری و نمایش قیمت',
|
||||||
|
icon: 'vuesax-linear:clock',
|
||||||
|
match: (k) => /^(close_price|startDay|endDay|show_last_price)$/.test(k)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'pricing',
|
id: 'pricing',
|
||||||
title: 'کارمزد و محدودیتها',
|
title: 'کارمزد و محدودیتها',
|
||||||
icon: 'vuesax-linear:dollar-circle',
|
icon: 'vuesax-linear:dollar-circle',
|
||||||
match: (k) =>
|
match: (k) =>
|
||||||
/^(fee_|buy_fee|sell_fee|min_|minimum_|show_last_price|close_price|startDay|endDay|up_price|down_price|price_extreme_date|balance_report_exclude)/.test(
|
/^(fee_|buy_fee|sell_fee|min_|minimum_|up_price|down_price|price_extreme_date|balance_report_exclude)/.test(
|
||||||
k
|
k
|
||||||
) || k.includes('charge_percentage')
|
) || k.includes('charge_percentage')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Const } from '@/model/configuration'
|
import type { Const } from '@/model/configuration'
|
||||||
import { isStoreApp } from '@/utils/config'
|
import { isStoreApp } from '@/utils/config'
|
||||||
|
|
||||||
export type SettingFieldType = 'text' | 'number' | 'price' | 'switch' | 'select' | 'longtext'
|
export type SettingFieldType = 'text' | 'number' | 'price' | 'switch' | 'select' | 'longtext' | 'time'
|
||||||
|
|
||||||
/** کلیدهای legacy — در پنل نمایش داده نمیشوند */
|
/** کلیدهای legacy — در پنل نمایش داده نمیشوند */
|
||||||
export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
|
export const HIDDEN_LEGACY_SETTING_KEYS = new Set([
|
||||||
@@ -42,7 +42,8 @@ export const SETTING_TYPE_OPTIONS = [
|
|||||||
{ key: 'price', value: 'مبلغ (ریال)' },
|
{ key: 'price', value: 'مبلغ (ریال)' },
|
||||||
{ key: 'switch', value: 'روشن/خاموش' },
|
{ key: 'switch', value: 'روشن/خاموش' },
|
||||||
{ key: 'select', value: 'انتخابی' },
|
{ key: 'select', value: 'انتخابی' },
|
||||||
{ key: 'longtext', value: 'متن بلند' }
|
{ key: 'longtext', value: 'متن بلند' },
|
||||||
|
{ key: 'time', value: 'ساعت' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||||
@@ -73,6 +74,8 @@ const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
|||||||
open_register: 'switch',
|
open_register: 'switch',
|
||||||
auto_active_customer: 'switch',
|
auto_active_customer: 'switch',
|
||||||
close_price: 'switch',
|
close_price: 'switch',
|
||||||
|
startDay: 'time',
|
||||||
|
endDay: 'time',
|
||||||
show_last_price: 'switch',
|
show_last_price: 'switch',
|
||||||
send_to_tahesab: 'switch',
|
send_to_tahesab: 'switch',
|
||||||
profit_gold_api_enabled: 'switch',
|
profit_gold_api_enabled: 'switch',
|
||||||
@@ -136,7 +139,7 @@ export function referralRequiredToString(checked: boolean): string {
|
|||||||
|
|
||||||
export function resolveSettingFieldType(row: Pick<Const, 'key' | 'type'>): SettingFieldType {
|
export function resolveSettingFieldType(row: Pick<Const, 'key' | 'type'>): SettingFieldType {
|
||||||
const raw = (row.type || '').trim().toLowerCase()
|
const raw = (row.type || '').trim().toLowerCase()
|
||||||
if (['text', 'number', 'price', 'switch', 'select', 'longtext'].includes(raw)) {
|
if (['text', 'number', 'price', 'switch', 'select', 'longtext', 'time'].includes(raw)) {
|
||||||
return raw as SettingFieldType
|
return raw as SettingFieldType
|
||||||
}
|
}
|
||||||
if (row.key && KEY_TYPE_FALLBACK[row.key]) {
|
if (row.key && KEY_TYPE_FALLBACK[row.key]) {
|
||||||
@@ -205,6 +208,9 @@ export function formatValueForSave(
|
|||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
return String(value ?? '').replace(/,/g, '')
|
return String(value ?? '').replace(/,/g, '')
|
||||||
}
|
}
|
||||||
|
if (type === 'time') {
|
||||||
|
return normalizeTimeValue(value)
|
||||||
|
}
|
||||||
return value ?? ''
|
return value ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,12 +224,45 @@ export function switchLabels(key: string | undefined): { on: string; off: string
|
|||||||
return { on: 'فعال', off: 'غیرفعال' }
|
return { on: 'فعال', off: 'غیرفعال' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** توضیحات کامل و ساده برای کلیدهای مهم — اگر description در دیتابیس خالی بود استفاده میشود */
|
||||||
|
const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||||
|
close_price:
|
||||||
|
'فیلتر اصلی فروشگاه. وقتی روشن باشد، خرید و فروش و نمایش قیمت فقط در بازهٔ «ساعت شروع» تا «ساعت پایان» انجام میشود و خارج از این بازه به مشتری پیام «فروشگاه بسته است» نشان داده میشود. اگر خاموش باشد، فروشگاه همیشه باز است (مگر اینکه برای محصولی بهصورت اختصاصی ساعت تعریف کرده باشید).',
|
||||||
|
startDay:
|
||||||
|
'ساعت شروع فعالیت فروشگاه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال میشود. ساعت را از روی ساعتشمار انتخاب کنید (مثلاً ۰۹:۰۰).',
|
||||||
|
endDay:
|
||||||
|
'ساعت پایان فعالیت فروشگاه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال میشود. بعد از این ساعت فروشگاه بسته میشود (مثلاً ۱۷:۰۰).',
|
||||||
|
show_last_price:
|
||||||
|
'وقتی قیمت زندهٔ لحظهای در دسترس نباشد: اگر روشن باشد، آخرین قیمت ذخیرهشده در دیتابیس به مشتری نشان داده میشود؛ اگر خاموش باشد، بهجای قیمت صفر پیام «مظنه در دسترس نیست» نمایش داده میشود.'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** برچسب فارسی ساده برای کلیدهای مهم — اگر سرور برچسبی نفرستد */
|
||||||
|
export const KEY_LABEL_FALLBACK: Record<string, string> = {
|
||||||
|
close_price: 'بستن خودکار خارج از ساعت کاری',
|
||||||
|
startDay: 'ساعت شروع',
|
||||||
|
endDay: 'ساعت پایان',
|
||||||
|
show_last_price: 'نمایش آخرین قیمت ذخیرهشده'
|
||||||
|
}
|
||||||
|
|
||||||
export function helpText(row: Const): string {
|
export function helpText(row: Const): string {
|
||||||
const desc = (row.description || '').trim()
|
const desc = (row.description || '').trim()
|
||||||
if (!desc) return ''
|
if (!desc) {
|
||||||
|
return (row.key && KEY_HELP_FALLBACK[row.key]) || ''
|
||||||
|
}
|
||||||
if (resolveSettingFieldType(row) === 'select' && desc.includes(',')) {
|
if (resolveSettingFieldType(row) === 'select' && desc.includes(',')) {
|
||||||
const parts = desc.split('|')
|
const parts = desc.split('|')
|
||||||
return parts.length > 1 ? parts[0].trim() : ''
|
return parts.length > 1 ? parts[0].trim() : ''
|
||||||
}
|
}
|
||||||
return desc
|
return desc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** نرمالسازی ساعت به فرمت HH:mm:ss برای ذخیره و سازگاری با بکاند */
|
||||||
|
export function normalizeTimeValue(value: string | undefined | null): string {
|
||||||
|
const raw = String(value ?? '').trim()
|
||||||
|
if (!raw) return ''
|
||||||
|
const parts = raw.split(':')
|
||||||
|
const pad = (n: string) => n.padStart(2, '0')
|
||||||
|
if (parts.length >= 3) return `${pad(parts[0])}:${pad(parts[1])}:${pad(parts[2])}`
|
||||||
|
if (parts.length === 2) return `${pad(parts[0])}:${pad(parts[1])}:00`
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
@click="openTahesabRegister(record)"
|
@click="openTahesabRegister(record)"
|
||||||
>
|
>
|
||||||
<template #title><span>ثبت در تهحساب</span></template>
|
<template #title><span>در تهحساب ثبت نشده — کلیک برای ثبت</span></template>
|
||||||
<Icon icon="vuesax-linear:refresh-circle" color="#CA8A04" />
|
<Icon icon="vuesax-linear:profile-add" color="#CA8A04" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
|
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
|
||||||
<template #title><span>مشخصات</span></template>
|
<template #title><span>مشخصات</span></template>
|
||||||
|
|||||||
@@ -225,11 +225,30 @@
|
|||||||
<a-form-item label="هزینه نقدی موقع فروش" name="cash_fee_sell" :rules="requiredPriceRules('هزینه نقدی موقع فروش')">
|
<a-form-item label="هزینه نقدی موقع فروش" name="cash_fee_sell" :rules="requiredPriceRules('هزینه نقدی موقع فروش')">
|
||||||
<BasicInput v-model:value="form.cash_fee_sell" addon-before="ریال" type="price" />
|
<BasicInput v-model:value="form.cash_fee_sell" addon-before="ریال" type="price" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item label="ساعت کاری اختصاصی این محصول" name="override_trading_hours">
|
||||||
|
<a-switch v-model:checked="form.override_trading_hours" />
|
||||||
|
<div class="text-xs text-gray-500 mt-1 leading-6">
|
||||||
|
روشن: این محصول ساعت شروع و پایان مخصوص خودش را دارد و ساعت کاری سراسری فروشگاه را نادیده میگیرد (override).
|
||||||
|
خاموش: همان ساعت کاری سراسری فروشگاه برای این محصول اعمال میشود و زمانهای زیر نادیده گرفته میشوند.
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label="زمان شروع فعال سازی" name="auto_open_at">
|
<a-form-item label="زمان شروع فعال سازی" name="auto_open_at">
|
||||||
<TimePicker v-model:value="form.auto_open_at" :locale="customLocale" placeholder="انتخاب زمان" :defaultValue="dayjs('00:00:00', 'HH:mm:ss')" />
|
<TimePicker
|
||||||
|
v-model:value="form.auto_open_at"
|
||||||
|
:locale="customLocale"
|
||||||
|
placeholder="انتخاب زمان"
|
||||||
|
:disabled="!form.override_trading_hours"
|
||||||
|
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="زمان پایان فعال سازی" name="auto_close_at">
|
<a-form-item label="زمان پایان فعال سازی" name="auto_close_at">
|
||||||
<TimePicker v-model:value="form.auto_close_at" :locale="customLocale" placeholder="انتخاب زمان" :defaultValue="dayjs('00:00:00', 'HH:mm:ss')" />
|
<TimePicker
|
||||||
|
v-model:value="form.auto_close_at"
|
||||||
|
:locale="customLocale"
|
||||||
|
placeholder="انتخاب زمان"
|
||||||
|
:disabled="!form.override_trading_hours"
|
||||||
|
:defaultValue="dayjs('00:00:00', 'HH:mm:ss')"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="محدودیت مبلغ سفارش" name="order_limit_enabled">
|
<a-form-item label="محدودیت مبلغ سفارش" name="order_limit_enabled">
|
||||||
<a-switch v-model:checked="form.order_limit_enabled" @change="onOrderLimitChange" />
|
<a-switch v-model:checked="form.order_limit_enabled" @change="onOrderLimitChange" />
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ async function edit(record: Product) {
|
|||||||
...productData,
|
...productData,
|
||||||
show_in_assets: productData.show_in_assets ?? false,
|
show_in_assets: productData.show_in_assets ?? false,
|
||||||
trade_by_quantity: productData.trade_by_quantity ?? false,
|
trade_by_quantity: productData.trade_by_quantity ?? false,
|
||||||
|
override_trading_hours: productData.override_trading_hours ?? false,
|
||||||
price_offset_percent: productData.price_offset_percent ?? 0,
|
price_offset_percent: productData.price_offset_percent ?? 0,
|
||||||
direct_price: productData.direct_price ?? 0,
|
direct_price: productData.direct_price ?? 0,
|
||||||
auto_open_at: start.isValid() ? start : dayjs('00:00:00', 'HH:mm:ss'),
|
auto_open_at: start.isValid() ? start : dayjs('00:00:00', 'HH:mm:ss'),
|
||||||
@@ -382,6 +383,7 @@ function resetData() {
|
|||||||
order_limit_enabled: false,
|
order_limit_enabled: false,
|
||||||
show_in_assets: false,
|
show_in_assets: false,
|
||||||
trade_by_quantity: false,
|
trade_by_quantity: false,
|
||||||
|
override_trading_hours: false,
|
||||||
use_last_price_if_zero: false,
|
use_last_price_if_zero: false,
|
||||||
inventory: 0,
|
inventory: 0,
|
||||||
min_order_cash: undefined,
|
min_order_cash: undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user