feat(treasury): separate receipts from income and fit finance on a phone
A receipt or payment only reduces the party balance, while an income or expense keeps both amounts. Finance screens stack into cards on a phone so the forms and lists stay readable. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Segmented as ASegmented } from 'ant-design-vue';
|
||||
import { treasurySectionAllowed, type TreasurySectionKey } from '@mento/shared';
|
||||
@@ -8,6 +8,7 @@ import { useAuthStore } from '@/stores/auth';
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const scroller = ref<HTMLElement | null>(null);
|
||||
|
||||
const links = computed(() => {
|
||||
const role = auth.user?.role;
|
||||
@@ -41,6 +42,17 @@ const active = computed(() => {
|
||||
return '/finance';
|
||||
});
|
||||
|
||||
watch(
|
||||
active,
|
||||
async () => {
|
||||
await nextTick();
|
||||
scroller.value
|
||||
?.querySelector('.ant-segmented-item-selected')
|
||||
?.scrollIntoView({ inline: 'center', block: 'nearest' });
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onChange(value: string | number) {
|
||||
const next = String(value);
|
||||
if (next && next !== route.path) void router.push(next);
|
||||
@@ -49,6 +61,7 @@ function onChange(value: string | number) {
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="scroller"
|
||||
:style="{
|
||||
width: '100%',
|
||||
overflowX: 'auto',
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Table as ATable } from 'ant-design-vue';
|
||||
import {
|
||||
Flex as AFlex,
|
||||
List as AList,
|
||||
Table as ATable,
|
||||
Typography,
|
||||
} from 'ant-design-vue';
|
||||
import type { TreasuryPartyBalanceDto } from '@mento/shared';
|
||||
import { toman } from '@/views/treasury/treasury-format';
|
||||
import { useTreasuryMobile } from '@/views/treasury/useTreasuryMobile';
|
||||
|
||||
const props = defineProps<{
|
||||
rows: TreasuryPartyBalanceDto[];
|
||||
settledLabel: string;
|
||||
}>();
|
||||
|
||||
const isMobile = useTreasuryMobile();
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const AListItem = AList.Item;
|
||||
|
||||
const kindLabel: Record<string, string> = {
|
||||
person: 'مشتری',
|
||||
company: 'شرکت',
|
||||
@@ -39,10 +49,43 @@ const columns = computed(() => [
|
||||
customRender: ({ record }: { record: TreasuryPartyBalanceDto }) => toman(record.remaining),
|
||||
},
|
||||
]);
|
||||
|
||||
function kindOf(record: TreasuryPartyBalanceDto) {
|
||||
return (record.partyKind && kindLabel[record.partyKind]) || 'ثبت قبلی';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-list
|
||||
v-if="isMobile"
|
||||
:data-source="rows"
|
||||
:locale="{ emptyText: 'ماندهای نیست.' }"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-flex vertical :gap="4" style="width: 100%">
|
||||
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
|
||||
<a-typography-text strong>{{ item.partyName }}</a-typography-text>
|
||||
<a-typography-text type="secondary">{{ kindOf(item) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-flex justify="space-between" align="center" :gap="8">
|
||||
<a-typography-text type="secondary">مبلغ کل</a-typography-text>
|
||||
<a-typography-text>{{ toman(item.amount) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-flex justify="space-between" align="center" :gap="8">
|
||||
<a-typography-text type="secondary">{{ settledLabel }}</a-typography-text>
|
||||
<a-typography-text>{{ toman(item.settledAmount) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-flex justify="space-between" align="center" :gap="8">
|
||||
<a-typography-text type="secondary">مانده</a-typography-text>
|
||||
<a-typography-text strong>{{ toman(item.remaining) }}</a-typography-text>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-table
|
||||
v-else
|
||||
:columns="columns"
|
||||
:data-source="rows"
|
||||
:pagination="false"
|
||||
|
||||
@@ -25,6 +25,11 @@ const ASelectOption = ASelect.Option;
|
||||
const ASelectOptGroup = ASelect.OptGroup;
|
||||
const { Text: ATypographyText } = Typography;
|
||||
|
||||
withDefaults(
|
||||
defineProps<{ allowOther?: boolean; placeholder?: string }>(),
|
||||
{ allowOther: true, placeholder: 'مشتری، شرکت، کاربر یا سایر' },
|
||||
);
|
||||
|
||||
const kind = defineModel<FinancialPartyKind | undefined>('kind');
|
||||
const partyId = defineModel<string | undefined>('partyId');
|
||||
|
||||
@@ -156,12 +161,12 @@ onMounted(() => {
|
||||
:filter-option="false"
|
||||
:loading="loading"
|
||||
:value="selectValue"
|
||||
placeholder="مشتری، شرکت، کاربر یا سایر"
|
||||
:placeholder="placeholder"
|
||||
style="width: 100%"
|
||||
@search="onSearch"
|
||||
@change="onChange"
|
||||
>
|
||||
<a-select-option value="other" label="سایر">سایر</a-select-option>
|
||||
<a-select-option v-if="allowOther" value="other" label="سایر">سایر</a-select-option>
|
||||
<a-select-opt-group v-if="peopleOptions.length" label="مشتریان">
|
||||
<a-select-option
|
||||
v-for="row in peopleOptions"
|
||||
|
||||
@@ -16,12 +16,16 @@ import {
|
||||
Space as ASpace,
|
||||
Statistic as AStatistic,
|
||||
Table as ATable,
|
||||
Descriptions as ADescriptions,
|
||||
DescriptionsItem as ADescriptionsItem,
|
||||
List as AList,
|
||||
} from 'ant-design-vue';
|
||||
import type { TableColumnsType } from 'ant-design-vue';
|
||||
import { api } from '@/lib/api';
|
||||
import type { FinancialAccountDto, FinancialMovementDto } from '@mento/shared';
|
||||
import MoneyInput from '@/components/MoneyInput.vue';
|
||||
import { toman } from './treasury-format';
|
||||
import { useTreasuryMobile } from './useTreasuryMobile';
|
||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||
import { parseMoneyNumber } from '@/lib/money';
|
||||
import { useTreasuryPageChrome } from './treasuryPageChrome';
|
||||
@@ -37,6 +41,8 @@ const reason = ref('');
|
||||
const note = ref('');
|
||||
const busy = ref(false);
|
||||
|
||||
const isMobile = useTreasuryMobile();
|
||||
const AListItem = AList.Item;
|
||||
const columns: TableColumnsType<FinancialMovementDto> = [
|
||||
{
|
||||
title: 'تاریخ',
|
||||
@@ -190,8 +196,29 @@ onUnmounted(() => {
|
||||
|
||||
<a-divider orientation="right">گردش حساب</a-divider>
|
||||
<a-empty v-if="!items.length" description="گردشی ثبت نشده است." />
|
||||
<a-list v-else-if="isMobile" :data-source="items">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-card :title="formatJalaliNumeric(new Date(item.occurredAt))" style="width: 100%">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="شرح">{{ item.memo || '—' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="نوع">{{ item.kindLabel }}</a-descriptions-item>
|
||||
<a-descriptions-item label="ورودی">
|
||||
{{ item.direction === 'in' ? toman(item.amount) : '—' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="خروجی">
|
||||
{{ item.direction === 'out' ? toman(item.amount) : '—' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="مانده">
|
||||
{{ item.balanceAfter ? toman(item.balanceAfter) : '—' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-table
|
||||
v-else
|
||||
v-else-if="items.length"
|
||||
:columns="columns"
|
||||
:data-source="items"
|
||||
:pagination="false"
|
||||
|
||||
@@ -27,9 +27,11 @@ import type { FinancialAccountDto, FinancialAccountType, OrgUserDto } from '@men
|
||||
import MoneyInput from '@/components/MoneyInput.vue';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import { toman } from './treasury-format';
|
||||
import { useTreasuryMobile } from './useTreasuryMobile';
|
||||
import { parseMoneyNumber } from '@/lib/money';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const isMobile = useTreasuryMobile();
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
const ASelectOption = ASelect.Option;
|
||||
@@ -420,7 +422,7 @@ onMounted(() => {
|
||||
|
||||
<a-divider orientation="right">فهرست حسابها</a-divider>
|
||||
<a-empty v-if="!accounts.length" description="حسابی ثبت نشده است." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="accounts">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="accounts">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
|
||||
@@ -23,8 +23,10 @@ import {
|
||||
} from 'ant-design-vue';
|
||||
import { api } from '@/lib/api';
|
||||
import type { FinancialCategoryDto } from '@mento/shared';
|
||||
import { useTreasuryMobile } from './useTreasuryMobile';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const isMobile = useTreasuryMobile();
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
const ASelectOption = ASelect.Option;
|
||||
@@ -151,7 +153,7 @@ onMounted(() => {
|
||||
|
||||
<a-divider orientation="right">درآمد</a-divider>
|
||||
<a-empty v-if="!incomeRows.length" description="دسته درآمدی نیست." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="incomeRows">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="incomeRows">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
@@ -176,7 +178,7 @@ onMounted(() => {
|
||||
|
||||
<a-divider orientation="right">هزینه</a-divider>
|
||||
<a-empty v-if="!expenseRows.length" description="دسته هزینهای نیست." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="expenseRows">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="expenseRows">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
@@ -14,11 +14,13 @@ import {
|
||||
Input as AInput,
|
||||
List as AList,
|
||||
Row as ARow,
|
||||
Segmented as ASegmented,
|
||||
Select as ASelect,
|
||||
Typography,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatLocalDateInput } from '@/lib/jalali';
|
||||
import type {
|
||||
FinancialAccountDto,
|
||||
FinancialCategoryDto,
|
||||
@@ -26,18 +28,20 @@ import type {
|
||||
FinancialPartyKind,
|
||||
TreasuryPartyBalanceDto,
|
||||
} from '@mento/shared';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import MoneyInput from '@/components/MoneyInput.vue';
|
||||
import TreasuryPartyBalanceTable from '@/components/treasury/TreasuryPartyBalanceTable.vue';
|
||||
import TreasuryPartySelect from '@/components/treasury/TreasuryPartySelect.vue';
|
||||
import { useSettleDefault } from '@/components/treasury/useSettleDefault';
|
||||
import { partyNetPosition, toman } from './treasury-format';
|
||||
import { parseMoneyNumber } from '@/lib/money';
|
||||
import { useTreasuryMobile } from './useTreasuryMobile';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
const ASelectOption = ASelect.Option;
|
||||
|
||||
const isMobile = useTreasuryMobile();
|
||||
const kind = 'expense' as const;
|
||||
const rows = ref<FinancialDocumentDto[]>([]);
|
||||
const balances = ref<TreasuryPartyBalanceDto[]>([]);
|
||||
@@ -58,10 +62,17 @@ const form = ref({
|
||||
settleAmount: '',
|
||||
method: 'transfer',
|
||||
notes: '',
|
||||
bookedAt: formatLocalDateInput(new Date()),
|
||||
});
|
||||
const settleMirror = useSettleDefault(form);
|
||||
|
||||
const settleLabel = 'مبلغ پرداختشده';
|
||||
const entryMode = ref<'settlement' | 'accrual'>('settlement');
|
||||
const isSettlement = computed(() => entryMode.value === 'settlement');
|
||||
const entryOptions = [
|
||||
{ label: 'پرداخت', value: 'settlement' },
|
||||
{ label: 'هزینه', value: 'accrual' },
|
||||
];
|
||||
const statusLabel: Record<string, string> = {
|
||||
open: 'پرداختنشده',
|
||||
partial: 'جزئی',
|
||||
@@ -134,46 +145,61 @@ async function createDoc() {
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
try {
|
||||
const amount = parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
||||
const settlement = isSettlement.value;
|
||||
const settleAmount = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
const amount = settlement
|
||||
? settleAmount
|
||||
: parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
||||
if (amount == null || amount <= 0) {
|
||||
error.value = 'مبلغ کل معتبر نیست.';
|
||||
error.value = settlement ? 'مبلغ پرداختشده معتبر نیست.' : 'مبلغ کل معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
if (!form.value.partyKind || (form.value.partyKind !== 'other' && !form.value.partyId)) {
|
||||
error.value = 'طرف حساب را انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
const settleAmount = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
if (form.value.partyKind === 'other' && settleAmount !== amount) {
|
||||
if (settlement && form.value.partyKind === 'other') {
|
||||
error.value = 'برای پرداخت، طرف حساب واقعی لازم است.';
|
||||
return;
|
||||
}
|
||||
if (!settlement && form.value.partyKind === 'other' && settleAmount !== amount) {
|
||||
error.value = 'برای سایر، مبلغ پرداختشده باید با مبلغ کل یکی باشد.';
|
||||
return;
|
||||
}
|
||||
if (!settlement && settleAmount > amount) {
|
||||
error.value = 'مبلغ پرداختشده از مبلغ کل بیشتر است.';
|
||||
return;
|
||||
}
|
||||
if ((settlement || settleAmount > 0) && !form.value.accountId) {
|
||||
error.value = 'حساب را انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
const partyKey =
|
||||
form.value.partyKind && form.value.partyKind !== 'other' && form.value.partyId
|
||||
? `${form.value.partyKind}:${form.value.partyId}`
|
||||
: '';
|
||||
const openBalance = partyKey
|
||||
? Number(
|
||||
balances.value.find((row) => row.key === partyKey)?.documentRemaining ||
|
||||
balances.value.find((row) => row.key === partyKey)?.remaining ||
|
||||
0,
|
||||
)
|
||||
const payable = partyKey
|
||||
? Number(balances.value.find((row) => row.key === partyKey)?.documentRemaining || 0)
|
||||
: 0;
|
||||
const collectsBalance =
|
||||
Boolean(partyKey) && settleAmount === amount && settleAmount > 0 && openBalance > 0;
|
||||
const receivable = partyKey
|
||||
? Number(receivableBalances.value.find((row) => row.key === partyKey)?.documentRemaining || 0)
|
||||
: 0;
|
||||
const collectsBalance = settlement && (payable > 0 || receivable > 0);
|
||||
const { data } = await api.post<FinancialDocumentDto>('/treasury/documents', {
|
||||
kind,
|
||||
settlement,
|
||||
title: form.value.title,
|
||||
amount,
|
||||
categoryId: form.value.categoryId || null,
|
||||
categoryId: settlement ? null : form.value.categoryId || null,
|
||||
partyKind: form.value.partyKind,
|
||||
personId: form.value.partyKind === 'person' ? form.value.partyId : null,
|
||||
companyId: form.value.partyKind === 'company' ? form.value.partyId : null,
|
||||
userId: form.value.partyKind === 'user' ? form.value.partyId : null,
|
||||
accountId: form.value.accountId || null,
|
||||
settleAmount,
|
||||
settleAmount: settlement ? amount : settleAmount,
|
||||
method: form.value.method,
|
||||
notes: form.value.notes || null,
|
||||
bookedAt: form.value.bookedAt || undefined,
|
||||
});
|
||||
settleMirror.reset();
|
||||
form.value = {
|
||||
@@ -186,6 +212,7 @@ async function createDoc() {
|
||||
settleAmount: '',
|
||||
method: 'transfer',
|
||||
notes: '',
|
||||
bookedAt: formatLocalDateInput(new Date()),
|
||||
};
|
||||
ok.value = collectsBalance
|
||||
? `${toman(settleAmount)} از ماندهٔ طرف حساب کم شد.`
|
||||
@@ -204,6 +231,9 @@ const partyRules = [
|
||||
{
|
||||
validator: async () => {
|
||||
if (!form.value.partyKind) throw new Error('طرف حساب الزامی است');
|
||||
if (isSettlement.value && form.value.partyKind === 'other') {
|
||||
throw new Error('برای پرداخت، طرف حساب واقعی لازم است');
|
||||
}
|
||||
if (form.value.partyKind !== 'other' && !form.value.partyId) {
|
||||
throw new Error('طرف حساب را انتخاب کنید');
|
||||
}
|
||||
@@ -211,6 +241,33 @@ const partyRules = [
|
||||
},
|
||||
];
|
||||
|
||||
const accountRules = [
|
||||
{
|
||||
validator: async () => {
|
||||
const settled = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
if ((isSettlement.value || settled > 0) && !form.value.accountId) {
|
||||
throw new Error('حساب را انتخاب کنید');
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const remainingAmount = computed(() => {
|
||||
const total = parseMoneyNumber(String(form.value.amount ?? ''), 0) ?? 0;
|
||||
const settled = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
return Math.max(0, total - settled);
|
||||
});
|
||||
|
||||
watch(entryMode, () => {
|
||||
settleMirror.reset();
|
||||
form.value.amount = '';
|
||||
form.value.settleAmount = '';
|
||||
if (isSettlement.value && form.value.partyKind === 'other') {
|
||||
form.value.partyKind = undefined;
|
||||
form.value.partyId = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const partyPosition = computed(() => {
|
||||
const partyKind = form.value.partyKind;
|
||||
const id = form.value.partyId;
|
||||
@@ -244,7 +301,10 @@ onMounted(() => {
|
||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-divider orientation="right">سند جدید</a-divider>
|
||||
<a-flex align="center" justify="space-between" wrap="wrap" gap="small">
|
||||
<a-typography-text strong>سند جدید</a-typography-text>
|
||||
<a-segmented v-model:value="entryMode" :options="entryOptions" />
|
||||
</a-flex>
|
||||
<a-form
|
||||
layout="vertical"
|
||||
:model="form"
|
||||
@@ -256,7 +316,12 @@ onMounted(() => {
|
||||
<a-form-item label="طرف حساب" name="partyKind" :rules="partyRules">
|
||||
<a-row :gutter="8" align="middle">
|
||||
<a-col :span="partyPosition ? 16 : 24">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
<TreasuryPartySelect
|
||||
v-model:kind="form.partyKind"
|
||||
v-model:party-id="form.partyId"
|
||||
:allow-other="!isSettlement"
|
||||
:placeholder="isSettlement ? 'مشتری، شرکت یا کاربر' : undefined"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col v-if="partyPosition" :span="8">
|
||||
<a-typography-text :type="partyPosition.type">{{ partyPosition.text }}</a-typography-text>
|
||||
@@ -273,7 +338,7 @@ onMounted(() => {
|
||||
<a-input v-model:value="form.title" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-col v-if="!isSettlement" :xs="24" :md="8">
|
||||
<a-form-item
|
||||
label="مبلغ کل"
|
||||
name="amount"
|
||||
@@ -283,11 +348,25 @@ onMounted(() => {
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item :label="settleLabel" name="settleAmount">
|
||||
<MoneyInput addon-after="تومان" v-model="form.settleAmount" :decimals="0" :disabled="form.partyKind === 'other'" />
|
||||
<a-form-item
|
||||
:label="settleLabel"
|
||||
name="settleAmount"
|
||||
:rules="isSettlement ? [{ required: true, message: 'مبلغ را وارد کنید' }] : []"
|
||||
>
|
||||
<MoneyInput
|
||||
addon-after="تومان"
|
||||
v-model="form.settleAmount"
|
||||
:decimals="0"
|
||||
:disabled="!isSettlement && form.partyKind === 'other'"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-col v-if="!isSettlement" :xs="24" :md="8">
|
||||
<a-form-item label="مانده این سند">
|
||||
<a-typography-text>{{ toman(remainingAmount) }}</a-typography-text>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="!isSettlement" :xs="24" :md="8">
|
||||
<a-form-item label="دستهبندی" name="categoryId">
|
||||
<a-select show-search option-filter-prop="label"
|
||||
v-model:value="form.categoryId"
|
||||
@@ -302,7 +381,7 @@ onMounted(() => {
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="حساب" name="accountId">
|
||||
<a-form-item label="حساب" name="accountId" :rules="accountRules">
|
||||
<a-select show-search option-filter-prop="label"
|
||||
v-model:value="form.accountId"
|
||||
allow-clear
|
||||
@@ -324,8 +403,18 @@ onMounted(() => {
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="تاریخ" name="bookedAt">
|
||||
<JalaliDateInput v-model="form.bookedAt" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="!isSettlement" :span="24">
|
||||
<a-form-item label="توضیحات" name="notes">
|
||||
<a-input v-model:value="form.notes" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button type="primary" html-type="submit" :loading="saving">ثبت</a-button>
|
||||
<a-button type="primary" html-type="submit" :block="isMobile" :loading="saving">ثبت</a-button>
|
||||
</a-form>
|
||||
|
||||
<a-divider orientation="right">مانده پرداختنی</a-divider>
|
||||
@@ -349,10 +438,10 @@ onMounted(() => {
|
||||
</a-form>
|
||||
|
||||
<a-empty v-if="!rows.length" description="سندی نیست." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="rows">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="rows">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<template v-if="!isMobile" #actions>
|
||||
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
||||
<a-button
|
||||
v-if="item.status !== 'void'"
|
||||
@@ -363,7 +452,29 @@ onMounted(() => {
|
||||
ابطال
|
||||
</a-button>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<a-flex v-if="isMobile" vertical :gap="4" style="width: 100%">
|
||||
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
|
||||
<a-typography-text strong>{{ item.title }}</a-typography-text>
|
||||
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-typography-text type="secondary">
|
||||
{{ item.partyName || 'بدون طرف حساب' }} · {{ statusLabel[item.status] ?? item.status }}
|
||||
</a-typography-text>
|
||||
<a-typography-text v-if="item.reference" type="secondary">پیگیری {{ item.reference }}</a-typography-text>
|
||||
<a-typography-text v-if="item.categoryName" type="secondary">{{ item.categoryName }}</a-typography-text>
|
||||
<a-flex justify="space-between" align="center">
|
||||
<a-typography-text>مانده {{ toman(item.remaining) }}</a-typography-text>
|
||||
<a-button
|
||||
v-if="item.status !== 'void'"
|
||||
type="link"
|
||||
danger
|
||||
@click="voidDoc(item.id)"
|
||||
>
|
||||
ابطال
|
||||
</a-button>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
<a-list-item-meta v-else>
|
||||
<template #title>{{ item.title }}</template>
|
||||
<template #description>{{ docLine(item) }}</template>
|
||||
</a-list-item-meta>
|
||||
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
import { api } from '@/lib/api';
|
||||
import type { TreasuryDashboardDto } from '@mento/shared';
|
||||
import { toman } from './treasury-format';
|
||||
import { useTreasuryMobile } from './useTreasuryMobile';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const isMobile = useTreasuryMobile();
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
|
||||
@@ -75,7 +77,7 @@ onMounted(async () => {
|
||||
/>
|
||||
<a-list
|
||||
v-else
|
||||
item-layout="horizontal"
|
||||
:item-layout="isMobile ? 'vertical' : 'horizontal'"
|
||||
:data-source="data.upcomingInstallments"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
@@ -102,7 +104,7 @@ onMounted(async () => {
|
||||
/>
|
||||
<a-list
|
||||
v-else
|
||||
item-layout="horizontal"
|
||||
:item-layout="isMobile ? 'vertical' : 'horizontal'"
|
||||
:data-source="data.upcomingPayables"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
@@ -127,7 +129,7 @@ onMounted(async () => {
|
||||
/>
|
||||
<a-list
|
||||
v-else
|
||||
item-layout="horizontal"
|
||||
:item-layout="isMobile ? 'vertical' : 'horizontal'"
|
||||
:data-source="data.recent"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Avatar as AAvatar,
|
||||
Button as AButton,
|
||||
Card as ACard,
|
||||
Col as ACol,
|
||||
@@ -18,23 +17,9 @@ import {
|
||||
Row as ARow,
|
||||
Segmented as ASegmented,
|
||||
Select as ASelect,
|
||||
Space as ASpace,
|
||||
Textarea as ATextarea,
|
||||
Typography,
|
||||
message,
|
||||
theme,
|
||||
} from 'ant-design-vue';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import {
|
||||
ArrowDownOutlined,
|
||||
BankOutlined,
|
||||
CheckOutlined,
|
||||
ClockCircleOutlined,
|
||||
CreditCardOutlined,
|
||||
FileTextOutlined,
|
||||
InfoCircleOutlined,
|
||||
TagOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatLocalDateInput } from '@/lib/jalali';
|
||||
import type {
|
||||
@@ -42,7 +27,6 @@ import type {
|
||||
FinancialCategoryDto,
|
||||
FinancialDocumentDto,
|
||||
FinancialPartyKind,
|
||||
TreasuryDashboardDto,
|
||||
TreasuryPartyBalanceDto,
|
||||
} from '@mento/shared';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
@@ -57,7 +41,6 @@ const { Text: ATypographyText } = Typography;
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
const ASelectOption = ASelect.Option;
|
||||
const { token } = theme.useToken();
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = computed(() => !screens.value.md);
|
||||
|
||||
@@ -67,13 +50,10 @@ const balances = ref<TreasuryPartyBalanceDto[]>([]);
|
||||
const payableBalances = ref<TreasuryPartyBalanceDto[]>([]);
|
||||
const categories = ref<FinancialCategoryDto[]>([]);
|
||||
const accounts = ref<FinancialAccountDto[]>([]);
|
||||
const dashboard = ref<TreasuryDashboardDto | null>(null);
|
||||
const error = ref('');
|
||||
const ok = ref('');
|
||||
const saving = ref(false);
|
||||
const q = ref('');
|
||||
const formTab = ref<'main' | 'extra'>('main');
|
||||
const mobileFormRef = ref<FormInstance>();
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
@@ -94,6 +74,12 @@ const form = ref(emptyForm());
|
||||
const settleMirror = useSettleDefault(form);
|
||||
|
||||
const settleLabel = 'مبلغ دریافتشده';
|
||||
const entryMode = ref<'settlement' | 'accrual'>('settlement');
|
||||
const isSettlement = computed(() => entryMode.value === 'settlement');
|
||||
const entryOptions = [
|
||||
{ label: 'دریافت', value: 'settlement' },
|
||||
{ label: 'درآمد', value: 'accrual' },
|
||||
];
|
||||
const statusLabel: Record<string, string> = {
|
||||
open: 'دریافتنشده',
|
||||
partial: 'جزئی',
|
||||
@@ -109,23 +95,13 @@ const methodOptions = [
|
||||
{ value: 'other', label: 'سایر' },
|
||||
];
|
||||
|
||||
const formTabOptions = [
|
||||
{
|
||||
value: 'main',
|
||||
payload: 'main',
|
||||
label: h('span', {}, [h(InfoCircleOutlined), ' اطلاعات اصلی']),
|
||||
},
|
||||
{
|
||||
value: 'extra',
|
||||
payload: 'extra',
|
||||
label: h('span', {}, [h(FileTextOutlined), ' جزئیات تکمیلی']),
|
||||
},
|
||||
];
|
||||
|
||||
const partyRules = [
|
||||
{
|
||||
validator: async () => {
|
||||
if (!form.value.partyKind) throw new Error('طرف حساب الزامی است');
|
||||
if (isSettlement.value && form.value.partyKind === 'other') {
|
||||
throw new Error('برای دریافت، طرف حساب واقعی لازم است');
|
||||
}
|
||||
if (form.value.partyKind !== 'other' && !form.value.partyId) {
|
||||
throw new Error('طرف حساب را انتخاب کنید');
|
||||
}
|
||||
@@ -133,6 +109,17 @@ const partyRules = [
|
||||
},
|
||||
];
|
||||
|
||||
const accountRules = [
|
||||
{
|
||||
validator: async () => {
|
||||
const settled = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
if ((isSettlement.value || settled > 0) && !form.value.accountId) {
|
||||
throw new Error('حساب را انتخاب کنید');
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const partyPosition = computed(() => {
|
||||
const partyKind = form.value.partyKind;
|
||||
const id = form.value.partyId;
|
||||
@@ -143,6 +130,16 @@ const partyPosition = computed(() => {
|
||||
return partyNetPosition(receivable, payable);
|
||||
});
|
||||
|
||||
watch(entryMode, () => {
|
||||
settleMirror.reset();
|
||||
form.value.amount = '';
|
||||
form.value.settleAmount = '';
|
||||
if (isSettlement.value && form.value.partyKind === 'other') {
|
||||
form.value.partyKind = undefined;
|
||||
form.value.partyId = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const remainingAmount = computed(() => {
|
||||
const total = parseMoneyNumber(String(form.value.amount ?? ''), 0) ?? 0;
|
||||
const settled = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
@@ -171,7 +168,7 @@ function docLine(item: FinancialDocumentDto) {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const [docs, cats, acc, dash, bals, payables] = await Promise.all([
|
||||
const [docs, cats, acc, bals, payables] = await Promise.all([
|
||||
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
|
||||
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
|
||||
@@ -185,7 +182,6 @@ async function load() {
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
api.get<TreasuryDashboardDto>('/treasury/dashboard').catch(() => null),
|
||||
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind } }).catch((e: unknown) => {
|
||||
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
|
||||
throw e;
|
||||
@@ -200,13 +196,11 @@ async function load() {
|
||||
payableBalances.value = payables.data;
|
||||
categories.value = cats.data.filter((row) => row.isActive);
|
||||
accounts.value = acc.data.filter((row) => row.isActive);
|
||||
if (dash) dashboard.value = dash.data;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
settleMirror.reset();
|
||||
form.value = emptyForm();
|
||||
formTab.value = 'main';
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
}
|
||||
@@ -216,44 +210,58 @@ async function createDoc() {
|
||||
error.value = '';
|
||||
ok.value = '';
|
||||
try {
|
||||
const amount = parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
||||
const settlement = isSettlement.value;
|
||||
const settleAmount = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
const amount = settlement
|
||||
? settleAmount
|
||||
: parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
||||
if (amount == null || amount <= 0) {
|
||||
error.value = 'مبلغ کل معتبر نیست.';
|
||||
error.value = settlement ? 'مبلغ دریافتشده معتبر نیست.' : 'مبلغ کل معتبر نیست.';
|
||||
return;
|
||||
}
|
||||
if (!form.value.partyKind || (form.value.partyKind !== 'other' && !form.value.partyId)) {
|
||||
error.value = 'طرف حساب را انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
const settleAmount = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||
if (form.value.partyKind === 'other' && settleAmount !== amount) {
|
||||
if (settlement && form.value.partyKind === 'other') {
|
||||
error.value = 'برای دریافت، طرف حساب واقعی لازم است.';
|
||||
return;
|
||||
}
|
||||
if (!settlement && form.value.partyKind === 'other' && settleAmount !== amount) {
|
||||
error.value = 'برای سایر، مبلغ دریافتشده باید با مبلغ کل یکی باشد.';
|
||||
return;
|
||||
}
|
||||
if (!settlement && settleAmount > amount) {
|
||||
error.value = 'مبلغ دریافتشده از مبلغ کل بیشتر است.';
|
||||
return;
|
||||
}
|
||||
if ((settlement || settleAmount > 0) && !form.value.accountId) {
|
||||
error.value = 'حساب را انتخاب کنید.';
|
||||
return;
|
||||
}
|
||||
const partyKey =
|
||||
form.value.partyKind && form.value.partyKind !== 'other' && form.value.partyId
|
||||
? `${form.value.partyKind}:${form.value.partyId}`
|
||||
: '';
|
||||
const openBalance = partyKey
|
||||
? Number(
|
||||
balances.value.find((row) => row.key === partyKey)?.documentRemaining ||
|
||||
balances.value.find((row) => row.key === partyKey)?.remaining ||
|
||||
0,
|
||||
)
|
||||
const receivable = partyKey
|
||||
? Number(balances.value.find((row) => row.key === partyKey)?.documentRemaining || 0)
|
||||
: 0;
|
||||
const collectsBalance =
|
||||
Boolean(partyKey) && settleAmount === amount && settleAmount > 0 && openBalance > 0;
|
||||
const payable = partyKey
|
||||
? Number(payableBalances.value.find((row) => row.key === partyKey)?.documentRemaining || 0)
|
||||
: 0;
|
||||
const collectsBalance = settlement && (receivable > 0 || payable > 0);
|
||||
const { data } = await api.post<FinancialDocumentDto>('/treasury/documents', {
|
||||
kind,
|
||||
settlement,
|
||||
title: form.value.title,
|
||||
amount,
|
||||
categoryId: form.value.categoryId || null,
|
||||
categoryId: settlement ? null : form.value.categoryId || null,
|
||||
partyKind: form.value.partyKind,
|
||||
personId: form.value.partyKind === 'person' ? form.value.partyId : null,
|
||||
companyId: form.value.partyKind === 'company' ? form.value.partyId : null,
|
||||
userId: form.value.partyKind === 'user' ? form.value.partyId : null,
|
||||
accountId: form.value.accountId || null,
|
||||
settleAmount,
|
||||
settleAmount: settlement ? amount : settleAmount,
|
||||
method: form.value.method,
|
||||
notes: form.value.notes || null,
|
||||
bookedAt: form.value.bookedAt || undefined,
|
||||
@@ -272,16 +280,6 @@ async function createDoc() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitMobileForm() {
|
||||
try {
|
||||
await mobileFormRef.value?.validate();
|
||||
} catch {
|
||||
formTab.value = 'main';
|
||||
return;
|
||||
}
|
||||
await createDoc();
|
||||
}
|
||||
|
||||
async function voidDoc(id: string) {
|
||||
if (!window.confirm('این سند و گردش متصل به آن باطل شود؟')) return;
|
||||
try {
|
||||
@@ -300,345 +298,15 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="
|
||||
isMobile
|
||||
? { paddingBottom: 'calc(96px + var(--mobile-tabbar-h, 3.65rem))' }
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<!-- ─── Mobile app experience ─── -->
|
||||
<template v-if="isMobile">
|
||||
<a-flex vertical :gap="token.margin">
|
||||
<a-row :gutter="[12, 12]">
|
||||
<a-col :span="12">
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:body-style="{ padding: `${token.paddingSM}px` }"
|
||||
:style="{
|
||||
background: token.colorPrimaryBg,
|
||||
borderRadius: `${token.borderRadiusLG}px`,
|
||||
boxShadow: token.boxShadow,
|
||||
}"
|
||||
>
|
||||
<a-flex align="center" justify="space-between" :gap="8">
|
||||
<div :style="{ minWidth: 0, flex: 1 }">
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
دریافتهای این ماه
|
||||
</a-typography-text>
|
||||
<div>
|
||||
<a-typography-text
|
||||
strong
|
||||
:style="{ fontSize: '15px', color: token.colorPrimary }"
|
||||
>
|
||||
{{ toman(dashboard?.actualReceipts) }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
<a-avatar
|
||||
:size="36"
|
||||
:style="{
|
||||
background: token.colorPrimary,
|
||||
color: token.colorTextLightSolid,
|
||||
flexShrink: 0,
|
||||
}"
|
||||
>
|
||||
<template #icon><ArrowDownOutlined /></template>
|
||||
</a-avatar>
|
||||
</a-flex>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card
|
||||
:bordered="false"
|
||||
:body-style="{ padding: `${token.paddingSM}px` }"
|
||||
:style="{
|
||||
background: token.colorPrimaryBg,
|
||||
borderRadius: `${token.borderRadiusLG}px`,
|
||||
boxShadow: token.boxShadow,
|
||||
}"
|
||||
>
|
||||
<a-flex align="center" justify="space-between" :gap="8">
|
||||
<div :style="{ minWidth: 0, flex: 1 }">
|
||||
<a-typography-text type="secondary" :style="{ fontSize: '12px' }">
|
||||
در انتظار دریافت
|
||||
</a-typography-text>
|
||||
<div>
|
||||
<a-typography-text
|
||||
strong
|
||||
:style="{ fontSize: '15px', color: token.colorPrimary }"
|
||||
>
|
||||
{{ toman(dashboard?.receivables) }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
</div>
|
||||
<a-avatar
|
||||
:size="36"
|
||||
:style="{
|
||||
background: token.colorPrimary,
|
||||
color: token.colorTextLightSolid,
|
||||
flexShrink: 0,
|
||||
}"
|
||||
>
|
||||
<template #icon><ClockCircleOutlined /></template>
|
||||
</a-avatar>
|
||||
</a-flex>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-alert v-if="error" type="error" show-icon :message="error" closable @close="error = ''" />
|
||||
<a-alert v-if="ok" type="success" show-icon :message="ok" closable @close="ok = ''" />
|
||||
|
||||
<a-segmented
|
||||
v-model:value="formTab"
|
||||
block
|
||||
:options="formTabOptions"
|
||||
:style="{ background: token.colorFillTertiary }"
|
||||
/>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-form ref="mobileFormRef" layout="vertical" :model="form" @finish="createDoc">
|
||||
<div v-show="formTab === 'main'">
|
||||
<a-form-item label="طرف حساب" name="partyKind" :rules="partyRules">
|
||||
<a-row :gutter="8" align="middle">
|
||||
<a-col :span="partyPosition ? 16 : 24">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
</a-col>
|
||||
<a-col v-if="partyPosition" :span="8">
|
||||
<a-typography-text :type="partyPosition.type">{{ partyPosition.text }}</a-typography-text>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
label="عنوان درآمد"
|
||||
name="title"
|
||||
:rules="[{ required: true, message: 'عنوان را وارد کنید' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.title"
|
||||
placeholder="مثلاً: دریافت قسط پروژه الف"
|
||||
>
|
||||
<template #prefix><FileTextOutlined /></template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="[12, 0]">
|
||||
<a-col :span="24">
|
||||
<a-form-item name="method">
|
||||
<template #label>
|
||||
<a-space :size="6">
|
||||
<CreditCardOutlined />
|
||||
<span>روش دریافت</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-select show-search option-filter-prop="label" v-model:value="form.method" style="width: 100%">
|
||||
<a-select-option
|
||||
v-for="opt in methodOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
:label="opt.label"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item name="categoryId">
|
||||
<template #label>
|
||||
<a-space :size="6">
|
||||
<TagOutlined />
|
||||
<span>دستهبندی درآمد</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-select show-search option-filter-prop="label"
|
||||
v-model:value="form.categoryId"
|
||||
allow-clear
|
||||
placeholder="انتخاب دسته"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option v-for="cat in categories" :key="cat.id" :value="cat.id" :label="cat.name">
|
||||
{{ cat.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="[12, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="مبلغ کل"
|
||||
name="amount"
|
||||
:rules="[{ required: true, message: 'مبلغ کل را وارد کنید' }]"
|
||||
>
|
||||
<MoneyInput addon-after="تومان" v-model="form.amount" :decimals="0" placeholder="۰" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="settleLabel"
|
||||
name="settleAmount"
|
||||
:rules="[{ required: true, message: 'مبلغ دریافتشده را وارد کنید' }]"
|
||||
>
|
||||
<MoneyInput addon-after="تومان"
|
||||
v-model="form.settleAmount"
|
||||
:decimals="0"
|
||||
:disabled="form.partyKind === 'other'"
|
||||
placeholder="۰"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
:style="{
|
||||
background: token.colorSuccessBg,
|
||||
borderRadius: `${token.borderRadiusLG * 2}px`,
|
||||
padding: `${token.paddingSM}px ${token.padding}px`,
|
||||
marginBottom: `${token.margin}px`,
|
||||
}"
|
||||
>
|
||||
<a-space :size="8">
|
||||
<span
|
||||
:style="{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
background: token.colorSuccess,
|
||||
display: 'inline-block',
|
||||
}"
|
||||
/>
|
||||
<a-typography-text type="secondary">مانده قابل دریافت</a-typography-text>
|
||||
</a-space>
|
||||
<a-typography-text strong :style="{ color: token.colorSuccess }">
|
||||
{{ toman(remainingAmount) }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
|
||||
<a-row :gutter="[12, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="تاریخ دریافت" name="bookedAt">
|
||||
<JalaliDateInput v-model="form.bookedAt" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item name="accountId">
|
||||
<template #label>
|
||||
<a-space :size="6">
|
||||
<BankOutlined />
|
||||
<span>حساب مقصد</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-select show-search option-filter-prop="label"
|
||||
v-model:value="form.accountId"
|
||||
allow-clear
|
||||
placeholder="انتخاب حساب"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="account in accounts"
|
||||
:key="account.id"
|
||||
:value="account.id"
|
||||
:label="account.name"
|
||||
>
|
||||
{{ account.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<div v-show="formTab === 'extra'">
|
||||
<a-form-item label="توضیحات" name="notes">
|
||||
<a-textarea
|
||||
v-model:value="form.notes"
|
||||
:rows="4"
|
||||
placeholder="توضیحات اختیاری…"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-divider orientation="right">مانده دریافتنی</a-divider>
|
||||
<TreasuryPartyBalanceTable :rows="balances" settled-label="دریافتشده" />
|
||||
<a-divider orientation="right">فهرست اسناد</a-divider>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="جستجو">
|
||||
<a-input
|
||||
v-model:value="q"
|
||||
allow-clear
|
||||
placeholder="عنوان یا شماره پیگیری"
|
||||
@press-enter="load"
|
||||
@change="load"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-empty v-if="!rows.length" description="سندی نیست." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="rows">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
||||
<a-button
|
||||
v-if="item.status !== 'void'"
|
||||
type="link"
|
||||
danger
|
||||
@click="voidDoc(item.id)"
|
||||
>
|
||||
ابطال
|
||||
</a-button>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<template #title>{{ item.title }}</template>
|
||||
<template #description>{{ docLine(item) }}</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-flex>
|
||||
|
||||
<div
|
||||
:style="{
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 'var(--mobile-tabbar-h, 3.65rem)',
|
||||
zIndex: 90,
|
||||
background: token.colorBgElevated,
|
||||
borderTop: `1px solid ${token.colorBorderSecondary}`,
|
||||
padding: `${token.paddingSM}px ${token.padding}px`,
|
||||
boxShadow: token.boxShadowSecondary,
|
||||
}"
|
||||
>
|
||||
<a-row :gutter="12">
|
||||
<a-col :span="8">
|
||||
<a-button block @click="resetForm">انصراف</a-button>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-button type="primary" block :loading="saving" @click="submitMobileForm">
|
||||
<template #icon><CheckOutlined /></template>
|
||||
ثبت دریافت
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ─── Desktop (unchanged interaction model) ─── -->
|
||||
<template v-else>
|
||||
<a-flex vertical :gap="16">
|
||||
<a-flex vertical :gap="16">
|
||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-divider orientation="right">سند جدید</a-divider>
|
||||
<a-flex align="center" justify="space-between" wrap="wrap" gap="small">
|
||||
<a-typography-text strong>سند جدید</a-typography-text>
|
||||
<a-segmented v-model:value="entryMode" :options="entryOptions" />
|
||||
</a-flex>
|
||||
<a-form
|
||||
layout="vertical"
|
||||
:model="form"
|
||||
@@ -650,7 +318,12 @@ onMounted(() => {
|
||||
<a-form-item label="طرف حساب" name="partyKind" :rules="partyRules">
|
||||
<a-row :gutter="8" align="middle">
|
||||
<a-col :span="partyPosition ? 16 : 24">
|
||||
<TreasuryPartySelect v-model:kind="form.partyKind" v-model:party-id="form.partyId" />
|
||||
<TreasuryPartySelect
|
||||
v-model:kind="form.partyKind"
|
||||
v-model:party-id="form.partyId"
|
||||
:allow-other="!isSettlement"
|
||||
:placeholder="isSettlement ? 'مشتری، شرکت یا کاربر' : undefined"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col v-if="partyPosition" :span="8">
|
||||
<a-typography-text :type="partyPosition.type">{{ partyPosition.text }}</a-typography-text>
|
||||
@@ -667,7 +340,7 @@ onMounted(() => {
|
||||
<a-input v-model:value="form.title" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-col v-if="!isSettlement" :xs="24" :md="8">
|
||||
<a-form-item
|
||||
label="مبلغ کل"
|
||||
name="amount"
|
||||
@@ -677,11 +350,25 @@ onMounted(() => {
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item :label="settleLabel" name="settleAmount">
|
||||
<MoneyInput addon-after="تومان" v-model="form.settleAmount" :decimals="0" :disabled="form.partyKind === 'other'" />
|
||||
<a-form-item
|
||||
:label="settleLabel"
|
||||
name="settleAmount"
|
||||
:rules="isSettlement ? [{ required: true, message: 'مبلغ را وارد کنید' }] : []"
|
||||
>
|
||||
<MoneyInput
|
||||
addon-after="تومان"
|
||||
v-model="form.settleAmount"
|
||||
:decimals="0"
|
||||
:disabled="!isSettlement && form.partyKind === 'other'"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-col v-if="!isSettlement" :xs="24" :md="8">
|
||||
<a-form-item label="مانده این سند">
|
||||
<a-typography-text>{{ toman(remainingAmount) }}</a-typography-text>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="!isSettlement" :xs="24" :md="8">
|
||||
<a-form-item label="دستهبندی" name="categoryId">
|
||||
<a-select show-search option-filter-prop="label"
|
||||
v-model:value="form.categoryId"
|
||||
@@ -696,7 +383,7 @@ onMounted(() => {
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item label="حساب" name="accountId">
|
||||
<a-form-item label="حساب" name="accountId" :rules="accountRules">
|
||||
<a-select show-search option-filter-prop="label"
|
||||
v-model:value="form.accountId"
|
||||
allow-clear
|
||||
@@ -733,13 +420,13 @@ onMounted(() => {
|
||||
<JalaliDateInput v-model="form.bookedAt" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="16">
|
||||
<a-col v-if="!isSettlement" :xs="24" :md="16">
|
||||
<a-form-item label="توضیحات" name="notes">
|
||||
<a-input v-model:value="form.notes" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button type="primary" html-type="submit" :loading="saving">ثبت</a-button>
|
||||
<a-button type="primary" html-type="submit" :block="isMobile" :loading="saving">ثبت</a-button>
|
||||
</a-form>
|
||||
|
||||
<a-divider orientation="right">مانده دریافتنی</a-divider>
|
||||
@@ -762,10 +449,10 @@ onMounted(() => {
|
||||
</a-form>
|
||||
|
||||
<a-empty v-if="!rows.length" description="سندی نیست." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="rows">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="rows">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
<template v-if="!isMobile" #actions>
|
||||
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
||||
<a-button
|
||||
v-if="item.status !== 'void'"
|
||||
@@ -776,7 +463,29 @@ onMounted(() => {
|
||||
ابطال
|
||||
</a-button>
|
||||
</template>
|
||||
<a-list-item-meta>
|
||||
<a-flex v-if="isMobile" vertical :gap="4" style="width: 100%">
|
||||
<a-flex justify="space-between" align="center" wrap="wrap" :gap="8">
|
||||
<a-typography-text strong>{{ item.title }}</a-typography-text>
|
||||
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-typography-text type="secondary">
|
||||
{{ item.partyName || 'بدون طرف حساب' }} · {{ statusLabel[item.status] ?? item.status }}
|
||||
</a-typography-text>
|
||||
<a-typography-text v-if="item.reference" type="secondary">پیگیری {{ item.reference }}</a-typography-text>
|
||||
<a-typography-text v-if="item.categoryName" type="secondary">{{ item.categoryName }}</a-typography-text>
|
||||
<a-flex justify="space-between" align="center">
|
||||
<a-typography-text>مانده {{ toman(item.remaining) }}</a-typography-text>
|
||||
<a-button
|
||||
v-if="item.status !== 'void'"
|
||||
type="link"
|
||||
danger
|
||||
@click="voidDoc(item.id)"
|
||||
>
|
||||
ابطال
|
||||
</a-button>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
<a-list-item-meta v-else>
|
||||
<template #title>{{ item.title }}</template>
|
||||
<template #description>{{ docLine(item) }}</template>
|
||||
</a-list-item-meta>
|
||||
@@ -784,8 +493,5 @@ onMounted(() => {
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-flex>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</a-flex>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import { RouterView, useRoute } from 'vue-router';
|
||||
import { Flex as AFlex, Typography, theme } from 'ant-design-vue';
|
||||
import { Flex as AFlex, Grid, Typography, theme } from 'ant-design-vue';
|
||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
||||
import { provideTreasuryPageChrome } from './treasuryPageChrome';
|
||||
|
||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
||||
const { token } = theme.useToken();
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = computed(() => !screens.value.md);
|
||||
const route = useRoute();
|
||||
const chrome = provideTreasuryPageChrome();
|
||||
|
||||
@@ -31,16 +33,19 @@ const description = computed(
|
||||
(typeof route.meta.treasuryDescription === 'string' ? route.meta.treasuryDescription : ''),
|
||||
);
|
||||
|
||||
/** Keep title+subtitle block height stable across tabs (one-line subtitle). */
|
||||
const headerStyle = computed(() => ({
|
||||
minHeight: `${token.value.controlHeightLG + token.value.fontSizeLG + token.value.marginXXS}px`,
|
||||
}));
|
||||
const headerStyle = computed(() =>
|
||||
isMobile.value
|
||||
? undefined
|
||||
: {
|
||||
minHeight: `${token.value.controlHeightLG + token.value.fontSizeLG + token.value.marginXXS}px`,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-flex vertical :gap="token.margin" :style="{ width: '100%' }">
|
||||
<a-flex vertical :gap="isMobile ? token.marginSM : token.margin" :style="{ width: '100%' }">
|
||||
<div :style="headerStyle">
|
||||
<a-typography-title :level="3" :style="{ margin: 0 }">{{ title }}</a-typography-title>
|
||||
<a-typography-title :level="isMobile ? 4 : 3" :style="{ margin: 0 }">{{ title }}</a-typography-title>
|
||||
<a-typography-text
|
||||
type="secondary"
|
||||
:ellipsis="description ? { tooltip: description } : false"
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Select as ASelect,
|
||||
Space as ASpace,
|
||||
Table as ATable,
|
||||
List as AList,
|
||||
Textarea as ATextarea,
|
||||
Tooltip as ATooltip,
|
||||
Typography,
|
||||
@@ -49,6 +50,7 @@ const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = computed(() => !screens.value.md);
|
||||
const AListItem = AList.Item;
|
||||
|
||||
type PayrollPayment = {
|
||||
id: string;
|
||||
@@ -651,8 +653,24 @@ onMounted(() => {
|
||||
</a-space>
|
||||
</a-form>
|
||||
|
||||
<a-list v-if="isMobile && selectedPay?.payments.length" :data-source="selectedPay.payments">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-card style="width: 100%">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="تاریخ">
|
||||
{{ formatJalaliNumeric(new Date(item.occurredAt)) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="حساب">{{ item.accountName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="مبلغ">{{ toman(item.amount) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-button type="link" @click="startEditPayment(item)">ویرایش</a-button>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-table
|
||||
v-if="selectedPay?.payments.length"
|
||||
v-else-if="selectedPay?.payments.length"
|
||||
:columns="paymentColumns"
|
||||
:data-source="selectedPay.payments"
|
||||
:pagination="false"
|
||||
|
||||
@@ -7,12 +7,15 @@ import {
|
||||
Card as ACard,
|
||||
Checkbox as ACheckbox,
|
||||
Col as ACol,
|
||||
Descriptions as ADescriptions,
|
||||
DescriptionsItem as ADescriptionsItem,
|
||||
Divider as ADivider,
|
||||
Empty as AEmpty,
|
||||
Flex as AFlex,
|
||||
Form as AForm,
|
||||
FormItem as AFormItem,
|
||||
Input as AInput,
|
||||
List as AList,
|
||||
Row as ARow,
|
||||
Select as ASelect,
|
||||
Space as ASpace,
|
||||
@@ -27,6 +30,7 @@ import type { FinancialAccountDto, FinancialReceivableDto } from '@mento/shared'
|
||||
import MoneyInput from '@/components/MoneyInput.vue';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import { toman } from './treasury-format';
|
||||
import { useTreasuryMobile } from './useTreasuryMobile';
|
||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||
import { parseMoneyNumber } from '@/lib/money';
|
||||
|
||||
@@ -87,6 +91,9 @@ const installmentColumns: TableColumnsType<InstallmentRow> = [
|
||||
{ title: '', key: 'actions' },
|
||||
];
|
||||
|
||||
const isMobile = useTreasuryMobile();
|
||||
const AListItem = AList.Item;
|
||||
|
||||
function apiMessage(e: unknown) {
|
||||
if (!axios.isAxiosError(e)) return 'عملیات ناموفق بود.';
|
||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||
@@ -365,7 +372,33 @@ onMounted(() => {
|
||||
</div>
|
||||
<a-typography-text strong>{{ toman(row.totalAmount) }}</a-typography-text>
|
||||
</a-flex>
|
||||
<a-list v-if="isMobile" :data-source="row.installments">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-card :title="`قسط ${item.position}`" style="width: 100%">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="سررسید">
|
||||
{{ formatJalaliNumeric(new Date(item.dueDate)) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="مبلغ">{{ toman(item.amount) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="مانده">{{ toman(item.remaining) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="وضعیت">
|
||||
{{ statusLabel[item.status] ?? item.status }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-button
|
||||
v-if="item.status !== 'settled'"
|
||||
type="link"
|
||||
@click="startCollect(row, item)"
|
||||
>
|
||||
دریافت
|
||||
</a-button>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-table
|
||||
v-else
|
||||
:columns="installmentColumns"
|
||||
:data-source="row.installments"
|
||||
:pagination="false"
|
||||
|
||||
@@ -22,10 +22,12 @@ import { api } from '@/lib/api';
|
||||
import MoneyInput from '@/components/MoneyInput.vue';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import { toman } from './treasury-format';
|
||||
import { useTreasuryMobile } from './useTreasuryMobile';
|
||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||
import { parseMoneyNumber } from '@/lib/money';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const isMobile = useTreasuryMobile();
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
const ASelectOption = ASelect.Option;
|
||||
@@ -146,7 +148,7 @@ onMounted(() => {
|
||||
|
||||
<a-divider orientation="right">فهرست الگوها</a-divider>
|
||||
<a-empty v-if="!rows.length" description="الگویی ثبت نشده است." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="rows">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="rows">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Empty as AEmpty,
|
||||
Flex as AFlex,
|
||||
List as AList,
|
||||
Descriptions as ADescriptions,
|
||||
DescriptionsItem as ADescriptionsItem,
|
||||
Table as ATable,
|
||||
Tabs as ATabs,
|
||||
Typography,
|
||||
@@ -20,8 +22,10 @@ import type { TreasuryPartyBalanceDto } from '@mento/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import TreasuryPartyBalanceTable from '@/components/treasury/TreasuryPartyBalanceTable.vue';
|
||||
import { toman } from './treasury-format';
|
||||
import { useTreasuryMobile } from './useTreasuryMobile';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const isMobile = useTreasuryMobile();
|
||||
const AListItem = AList.Item;
|
||||
const AListItemMeta = AList.Item.Meta;
|
||||
const ATabPane = ATabs.TabPane;
|
||||
@@ -186,8 +190,20 @@ onMounted(() => {
|
||||
<a-tabs v-model:activeKey="reportTab">
|
||||
<a-tab-pane key="periods" tab="مقایسه دورهها">
|
||||
<a-flex vertical :gap="16">
|
||||
<a-list v-if="isMobile && metricRows.length" :data-source="metricRows">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-card :title="item.label" style="width: 100%">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="این دوره">{{ item.current }}</a-descriptions-item>
|
||||
<a-descriptions-item label="دوره قبل">{{ item.previous }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-table
|
||||
v-if="metricRows.length"
|
||||
v-else-if="metricRows.length"
|
||||
:columns="metricColumns"
|
||||
:data-source="metricRows"
|
||||
:pagination="false"
|
||||
@@ -197,7 +213,7 @@ onMounted(() => {
|
||||
|
||||
<a-divider orientation="right">مطالبات به تفکیک طرف حساب</a-divider>
|
||||
<a-empty v-if="!receivablesByParty.length" description="مورد بازی نیست." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="receivablesByParty">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="receivablesByParty">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
@@ -212,7 +228,7 @@ onMounted(() => {
|
||||
|
||||
<a-divider orientation="right">تعهدات به تفکیک ماه</a-divider>
|
||||
<a-empty v-if="!payablesByMonth.length" description="تعهد بازی نیست." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="payablesByMonth">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="payablesByMonth">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
@@ -228,7 +244,7 @@ onMounted(() => {
|
||||
<template v-if="current">
|
||||
<a-divider orientation="right">سهم هزینهها</a-divider>
|
||||
<a-empty v-if="!current.expenseCategories.length" description="دستهای نیست." />
|
||||
<a-list v-else item-layout="horizontal" :data-source="current.expenseCategories">
|
||||
<a-list v-else :item-layout="isMobile ? 'vertical' : 'horizontal'" :data-source="current.expenseCategories">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<template #actions>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { computed } from 'vue';
|
||||
import { Grid } from 'ant-design-vue';
|
||||
|
||||
export function useTreasuryMobile() {
|
||||
const screens = Grid.useBreakpoint();
|
||||
return computed(() => !screens.value.md);
|
||||
}
|
||||
Reference in New Issue
Block a user