feat(convert): show convert requests under hesab ketab
Admins review pending cathode-to-wire requests in the shared table, and on mobile they see cards. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import useAxios from '@core/utils/useAxios'
|
||||
|
||||
export interface CopperConvertRequestRow {
|
||||
id: number
|
||||
status: 'pending' | 'approved' | 'rejected' | string
|
||||
source: string
|
||||
customer_name: string
|
||||
customer_mobile: string
|
||||
asset_unit: string
|
||||
consumed_amount: number
|
||||
leftover_amount: number
|
||||
source_unit_price: number
|
||||
wire15_rings: number
|
||||
wire15_unit_price: number
|
||||
wire25_rings: number
|
||||
wire25_unit_price: number
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export const getCopperConvertRequests = () =>
|
||||
useAxios.get<CopperConvertRequestRow[]>('/admin/copper-convert-request')
|
||||
|
||||
export const approveCopperConvertRequest = (id: number) =>
|
||||
useAxios.post(`/admin/copper-convert-request/${id}/approve`)
|
||||
|
||||
export const rejectCopperConvertRequest = (id: number) =>
|
||||
useAxios.post(`/admin/copper-convert-request/${id}/reject`)
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<MobileCardList
|
||||
v-model:search="search"
|
||||
:loading="loading"
|
||||
:has-more="hasMore"
|
||||
:show-empty="!loading && items.length === 0"
|
||||
empty-text="درخواستی یافت نشد"
|
||||
search-placeholder="جستجو مشتری، موبایل یا وضعیت"
|
||||
show-refresh
|
||||
@search="reload"
|
||||
@refresh="reload"
|
||||
@load-more="loadMore"
|
||||
>
|
||||
<CopperConvertMobileListItem
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:busy="busyId === item.id"
|
||||
:can-edit="can('edit')"
|
||||
@approve="approve(item)"
|
||||
@reject="reject(item)"
|
||||
/>
|
||||
</MobileCardList>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import MobileCardList from '@/components/mobile/MobileCardList.vue'
|
||||
import { useMobileTableList } from '@/components/mobile/composables/useMobileTableList'
|
||||
import { usePageActionAccess } from '@/composables/usePageActionAccess'
|
||||
import { fetchCopperConvertTable, useCopperConvertActions } from '../copperConvertTable'
|
||||
import CopperConvertMobileListItem from './CopperConvertMobileListItem.vue'
|
||||
|
||||
const { can } = usePageActionAccess()
|
||||
const { loading, search, items, hasMore, reload, loadMore, fetch } = useMobileTableList({
|
||||
fetchApi: fetchCopperConvertTable
|
||||
})
|
||||
const { busyId, approve, reject } = useCopperConvertActions(reload)
|
||||
|
||||
onMounted(() => {
|
||||
void reload()
|
||||
})
|
||||
|
||||
defineExpose({ fetch })
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<MobileCard>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-700 text-gray-900">{{ item.customer_name }}</div>
|
||||
<div class="text-xs text-gray-500">{{ item.customer_mobile || '-' }}</div>
|
||||
</div>
|
||||
<CustomTag class="shrink-0" :text-color="colors.text" :bg-color="colors.bg">
|
||||
{{ item.status_label }}
|
||||
</CustomTag>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1 text-xs">
|
||||
<span class="text-gray-600">دارایی: <span class="font-600 text-gray-900">{{ item.source_label }}</span></span>
|
||||
<span class="text-gray-600">واحد: <span class="font-600 text-gray-900">{{ item.asset_unit }}</span></span>
|
||||
<span class="text-gray-600">حلقه ۱.۵: <span class="font-600 text-gray-900">{{ item.wire15_rings }}</span></span>
|
||||
<span class="text-gray-600">حلقه ۲.۵: <span class="font-600 text-gray-900">{{ item.wire25_rings }}</span></span>
|
||||
<span class="text-gray-600">مقدار فروش: <span class="font-600 text-gray-900">{{ item.consumed_amount }}</span></span>
|
||||
<span class="text-gray-600">مانده: <span class="font-600 text-gray-900">{{ item.leftover_amount }}</span></span>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-400">{{ item.created_label }}</div>
|
||||
|
||||
<div v-if="item.status === 'pending' && canEdit" class="grid grid-cols-2 gap-2">
|
||||
<BaseButton type="primary" size="small" :loading="busy" @click="$emit('approve')">تأیید</BaseButton>
|
||||
<BaseButton size="small" :loading="busy" @click="$emit('reject')">رد</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</MobileCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
import MobileCard from '@/components/mobile/MobileCard.vue'
|
||||
import { statusColors, type CopperConvertTableRow } from '../copperConvertTable'
|
||||
|
||||
const props = defineProps<{
|
||||
item: CopperConvertTableRow
|
||||
busy?: boolean
|
||||
canEdit?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'approve'): void
|
||||
(e: 'reject'): void
|
||||
}>()
|
||||
|
||||
const colors = computed(() => statusColors(props.item.status))
|
||||
</script>
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||
<BasicShowContext title="درخواست تبدیل" :help="copperConvertPageIntro">
|
||||
<div v-if="!isMobile" class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||
<BaseTable v-bind="tblProps" @register="submit">
|
||||
<template #status="{ record }">
|
||||
<div class="flex justify-center">
|
||||
<CustomTag :text-color="statusColors(record.status).text" :bg-color="statusColors(record.status).bg">
|
||||
{{ record.status_label }}
|
||||
</CustomTag>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ record }">
|
||||
<div v-if="record.status === 'pending' && can('edit')" class="flex justify-center gap-2">
|
||||
<BaseButton type="primary" size="small" :loading="busyId === record.id" @click="approve(record)">
|
||||
تأیید
|
||||
</BaseButton>
|
||||
<BaseButton size="small" :loading="busyId === record.id" @click="reject(record)">رد</BaseButton>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<CopperConvertMobileCardList v-else />
|
||||
</BasicShowContext>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
import { copperConvertPageIntro } from '@/utils/adminHelpCopy'
|
||||
import { usePageActionAccess } from '@/composables/usePageActionAccess'
|
||||
import CopperConvertMobileCardList from './CopperConvertMobileCardList.vue'
|
||||
import { fetchCopperConvertTable, statusColors, useCopperConvertActions } from '../copperConvertTable'
|
||||
|
||||
const { can } = usePageActionAccess()
|
||||
const [submit, reload] = useBaseTable()
|
||||
const { busyId, approve, reject } = useCopperConvertActions(reload)
|
||||
|
||||
const columns: BaseTableColumn[] = [
|
||||
{
|
||||
title: 'شناسه',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
type: 'Number',
|
||||
width: 80,
|
||||
sortField: 'id',
|
||||
filterField: 'id'
|
||||
},
|
||||
{
|
||||
title: 'مشتری',
|
||||
dataIndex: 'customer_name',
|
||||
key: 'customer_name',
|
||||
type: 'Text',
|
||||
sortField: 'customer_name',
|
||||
filterField: 'customer_name'
|
||||
},
|
||||
{
|
||||
title: 'موبایل',
|
||||
dataIndex: 'customer_mobile',
|
||||
key: 'customer_mobile',
|
||||
type: 'Text',
|
||||
sortField: 'customer_mobile',
|
||||
filterField: 'customer_mobile'
|
||||
},
|
||||
{
|
||||
title: 'دارایی',
|
||||
dataIndex: 'source_label',
|
||||
key: 'source_label',
|
||||
type: 'Text',
|
||||
sortField: 'source_label',
|
||||
filterField: 'source_label'
|
||||
},
|
||||
{
|
||||
title: 'حلقه ۱.۵',
|
||||
dataIndex: 'wire15_rings',
|
||||
key: 'wire15_rings',
|
||||
type: 'Number',
|
||||
sortField: 'wire15_rings',
|
||||
filterField: 'wire15_rings'
|
||||
},
|
||||
{
|
||||
title: 'حلقه ۲.۵',
|
||||
dataIndex: 'wire25_rings',
|
||||
key: 'wire25_rings',
|
||||
type: 'Number',
|
||||
sortField: 'wire25_rings',
|
||||
filterField: 'wire25_rings'
|
||||
},
|
||||
{
|
||||
title: 'مقدار فروش',
|
||||
dataIndex: 'consumed_amount',
|
||||
key: 'consumed_amount',
|
||||
type: 'Number',
|
||||
sortField: 'consumed_amount',
|
||||
filterField: 'consumed_amount'
|
||||
},
|
||||
{
|
||||
title: 'مانده',
|
||||
dataIndex: 'leftover_amount',
|
||||
key: 'leftover_amount',
|
||||
type: 'Number',
|
||||
sortField: 'leftover_amount',
|
||||
filterField: 'leftover_amount'
|
||||
},
|
||||
{
|
||||
title: 'واحد',
|
||||
dataIndex: 'asset_unit',
|
||||
key: 'asset_unit',
|
||||
type: 'Text',
|
||||
sortField: 'asset_unit',
|
||||
filterField: 'asset_unit'
|
||||
},
|
||||
{
|
||||
title: 'وضعیت',
|
||||
dataIndex: 'status_label',
|
||||
key: 'status',
|
||||
type: 'Text',
|
||||
sortField: 'status_label',
|
||||
filterField: 'status_label',
|
||||
slot: 'status'
|
||||
},
|
||||
{
|
||||
title: 'تاریخ',
|
||||
dataIndex: 'created_label',
|
||||
key: 'created_label',
|
||||
type: 'Text',
|
||||
sortField: 'created_at',
|
||||
filterField: 'created_label'
|
||||
},
|
||||
{
|
||||
title: 'عملیات',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
slot: 'action',
|
||||
filterable: false,
|
||||
sortable: false
|
||||
}
|
||||
]
|
||||
|
||||
const tblProps = ref({
|
||||
columns,
|
||||
api: fetchCopperConvertTable
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,170 @@
|
||||
import { ref } from 'vue'
|
||||
import { Modal, message } from 'ant-design-vue'
|
||||
import type { Filter, TableInput, TableOutput } from '@core/type/baseTable'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import {
|
||||
approveCopperConvertRequest,
|
||||
getCopperConvertRequests,
|
||||
rejectCopperConvertRequest,
|
||||
type CopperConvertRequestRow
|
||||
} from '@/api/copperConvert'
|
||||
|
||||
export type CopperConvertTableRow = CopperConvertRequestRow & {
|
||||
source_label: string
|
||||
status_label: string
|
||||
created_label: string
|
||||
}
|
||||
|
||||
const numericKeys = new Set(['id', 'wire15_rings', 'wire25_rings', 'consumed_amount', 'leftover_amount'])
|
||||
|
||||
export function sourceLabel(source: string) {
|
||||
if (source === 'katod') return 'کاتد'
|
||||
if (source === 'maftoul') return 'مفتول'
|
||||
if (source === 'shamsh') return 'شمش'
|
||||
return source
|
||||
}
|
||||
|
||||
export function statusLabel(status: string) {
|
||||
if (status === 'pending') return 'در انتظار'
|
||||
if (status === 'approved') return 'تأیید شده'
|
||||
if (status === 'rejected') return 'رد شده'
|
||||
return status
|
||||
}
|
||||
|
||||
export function statusColors(status: string) {
|
||||
if (status === 'approved') return { text: '#25855A', bg: '#E3F4E9' }
|
||||
if (status === 'rejected') return { text: '#C53030', bg: '#FECACA' }
|
||||
return { text: '#B45309', bg: '#FEF3C7' }
|
||||
}
|
||||
|
||||
function decorate(row: CopperConvertRequestRow): CopperConvertTableRow {
|
||||
return {
|
||||
...row,
|
||||
source_label: sourceLabel(row.source),
|
||||
status_label: statusLabel(row.status),
|
||||
created_label: row.created_at ? toJalali(row.created_at) : '-'
|
||||
}
|
||||
}
|
||||
|
||||
function asText(value: unknown) {
|
||||
return value == null ? '' : String(value).toLowerCase()
|
||||
}
|
||||
|
||||
function matchFilter(row: Record<string, unknown>, filter: Filter) {
|
||||
const raw = row[filter.field]
|
||||
const op = filter.op
|
||||
const value = filter.value
|
||||
if (op === 'null') return raw == null || raw === ''
|
||||
if (op === 'nn') return raw != null && raw !== ''
|
||||
if (value === '' || value == null) return true
|
||||
if (op === 'bet' && Array.isArray(value)) {
|
||||
const n = Number(raw)
|
||||
return n >= Number(value[0]) && n <= Number(value[1])
|
||||
}
|
||||
if (op === 'in' && Array.isArray(value)) {
|
||||
return value.map(String).includes(String(raw ?? ''))
|
||||
}
|
||||
const text = asText(raw)
|
||||
const needle = asText(value)
|
||||
if (op === 'eq') return text === needle
|
||||
if (op === 'not') return text !== needle
|
||||
if (op === 'sw') return text.startsWith(needle)
|
||||
if (op === 'ew') return text.endsWith(needle)
|
||||
if (op === 'has' || op == null) return text.includes(needle)
|
||||
const left = Number(raw)
|
||||
const right = Number(value)
|
||||
if (!Number.isFinite(left) || !Number.isFinite(right)) return false
|
||||
if (op === 'gt') return left > right
|
||||
if (op === 'lt') return left < right
|
||||
if (op === 'gte') return left >= right
|
||||
if (op === 'lte') return left <= right
|
||||
return true
|
||||
}
|
||||
|
||||
function queryRows(rows: CopperConvertTableRow[], params: TableInput): TableOutput<CopperConvertTableRow> {
|
||||
const search = (params.search ?? '').trim().toLowerCase()
|
||||
let list = rows.filter((row) => {
|
||||
if (!search) return true
|
||||
return [row.customer_name, row.customer_mobile, row.source_label, row.status_label, row.asset_unit, row.created_label]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(search)
|
||||
})
|
||||
for (const filter of params.filters ?? []) {
|
||||
if (!filter.field) continue
|
||||
list = list.filter((row) => matchFilter(row as unknown as Record<string, unknown>, filter))
|
||||
}
|
||||
const sortKey = params.sort?.key
|
||||
if (sortKey) {
|
||||
const dir = params.sort?.order === 'desc' ? -1 : 1
|
||||
list = [...list].sort((a, b) => {
|
||||
const av = (a as Record<string, unknown>)[sortKey]
|
||||
const bv = (b as Record<string, unknown>)[sortKey]
|
||||
if (numericKeys.has(sortKey)) return (Number(av) - Number(bv)) * dir
|
||||
return String(av ?? '').localeCompare(String(bv ?? ''), 'fa') * dir
|
||||
})
|
||||
}
|
||||
const page = params.page || 1
|
||||
const limit = params.limit || 10
|
||||
return {
|
||||
rows: list.slice((page - 1) * limit, page * limit),
|
||||
query: {
|
||||
...params,
|
||||
page,
|
||||
limit,
|
||||
total: list.length,
|
||||
search: params.search ?? null,
|
||||
filters: params.filters ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCopperConvertTable(params: TableInput) {
|
||||
const data = await getCopperConvertRequests()
|
||||
const rows = (Array.isArray(data) ? data : []).map(decorate)
|
||||
return queryRows(rows, params)
|
||||
}
|
||||
|
||||
export function useCopperConvertActions(reload: () => unknown) {
|
||||
const busyId = ref(0)
|
||||
|
||||
function approve(row: CopperConvertRequestRow) {
|
||||
Modal.confirm({
|
||||
title: 'تأیید تبدیل',
|
||||
content: 'دارایی مشتری فروخته و حلقههای سیم برای او خریداری شود؟',
|
||||
okText: 'تأیید',
|
||||
cancelText: 'انصراف',
|
||||
async onOk() {
|
||||
busyId.value = row.id
|
||||
try {
|
||||
await approveCopperConvertRequest(row.id)
|
||||
message.success('تبدیل ثبت شد')
|
||||
await reload()
|
||||
} finally {
|
||||
busyId.value = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function reject(row: CopperConvertRequestRow) {
|
||||
Modal.confirm({
|
||||
title: 'رد درخواست',
|
||||
content: 'این درخواست تبدیل رد شود؟',
|
||||
okText: 'رد',
|
||||
cancelText: 'انصراف',
|
||||
async onOk() {
|
||||
busyId.value = row.id
|
||||
try {
|
||||
await rejectCopperConvertRequest(row.id)
|
||||
message.success('درخواست رد شد')
|
||||
await reload()
|
||||
} finally {
|
||||
busyId.value = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { busyId, approve, reject }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="h-full min-h-0">
|
||||
<BasicMainCard>
|
||||
<div class="flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden">
|
||||
<tbl class="min-h-0 flex-1" />
|
||||
</div>
|
||||
</BasicMainCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import tbl from './components/table.vue'
|
||||
</script>
|
||||
@@ -130,6 +130,17 @@ const HesabKetab: RouteRecordRaw = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'copperConvertMain',
|
||||
path: 'copper-convert',
|
||||
component: () => import('@/pages/menuPages/copperConvert/index.vue'),
|
||||
meta: {
|
||||
permissionsKey: 'copperConvertMain',
|
||||
icon: 'vuesax-linear:empty-wallet-time',
|
||||
title: 'درخواست تبدیل',
|
||||
order: 6.9
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'accountingMain',
|
||||
path: 'accounting',
|
||||
|
||||
@@ -210,6 +210,9 @@ export const conditionalOrdersPageIntro =
|
||||
export const colleagueOrdersPageIntro =
|
||||
'تراکنشهای پوشششده با اپ همکار. وضعیت هر ردیف بعد از ارسال به همکار بهروز میشود (در انتظار، موفق، ناموفق). برای ثبت درست سند حسابداری، کد حساب همکار را هم در همین صفحه تنظیم کنید.'
|
||||
|
||||
export const copperConvertPageIntro =
|
||||
'درخواست فروش دارایی مشتری و خرید حلقه سیم ۱.۵ و ۲.۵. تأیید، دارایی را میفروشد و حلقهها را با قیمت همان درخواست میخرد.'
|
||||
|
||||
export const colleagueAccountingCodeHelp =
|
||||
'کدی که سند خرید/فروش پوشششدهٔ همکار روی آن در سیستم حسابداری فعال ثبت میشود. فقط فیلد مربوط به حسابداری فعال را پر کنید.'
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ export const ADMIN_PAGE_TREE: AccessNode[] = [
|
||||
route: 'colleagueOrdersMain',
|
||||
enabled: () => priceProviderSetting.value === 'montzar'
|
||||
},
|
||||
{ key: 'copperConvertMain', title: 'درخواست تبدیل', route: 'copperConvertMain' },
|
||||
{ key: 'accountingMain', title: 'حسابداری', route: 'accountingMain' }
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user