added some text handling 2

This commit is contained in:
2026-07-08 11:24:25 +03:30
parent 434b6a6ede
commit 89e2643dff
7 changed files with 680 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
import type { TableInput } from '@core/type/baseTable'
import useAxios from '@core/utils/useAxios'
import type { Withdraw } from '@/model/wallet'
export interface PhysicalWithdrawProductOption {
product_id: number
title: string
wallet_id: number
available: number
unit: string
is_main_wallet?: boolean
}
export interface PhysicalWithdrawAddressOption {
id: number
province?: string
city?: string
address?: string
phone?: string
zip?: string
default_address?: boolean
}
export interface PhysicalWithdrawOptions {
products: PhysicalWithdrawProductOption[]
addresses: PhysicalWithdrawAddressOption[]
}
export interface PhysicalWithdrawLineInput {
wallet_id: number
value: number
}
export interface CreatePhysicalWithdrawPayload {
customer_id: number
address_id: number
wallets: PhysicalWithdrawLineInput[]
}
export const getPhysicalWithdrawTable = async (params?: TableInput) =>
await useAxios.post<Array<Withdraw>>('/admin/wallet/withdraw_physical/table', params)
export const getPhysicalWithdrawOptions = async (customerId: number) =>
await useAxios.get<PhysicalWithdrawOptions>(`/admin/wallet/withdraw_physical/options/${customerId}`)
export const createPhysicalWithdraw = async (payload: CreatePhysicalWithdrawPayload) =>
await useAxios.post<{ id: number }>('/admin/wallet/withdraw_physical', payload)
export const adjustPhysicalWithdraw = async (
withdrawId: number,
payload: { transaction_id: number; direction: 'increase' | 'decrease' }
) => await useAxios.post(`/admin/wallet/withdraw_physical/${withdrawId}/adjust`, payload)
export const approvePhysicalWithdraw = async (withdrawId: number, sts = 51) =>
await useAxios.put(`/admin/wallet/withdraw/${withdrawId}`, { sts })
+2
View File
@@ -60,8 +60,10 @@ export interface WalletTableSums {
}
export interface WithdrawTransaction {
id?: number
title?: string
gold?: number
product?: { title?: string; amount?: number }
}
export interface Withdraw {
@@ -0,0 +1,187 @@
<template>
<a-form ref="formRef" :model="form" layout="vertical" autocomplete="off">
<a-form-item label="مشتری" name="customer_id" :rules="inputRules()">
<BasicSelect
v-model:value="form.customer_id"
show-search
:options="customerOptions"
placeholder="انتخاب مشتری"
@change="onCustomerChange"
/>
</a-form-item>
<a-form-item v-if="form.customer_id" label="آدرس تحویل" name="address_id" :rules="inputRules()">
<BasicSelect
v-model:value="form.address_id"
:options="addressOptions"
placeholder="انتخاب آدرس"
:loading="optionsLoading"
/>
</a-form-item>
<div v-if="form.customer_id" class="space-y-3">
<div class="text-sm font-semibold">محصولات</div>
<div
v-for="(line, index) in form.lines"
:key="`${line.wallet_id}-${index}`"
class="grid grid-cols-1 gap-2 rounded-lg border border-gray-200 p-3 md:grid-cols-[1fr_120px_auto]"
>
<a-form-item
class="!mb-0"
:name="['lines', index, 'wallet_id']"
:rules="inputRules()"
label="محصول"
>
<BasicSelect
v-model:value="line.wallet_id"
:options="productOptionsForLine(index)"
placeholder="انتخاب محصول"
:loading="optionsLoading"
@change="() => onProductChange(index)"
/>
</a-form-item>
<a-form-item
class="!mb-0"
:name="['lines', index, 'value']"
:rules="inputRules()"
label="مقدار"
>
<BasicInput v-model:value="line.value" type="number" min="0" />
</a-form-item>
<div class="flex items-end">
<BaseButton
v-if="form.lines.length > 1"
type="text"
danger
class="mb-1"
@click="removeLine(index)"
>
حذف
</BaseButton>
</div>
<div v-if="line.wallet_id" class="col-span-full text-xs text-gray-500">
موجودی: {{ availableForWallet(line.wallet_id) }}
</div>
</div>
<BaseButton type="dashed" block icon="vuesax-linear:add" @click="addLine">افزودن محصول</BaseButton>
</div>
</a-form>
</template>
<script setup lang="ts">
import { inputRules } from '@/utils/inputRules'
import type { Customer } from '@/model/customer'
import type { FormInstance } from 'ant-design-vue'
import {
getPhysicalWithdrawOptions,
type PhysicalWithdrawAddressOption,
type PhysicalWithdrawProductOption
} from '@/api/physicalWithdraw'
export interface PhysicalWithdrawFormLine {
wallet_id?: number
value?: number
}
export interface PhysicalWithdrawForm {
customer_id?: number
address_id?: number
lines: PhysicalWithdrawFormLine[]
}
const props = defineProps<{
form: PhysicalWithdrawForm
customers: Customer[]
}>()
const formRef = ref<FormInstance>()
const optionsLoading = ref(false)
const products = ref<PhysicalWithdrawProductOption[]>([])
const addresses = ref<PhysicalWithdrawAddressOption[]>([])
const customerOptions = computed(() =>
(props.customers ?? []).map((customer) => ({
label: `${customer.name ?? '-'} (${customer.mobile ?? '-'})`,
value: customer.id
}))
)
const addressOptions = computed(() =>
addresses.value.map((addr) => ({
label: [addr.province, addr.city, addr.address].filter(Boolean).join(' / '),
value: addr.id
}))
)
function productOptionsForLine(index: number) {
const usedWalletIds = props.form.lines
.map((line, i) => (i === index ? undefined : line.wallet_id))
.filter(Boolean)
return products.value
.filter((product) => !usedWalletIds.includes(product.wallet_id))
.map((product) => ({
label: `${product.title} (موجودی: ${product.available} ${product.unit})`,
value: product.wallet_id
}))
}
function availableForWallet(walletId?: number) {
const product = products.value.find((item) => item.wallet_id === walletId)
if (!product) return '-'
return `${product.available} ${product.unit}`
}
function addLine() {
props.form.lines.push({ wallet_id: undefined, value: undefined })
}
function removeLine(index: number) {
props.form.lines.splice(index, 1)
}
function onProductChange(index: number) {
const line = props.form.lines[index]
const product = products.value.find((item) => item.wallet_id === line.wallet_id)
if (product && !line.value) {
line.value = 1
}
}
async function onCustomerChange() {
props.form.address_id = undefined
props.form.lines = [{ wallet_id: undefined, value: undefined }]
products.value = []
addresses.value = []
if (!props.form.customer_id) return
try {
optionsLoading.value = true
const data = await getPhysicalWithdrawOptions(props.form.customer_id)
products.value = data.products ?? []
addresses.value = data.addresses ?? []
if (addresses.value.length === 1) {
props.form.address_id = addresses.value[0].id
}
} catch (e) {
console.error(e)
} finally {
optionsLoading.value = false
}
}
async function validateFields() {
return formRef.value?.validateFields()
}
function resetFields() {
formRef.value?.resetFields()
}
defineExpose({
validateFields,
resetFields
})
</script>
@@ -0,0 +1,187 @@
<template>
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
<BasicShowContext title="لیست برداشت فیزیکی">
<div class="flex h-full min-h-0 flex-col">
<BaseTable class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
<template #extraHeader>
<BaseButton type="primary" size="small" icon="vuesax-linear:add-square" @click="emits('add')">
ثبت برداشت فیزیکی
</BaseButton>
</template>
<template #action="{ record }">
<div class="flex flex-col items-center gap-2 py-1">
<div
v-for="tx in record.transaction ?? []"
:key="tx.id"
class="flex w-full max-w-[220px] items-center justify-between gap-2 rounded border border-gray-200 px-2 py-1"
>
<span class="truncate text-xs">{{ tx.title?.trim() || 'محصول' }}</span>
<span class="shrink-0 text-xs font-semibold">{{ Math.abs(Number(tx.gold ?? 0)) }}</span>
<div v-if="canAdjust(record)" class="flex shrink-0 gap-1">
<BaseButton
size="small"
class="!min-w-0 !px-2"
:loading="isAdjusting(record.id, tx.id, 'decrease')"
@click="handleAdjust(record, tx, 'decrease')"
>
کم
</BaseButton>
<BaseButton
size="small"
type="primary"
class="!min-w-0 !px-2"
:loading="isAdjusting(record.id, tx.id, 'increase')"
@click="handleAdjust(record, tx, 'increase')"
>
زیاد
</BaseButton>
</div>
</div>
<div class="flex flex-wrap items-center justify-center gap-2">
<BaseButton
v-if="canApprove(record)"
size="small"
type="primary"
:loading="isApproving(record.id)"
@click="handleApprove(record)"
>
تایید
</BaseButton>
<ADropdown v-if="canChangeStatus(record)" :trigger="['click']">
<BaseButton size="small">وضعیت</BaseButton>
<template #overlay>
<a-menu class="flex flex-col">
<a-menu-item
v-for="(item, key) in statusOptions(record.sts)"
:key="key"
class="border-none px-2 py-1"
@click="handleStatusChange(record.id, Number(key))"
>
{{ item.title }}
</a-menu-item>
</a-menu>
</template>
</ADropdown>
<a-tooltip v-if="record.sts >= 50 && record.sts <= 70">
<template #title>دریافت فاکتور</template>
<Icon
icon="vuesax-linear:document-download"
color="#1D4ED8"
class="cursor-pointer"
@click="$router.push({ name: 'factor', params: { id: record.id } })"
/>
</a-tooltip>
</div>
</div>
</template>
</BaseTable>
</div>
</BasicShowContext>
</div>
</template>
<script lang="tsx" setup>
import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { getPhysicalWithdrawColumns } from '../hooks/physicalWithdrawColumns'
import {
adjustPhysicalWithdraw,
approvePhysicalWithdraw,
getPhysicalWithdrawTable
} from '@/api/physicalWithdraw'
import { updateSts } from '@/api/wallet'
import { message } from 'ant-design-vue'
import type { Withdraw } from '@/model/wallet'
import { status as physicalStatusMap } from '@/pages/menuPages/withdraw/information/hooks/status'
const emits = defineEmits(['add'])
const [submit, reload] = useBaseTable()
const adjustingKey = ref('')
const approvingId = ref<number | null>(null)
const tblProps = computed(() => ({
columns: getPhysicalWithdrawColumns(),
api: (params: TableInput) => getPhysicalWithdrawTable(params)
}))
function canAdjust(record: Withdraw) {
return Number(record.sts) === 50
}
function canApprove(record: Withdraw) {
return Number(record.sts) === 50
}
function canChangeStatus(record: Withdraw) {
const sts = Number(record.sts)
return sts >= 50 && sts <= 53
}
function statusOptions(currentSts: number) {
return Object.fromEntries(
Object.entries(physicalStatusMap).filter(([key]) => Number(key) !== Number(currentSts))
)
}
function adjustKey(withdrawId?: number, transactionId?: number, direction?: string) {
return `${withdrawId ?? 0}-${transactionId ?? 0}-${direction ?? ''}`
}
function isAdjusting(withdrawId?: number, transactionId?: number, direction?: string) {
return adjustingKey.value === adjustKey(withdrawId, transactionId, direction)
}
function isApproving(withdrawId?: number) {
return approvingId.value === withdrawId
}
async function handleAdjust(
record: Withdraw,
tx: { id?: number },
direction: 'increase' | 'decrease'
) {
if (!record.id || !tx.id) return
const key = adjustKey(record.id, tx.id, direction)
adjustingKey.value = key
try {
await adjustPhysicalWithdraw(record.id, { transaction_id: tx.id, direction })
message.success('مقدار بروزرسانی شد')
await reload()
} catch (e) {
console.error(e)
} finally {
if (adjustingKey.value === key) adjustingKey.value = ''
}
}
async function handleApprove(record: Withdraw) {
if (!record.id) return
approvingId.value = record.id
try {
await approvePhysicalWithdraw(record.id, 51)
message.success('برداشت فیزیکی تایید شد')
await reload()
} catch (e) {
console.error(e)
} finally {
approvingId.value = null
}
}
async function handleStatusChange(withdrawId: number, sts: number) {
try {
await updateSts(withdrawId, { sts })
message.success('وضعیت ثبت شد')
await reload()
} catch (e) {
console.error(e)
}
}
defineExpose({
fetch: reload
})
</script>
@@ -0,0 +1,96 @@
import type { BaseTableColumn } from '@core/type/baseTable'
import { toJalali } from '@core/utils/useDataTime'
import CustomTag from '@/components/CustomTag.vue'
import type { Withdraw } from '@/model/wallet'
function formatProductsSummary(record: Withdraw): string {
const lines = record.transaction ?? []
if (!lines.length) return '-'
return lines
.map((tx) => {
const qty = Math.abs(Number(tx.gold ?? 0))
const title = String(tx.title ?? '').trim() || 'محصول'
return `${title}: ${qty}`
})
.join(' | ')
}
function formatAddress(record: Withdraw & { address?: { province?: string; city?: string; address?: string } }): string {
const addr = record.address ?? (record as { Address?: { province?: string; city?: string; address?: string } }).Address
if (!addr) return '-'
const parts = [addr.province, addr.city, addr.address].filter(Boolean)
return parts.length ? parts.join(' / ') : '-'
}
export function getPhysicalWithdrawColumns(): BaseTableColumn[] {
return [
{
title: 'ردیف',
dataIndex: 'id',
key: 'row',
filterable: false,
sortable: false,
customRender: (record: { index: number }) => record.index + 1
},
{
title: 'کد سند',
dataIndex: 'id',
key: 'id',
type: 'Number'
},
{
title: 'مشتری',
dataIndex: 'customer.name',
key: 'customer.name',
type: 'Text'
},
{
title: 'محصولات',
dataIndex: 'transaction',
key: 'products',
filterable: false,
sortable: false,
customRender: ({ record }) => (
<div class="max-w-xs whitespace-normal text-center text-xs leading-5">{formatProductsSummary(record)}</div>
)
},
{
title: 'آدرس',
dataIndex: 'address',
key: 'address',
filterable: false,
sortable: false,
customRender: ({ record }) => (
<div class="max-w-xs whitespace-normal text-center text-xs leading-5">{formatAddress(record)}</div>
)
},
{
title: 'وضعیت',
dataIndex: 'sts',
key: 'sts',
filterable: false,
customRender: ({ record }) => (
<CustomTag textColor="#ca8a04" bgColor="#fef3c7">
{record?.sts_str ?? '-'}
</CustomTag>
)
},
{
title: 'تاریخ ثبت',
dataIndex: 'created_at',
key: 'created_at',
type: 'DateTime',
customRender: (record: { record: { created_at?: string } }) => toJalali(record.record.created_at)
},
{
title: 'عملیات',
dataIndex: 'action',
key: 'action',
width: 260,
slot: 'action',
filterable: false,
fixed: 'right'
}
]
}
@@ -0,0 +1,122 @@
<template>
<div class="h-full min-h-0">
<BasicMainCard>
<div class="flex h-full min-h-0 w-full flex-col overflow-hidden">
<tbl ref="table" class="min-h-0 flex-1 w-full" @add="openCreate" />
</div>
</BasicMainCard>
<BasicActionSheet
v-model:visible="createVisible"
title="ثبت برداشت فیزیکی"
modal-width="32rem"
drawer-height="90vh"
:loading="loading"
:show-button="true"
:show-cancel="true"
:show-ok="true"
ok-text="ثبت"
cancel-text="انصراف"
@ok="submitCreate"
@close="closeCreate"
@cancel="closeCreate"
>
<PhysicalWithdrawEditorFormBody
ref="editorRef"
:form="form"
:customers="customers"
/>
</BasicActionSheet>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import tbl from './components/table.vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import PhysicalWithdrawEditorFormBody, {
type PhysicalWithdrawForm
} from './components/PhysicalWithdrawEditorFormBody.vue'
import { createPhysicalWithdraw } from '@/api/physicalWithdraw'
import { getCustomersList } from '@/api/customer'
import type { Customer } from '@/model/customer'
const table = ref<{ fetch: () => Promise<void> } | null>(null)
const editorRef = ref<{
validateFields?: () => Promise<unknown>
resetFields?: () => void
}>()
const loading = ref(false)
const createVisible = ref(false)
const customers = ref<Customer[]>([])
const form = ref<PhysicalWithdrawForm>({
customer_id: undefined,
address_id: undefined,
lines: [{ wallet_id: undefined, value: undefined }]
})
async function loadCustomers() {
try {
customers.value = (await getCustomersList()) ?? []
} catch (e) {
console.error(e)
}
}
function resetForm() {
form.value = {
customer_id: undefined,
address_id: undefined,
lines: [{ wallet_id: undefined, value: undefined }]
}
editorRef.value?.resetFields?.()
}
function openCreate() {
resetForm()
createVisible.value = true
}
function closeCreate() {
createVisible.value = false
}
async function submitCreate() {
try {
await editorRef.value?.validateFields?.()
} catch {
return
}
const lines = form.value.lines.filter((line) => line.wallet_id && Number(line.value) > 0)
if (!lines.length) {
message.error('حداقل یک محصول با مقدار معتبر انتخاب کنید')
return
}
loading.value = true
try {
await createPhysicalWithdraw({
customer_id: Number(form.value.customer_id),
address_id: Number(form.value.address_id),
wallets: lines.map((line) => ({
wallet_id: Number(line.wallet_id),
value: Number(line.value)
}))
})
message.success('برداشت فیزیکی با موفقیت ثبت شد')
createVisible.value = false
resetForm()
await table.value?.fetch?.()
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
onMounted(() => {
void loadCustomers()
})
</script>
+31
View File
@@ -0,0 +1,31 @@
import type { RouteRecordRaw } from 'vue-router'
const PhysicalWithdraw: RouteRecordRaw = {
name: 'physicalWithdraw',
path: '/physical-withdraw',
component: () => import('@core/layouts/BasicLayout.vue'),
redirect: { name: 'physicalWithdrawMain' },
meta: {
icon: 'vuesax-linear:truck-tick',
title: 'برداشت فیزیکی',
disabled: false,
hideMenu: false,
hideChildren: true,
order: 7.5
},
children: [
{
name: 'physicalWithdrawMain',
path: '',
component: () => import('@/pages/menuPages/physicalWithdraw/index.vue'),
meta: {
icon: 'vuesax-linear:truck-tick',
title: 'برداشت فیزیکی',
disabled: false,
hideMenu: false
}
}
]
}
export default PhysicalWithdraw