From 3df3164e0b59883e80c71a071da4ab2d4baf8f3e Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Sun, 1 Nov 2026 13:13:09 +0330 Subject: [PATCH] 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 --- src/api/copperConvert.ts | 27 +++ .../CopperConvertMobileCardList.vue | 45 +++++ .../CopperConvertMobileListItem.vue | 51 ++++++ .../copperConvert/components/table.vue | 149 +++++++++++++++ .../copperConvert/copperConvertTable.ts | 170 ++++++++++++++++++ src/pages/menuPages/copperConvert/index.vue | 13 ++ src/router/routes/hesabKetab.ts | 11 ++ src/utils/adminHelpCopy.ts | 3 + src/utils/adminPageAccess.ts | 1 + 9 files changed, 470 insertions(+) create mode 100644 src/api/copperConvert.ts create mode 100644 src/pages/menuPages/copperConvert/components/CopperConvertMobileCardList.vue create mode 100644 src/pages/menuPages/copperConvert/components/CopperConvertMobileListItem.vue create mode 100644 src/pages/menuPages/copperConvert/components/table.vue create mode 100644 src/pages/menuPages/copperConvert/copperConvertTable.ts create mode 100644 src/pages/menuPages/copperConvert/index.vue diff --git a/src/api/copperConvert.ts b/src/api/copperConvert.ts new file mode 100644 index 0000000..d61f1ad --- /dev/null +++ b/src/api/copperConvert.ts @@ -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('/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`) diff --git a/src/pages/menuPages/copperConvert/components/CopperConvertMobileCardList.vue b/src/pages/menuPages/copperConvert/components/CopperConvertMobileCardList.vue new file mode 100644 index 0000000..75ee46f --- /dev/null +++ b/src/pages/menuPages/copperConvert/components/CopperConvertMobileCardList.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/pages/menuPages/copperConvert/components/CopperConvertMobileListItem.vue b/src/pages/menuPages/copperConvert/components/CopperConvertMobileListItem.vue new file mode 100644 index 0000000..6eff22f --- /dev/null +++ b/src/pages/menuPages/copperConvert/components/CopperConvertMobileListItem.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/pages/menuPages/copperConvert/components/table.vue b/src/pages/menuPages/copperConvert/components/table.vue new file mode 100644 index 0000000..d6a94e4 --- /dev/null +++ b/src/pages/menuPages/copperConvert/components/table.vue @@ -0,0 +1,149 @@ + + + diff --git a/src/pages/menuPages/copperConvert/copperConvertTable.ts b/src/pages/menuPages/copperConvert/copperConvertTable.ts new file mode 100644 index 0000000..7f94ad6 --- /dev/null +++ b/src/pages/menuPages/copperConvert/copperConvertTable.ts @@ -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, 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 { + 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, 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)[sortKey] + const bv = (b as Record)[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 } +} diff --git a/src/pages/menuPages/copperConvert/index.vue b/src/pages/menuPages/copperConvert/index.vue new file mode 100644 index 0000000..f38ec4e --- /dev/null +++ b/src/pages/menuPages/copperConvert/index.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/router/routes/hesabKetab.ts b/src/router/routes/hesabKetab.ts index 05f0c5b..048f6f8 100644 --- a/src/router/routes/hesabKetab.ts +++ b/src/router/routes/hesabKetab.ts @@ -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', diff --git a/src/utils/adminHelpCopy.ts b/src/utils/adminHelpCopy.ts index a5edc72..9c0ac7b 100644 --- a/src/utils/adminHelpCopy.ts +++ b/src/utils/adminHelpCopy.ts @@ -210,6 +210,9 @@ export const conditionalOrdersPageIntro = export const colleagueOrdersPageIntro = 'تراکنش‌های پوشش‌شده با اپ همکار. وضعیت هر ردیف بعد از ارسال به همکار به‌روز می‌شود (در انتظار، موفق، ناموفق). برای ثبت درست سند حسابداری، کد حساب همکار را هم در همین صفحه تنظیم کنید.' +export const copperConvertPageIntro = + 'درخواست فروش دارایی مشتری و خرید حلقه سیم ۱.۵ و ۲.۵. تأیید، دارایی را می‌فروشد و حلقه‌ها را با قیمت همان درخواست می‌خرد.' + export const colleagueAccountingCodeHelp = 'کدی که سند خرید/فروش پوشش‌شدهٔ همکار روی آن در سیستم حسابداری فعال ثبت می‌شود. فقط فیلد مربوط به حسابداری فعال را پر کنید.' diff --git a/src/utils/adminPageAccess.ts b/src/utils/adminPageAccess.ts index b1a840a..01cbdd6 100644 --- a/src/utils/adminPageAccess.ts +++ b/src/utils/adminPageAccess.ts @@ -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' } ] },