fix(finance): shared treasury layout so tab switches stop jumping
Keep title and nav mounted in TreasuryLayout with stable header height, and finish Ant form chrome (MoneyInput, Jalali) on finance pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,66 +1,68 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { Segmented as ASegmented } from 'ant-design-vue';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const canPayroll = computed(() =>
|
|
||||||
auth.user?.role === 'owner'
|
const canPayroll = computed(
|
||||||
|| auth.user?.role === 'admin'
|
() =>
|
||||||
|| Boolean(auth.user?.permissions?.includes('treasury_payroll')),
|
auth.user?.role === 'owner' ||
|
||||||
|
auth.user?.role === 'admin' ||
|
||||||
|
Boolean(auth.user?.permissions?.includes('treasury_payroll')),
|
||||||
);
|
);
|
||||||
|
|
||||||
const links = computed(() => [
|
const links = computed(() => [
|
||||||
{ to: '/finance', label: 'داشبورد', exact: true },
|
{ value: '/finance', label: 'داشبورد', exact: true },
|
||||||
{ to: '/finance/incomes', label: 'دریافتها و درآمدها' },
|
{ value: '/finance/incomes', label: 'دریافتها و درآمدها' },
|
||||||
{ to: '/finance/expenses', label: 'پرداختها و هزینهها' },
|
{ value: '/finance/expenses', label: 'پرداختها و هزینهها' },
|
||||||
{ to: '/finance/receivables', label: 'اقساط و مطالبات' },
|
{ value: '/finance/receivables', label: 'اقساط و مطالبات' },
|
||||||
{ to: '/finance/accounts', label: 'صندوق و حسابها' },
|
{ value: '/finance/accounts', label: 'صندوق و حسابها' },
|
||||||
...(canPayroll.value ? [{ to: '/finance/payroll', label: 'حقوق' }] : []),
|
...(canPayroll.value ? [{ value: '/finance/payroll', label: 'حقوق' }] : []),
|
||||||
{ to: '/finance/recurring', label: 'تکرارشونده' },
|
{ value: '/finance/recurring', label: 'تکرارشونده' },
|
||||||
{ to: '/finance/reports', label: 'گزارش' },
|
{ value: '/finance/reports', label: 'گزارش' },
|
||||||
{ to: '/finance/audit', label: 'سوابق' },
|
{ value: '/finance/audit', label: 'سوابق' },
|
||||||
{ to: '/finance/categories', label: 'دستهبندیها' },
|
{ value: '/finance/categories', label: 'دستهبندیها' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function on(path: string, exact?: boolean) {
|
const options = computed(() => links.value.map((l) => ({ value: l.value, label: l.label })));
|
||||||
if (exact) return route.path === path;
|
|
||||||
return route.path === path || route.path.startsWith(`${path}/`);
|
const active = computed(() => {
|
||||||
|
const path = route.path;
|
||||||
|
for (const link of links.value) {
|
||||||
|
if (link.exact) {
|
||||||
|
if (path === link.value) return link.value;
|
||||||
|
} else if (path === link.value || path.startsWith(`${link.value}/`)) {
|
||||||
|
return link.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '/finance';
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChange(value: string | number) {
|
||||||
|
const next = String(value);
|
||||||
|
if (next && next !== route.path) void router.push(next);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="treasury-nav" aria-label="مدیریت مالی">
|
<div
|
||||||
<RouterLink
|
:style="{
|
||||||
v-for="link in links"
|
width: '100%',
|
||||||
:key="link.to"
|
overflowX: 'auto',
|
||||||
:to="link.to"
|
flexShrink: 0,
|
||||||
:class="{ on: on(link.to, link.exact) }"
|
minHeight: '36px',
|
||||||
>
|
}"
|
||||||
{{ link.label }}
|
>
|
||||||
</RouterLink>
|
<a-segmented
|
||||||
</nav>
|
:value="active"
|
||||||
|
:options="options"
|
||||||
|
dir="rtl"
|
||||||
|
:style="{ minWidth: 'max-content' }"
|
||||||
|
@change="onChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.treasury-nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.35rem;
|
|
||||||
overflow: auto;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
.treasury-nav a {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
color: #475569;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.86rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
.treasury-nav a.on {
|
|
||||||
background: #0f766e;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
+117
-62
@@ -241,69 +241,124 @@ export const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'finance',
|
path: 'finance',
|
||||||
name: 'finance',
|
component: () => import('@/views/treasury/TreasuryLayout.vue'),
|
||||||
component: () => import('@/views/treasury/TreasuryHomeView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/accounts',
|
|
||||||
name: 'finance-accounts',
|
|
||||||
component: () => import('@/views/treasury/TreasuryAccountsView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/accounts/:id',
|
|
||||||
name: 'finance-account',
|
|
||||||
component: () => import('@/views/treasury/TreasuryAccountView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/incomes',
|
|
||||||
name: 'finance-incomes',
|
|
||||||
component: () => import('@/views/treasury/TreasuryIncomesView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/expenses',
|
|
||||||
name: 'finance-expenses',
|
|
||||||
component: () => import('@/views/treasury/TreasuryExpensesView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/receivables',
|
|
||||||
name: 'finance-receivables',
|
|
||||||
component: () => import('@/views/treasury/TreasuryReceivablesView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/payroll',
|
|
||||||
name: 'finance-payroll',
|
|
||||||
component: () => import('@/views/treasury/TreasuryPayrollView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/recurring',
|
|
||||||
name: 'finance-recurring',
|
|
||||||
component: () => import('@/views/treasury/TreasuryRecurringView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/reports',
|
|
||||||
name: 'finance-reports',
|
|
||||||
component: () => import('@/views/treasury/TreasuryReportsView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/audit',
|
|
||||||
name: 'finance-audit',
|
|
||||||
component: () => import('@/views/treasury/TreasuryAuditView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'finance/categories',
|
|
||||||
name: 'finance-categories',
|
|
||||||
component: () => import('@/views/treasury/TreasuryCategoriesView.vue'),
|
|
||||||
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
meta: { roles: ['owner', 'admin'] as UserRole[] },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'finance',
|
||||||
|
component: () => import('@/views/treasury/TreasuryHomeView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'مدیریت مالی',
|
||||||
|
treasuryDescription:
|
||||||
|
'دفتر داخلی درآمد، هزینه و موجودی حساب. این بخش جایگزین فاکتور فروش یا حسابداری رسمی نیست.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'accounts',
|
||||||
|
name: 'finance-accounts',
|
||||||
|
component: () => import('@/views/treasury/TreasuryAccountsView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'صندوق و حسابها',
|
||||||
|
treasuryDescription: 'موجودی هر حساب از مانده افتتاحیه و گردش قطعی محاسبه میشود.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'accounts/:id',
|
||||||
|
name: 'finance-account',
|
||||||
|
component: () => import('@/views/treasury/TreasuryAccountView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'جزئیات حساب',
|
||||||
|
treasuryDescription: 'گردش قطعی این حساب',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'incomes',
|
||||||
|
name: 'finance-incomes',
|
||||||
|
component: () => import('@/views/treasury/TreasuryIncomesView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'دریافتها و درآمدها',
|
||||||
|
treasuryDescription:
|
||||||
|
'ثبت سند، موجودی را عوض نمیکند مگر مبلغ دریافت یا پرداخت واقعی وارد شود.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'expenses',
|
||||||
|
name: 'finance-expenses',
|
||||||
|
component: () => import('@/views/treasury/TreasuryExpensesView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'پرداختها و هزینهها',
|
||||||
|
treasuryDescription:
|
||||||
|
'ثبت سند، موجودی را عوض نمیکند مگر مبلغ دریافت یا پرداخت واقعی وارد شود.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'receivables',
|
||||||
|
name: 'finance-receivables',
|
||||||
|
component: () => import('@/views/treasury/TreasuryReceivablesView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'اقساط و مطالبات',
|
||||||
|
treasuryDescription:
|
||||||
|
'دریافت هر قسط فقط یک بار وارد حساب میشود و مانده همان قسط را کم میکند.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'payroll',
|
||||||
|
name: 'finance-payroll',
|
||||||
|
component: () => import('@/views/treasury/TreasuryPayrollView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'حقوق و دستمزد',
|
||||||
|
treasuryDescription: 'فقط مالک و مدیر این صفحه را میبینند.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'recurring',
|
||||||
|
name: 'finance-recurring',
|
||||||
|
component: () => import('@/views/treasury/TreasuryRecurringView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'درآمد و هزینه تکرارشونده',
|
||||||
|
treasuryDescription: 'تا تأیید دریافت یا پرداخت، موجودی حساب عوض نمیشود.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reports',
|
||||||
|
name: 'finance-reports',
|
||||||
|
component: () => import('@/views/treasury/TreasuryReportsView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'گزارش مالی',
|
||||||
|
treasuryDescription: 'سود عملیاتی از درآمد و هزینه ثبتشده است، نه از خالص وجه.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'audit',
|
||||||
|
name: 'finance-audit',
|
||||||
|
component: () => import('@/views/treasury/TreasuryAuditView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'سوابق تغییرات مالی',
|
||||||
|
treasuryDescription: 'تسویه سند و نشستن وجه در صندوق، با مقدار ثبتشده.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'categories',
|
||||||
|
name: 'finance-categories',
|
||||||
|
component: () => import('@/views/treasury/TreasuryCategoriesView.vue'),
|
||||||
|
meta: {
|
||||||
|
roles: ['owner', 'admin'] as UserRole[],
|
||||||
|
treasuryTitle: 'دستهبندیهای مالی',
|
||||||
|
treasuryDescription: 'دستههای پیشفرض با اولین بازدید ساخته میشوند.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'reservations',
|
path: 'reservations',
|
||||||
|
|||||||
@@ -1,121 +1,203 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import {
|
||||||
|
Alert as AAlert,
|
||||||
|
Button as AButton,
|
||||||
|
Card as ACard,
|
||||||
|
Col as ACol,
|
||||||
|
Divider as ADivider,
|
||||||
|
Empty as AEmpty,
|
||||||
|
Flex as AFlex,
|
||||||
|
Form as AForm,
|
||||||
|
FormItem as AFormItem,
|
||||||
|
Input as AInput,
|
||||||
|
Row as ARow,
|
||||||
|
Space as ASpace,
|
||||||
|
Statistic as AStatistic,
|
||||||
|
Table as ATable,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import type { TableColumnsType } from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { FinancialAccountDto, FinancialMovementDto } from '@mento/shared';
|
import type { FinancialAccountDto, FinancialMovementDto } from '@mento/shared';
|
||||||
import { UiButton, UiField, UiInput, UiPageHeader } from '@/components/ui';
|
|
||||||
import MoneyInput from '@/components/MoneyInput.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||||
|
import { parseMoneyNumber } from '@/lib/money';
|
||||||
|
import { useTreasuryPageChrome } from './treasuryPageChrome';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const pageChrome = useTreasuryPageChrome();
|
||||||
const account = ref<FinancialAccountDto | null>(null);
|
const account = ref<FinancialAccountDto | null>(null);
|
||||||
const items = ref<FinancialMovementDto[]>([]);
|
const items = ref<FinancialMovementDto[]>([]);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const stated = ref('');
|
const stated = ref<string | number | null>('');
|
||||||
const reason = ref('');
|
const reason = ref('');
|
||||||
const note = ref('');
|
const note = ref('');
|
||||||
|
const busy = ref(false);
|
||||||
|
|
||||||
|
const columns: TableColumnsType<FinancialMovementDto> = [
|
||||||
|
{
|
||||||
|
title: 'تاریخ',
|
||||||
|
key: 'occurredAt',
|
||||||
|
customRender: ({ record }) => formatJalaliNumeric(new Date(record.occurredAt)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'شرح',
|
||||||
|
key: 'memo',
|
||||||
|
customRender: ({ record }) => record.memo || '—',
|
||||||
|
},
|
||||||
|
{ title: 'نوع', dataIndex: 'kindLabel', key: 'kindLabel' },
|
||||||
|
{
|
||||||
|
title: 'ورودی',
|
||||||
|
key: 'in',
|
||||||
|
customRender: ({ record }) => (record.direction === 'in' ? toman(record.amount) : '—'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'خروجی',
|
||||||
|
key: 'out',
|
||||||
|
customRender: ({ record }) => (record.direction === 'out' ? toman(record.amount) : '—'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'مانده',
|
||||||
|
key: 'balanceAfter',
|
||||||
|
customRender: ({ record }) => (record.balanceAfter ? toman(record.balanceAfter) : '—'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const { data } = await api.get<{ account: FinancialAccountDto; items: FinancialMovementDto[] }>(
|
||||||
|
`/treasury/accounts/${route.params.id}/ledger`,
|
||||||
|
);
|
||||||
|
account.value = data.account;
|
||||||
|
items.value = data.items;
|
||||||
|
pageChrome?.set({
|
||||||
|
title: data.account.name || 'جزئیات حساب',
|
||||||
|
description: 'گردش قطعی این حساب',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function reconcile() {
|
async function reconcile() {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
note.value = '';
|
note.value = '';
|
||||||
|
busy.value = true;
|
||||||
try {
|
try {
|
||||||
const { data } = await api.post<{ difference: string; systemBalance: string }>('/treasury/reconciliations', {
|
const amount = parseMoneyNumber(String(stated.value ?? ''), 0);
|
||||||
accountId: route.params.id,
|
const { data } = await api.post<{ difference: string; systemBalance: string }>(
|
||||||
statedBalance: stated.value,
|
'/treasury/reconciliations',
|
||||||
});
|
{
|
||||||
|
accountId: route.params.id,
|
||||||
|
statedBalance: amount ?? 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
note.value = `اختلاف ${toman(data.difference)} نسبت به موجودی سیستم ${toman(data.systemBalance)}. موجودی خودکار عوض نشد.`;
|
note.value = `اختلاف ${toman(data.difference)} نسبت به موجودی سیستم ${toman(data.systemBalance)}. موجودی خودکار عوض نشد.`;
|
||||||
} catch {
|
} catch {
|
||||||
error.value = 'ثبت مغایرت ناموفق بود.';
|
error.value = 'ثبت مغایرت ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
busy.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function adjust(direction: 'in' | 'out') {
|
async function adjust(direction: 'in' | 'out') {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
busy.value = true;
|
||||||
try {
|
try {
|
||||||
|
const amount = parseMoneyNumber(String(stated.value ?? ''), 0);
|
||||||
await api.post('/treasury/adjustments', {
|
await api.post('/treasury/adjustments', {
|
||||||
accountId: route.params.id,
|
accountId: route.params.id,
|
||||||
amount: stated.value,
|
amount: amount ?? 0,
|
||||||
direction,
|
direction,
|
||||||
reason: reason.value,
|
reason: reason.value,
|
||||||
});
|
});
|
||||||
window.location.reload();
|
await load();
|
||||||
|
note.value = 'اصلاح موجودی ثبت شد.';
|
||||||
} catch {
|
} catch {
|
||||||
error.value = 'اصلاح موجودی ناموفق بود.';
|
error.value = 'اصلاح موجودی ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
busy.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
try {
|
void load().catch(() => {
|
||||||
const { data } = await api.get<{ account: FinancialAccountDto; items: FinancialMovementDto[] }>(
|
|
||||||
`/treasury/accounts/${route.params.id}/ledger`,
|
|
||||||
);
|
|
||||||
account.value = data.account;
|
|
||||||
items.value = data.items;
|
|
||||||
} catch {
|
|
||||||
error.value = 'گردش حساب بارگذاری نشد.';
|
error.value = 'گردش حساب بارگذاری نشد.';
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
pageChrome?.clear();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<a-flex vertical :gap="16">
|
||||||
<UiPageHeader :title="account?.name || 'حساب'" description="گردش قطعی این حساب" />
|
<a-space>
|
||||||
<TreasuryNav />
|
<a-button type="link" @click="router.push('/finance/accounts')">بازگشت به حسابها</a-button>
|
||||||
<p v-if="error" class="err">{{ error }}</p>
|
</a-space>
|
||||||
<div v-if="account" class="stats">
|
|
||||||
<article><span>موجودی فعلی</span><strong>{{ toman(account.balance) }}</strong></article>
|
|
||||||
<article><span>مانده افتتاحیه</span><strong>{{ toman(account.openingBalance) }}</strong></article>
|
|
||||||
<article><span>ورودی</span><strong>{{ toman(account.periodIn) }}</strong></article>
|
|
||||||
<article><span>خروجی</span><strong>{{ toman(account.periodOut) }}</strong></article>
|
|
||||||
</div>
|
|
||||||
<div v-if="account" class="reconcile">
|
|
||||||
<UiField label="موجودی واقعی"><MoneyInput v-model="stated" :decimals="0" /></UiField>
|
|
||||||
<UiField label="علت اصلاح"><UiInput v-model="reason" /></UiField>
|
|
||||||
<UiButton variant="secondary" @click="reconcile">ثبت مغایرت</UiButton>
|
|
||||||
<UiButton variant="secondary" @click="adjust('in')">اصلاح ورود</UiButton>
|
|
||||||
<UiButton variant="secondary" @click="adjust('out')">اصلاح خروج</UiButton>
|
|
||||||
<p v-if="note" class="note">{{ note }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>تاریخ</th>
|
|
||||||
<th>شرح</th>
|
|
||||||
<th>نوع</th>
|
|
||||||
<th>ورودی</th>
|
|
||||||
<th>خروجی</th>
|
|
||||||
<th>مانده</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="row in items" :key="row.id">
|
|
||||||
<td>{{ formatJalaliNumeric(new Date(row.occurredAt)) }}</td>
|
|
||||||
<td>{{ row.memo || '—' }}</td>
|
|
||||||
<td>{{ row.kindLabel }}</td>
|
|
||||||
<td>{{ row.direction === 'in' ? toman(row.amount) : '—' }}</td>
|
|
||||||
<td>{{ row.direction === 'out' ? toman(row.amount) : '—' }}</td>
|
|
||||||
<td>{{ row.balanceAfter ? toman(row.balanceAfter) : '—' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="!items.length"><td colspan="6">گردشی ثبت نشده است.</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
.page { display: grid; gap: 1rem; }
|
<a-alert v-if="note" type="success" show-icon :message="note" />
|
||||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.7rem; }
|
|
||||||
.stats article { background: #fff; border: 1px solid #e7eef2; border-radius: 14px; padding: 0.8rem; }
|
<a-row v-if="account" :gutter="[16, 16]">
|
||||||
.stats span { color: #64748b; display: block; }
|
<a-col :xs="24" :sm="12" :md="6">
|
||||||
.table-wrap { overflow: auto; background: #fff; border: 1px solid #e7eef2; border-radius: 16px; }
|
<a-card :bordered="false">
|
||||||
table { width: 100%; border-collapse: collapse; }
|
<a-statistic title="موجودی فعلی" :value="toman(account.balance)" />
|
||||||
th, td { padding: 0.7rem; text-align: right; border-bottom: 1px solid #f1f5f9; white-space: nowrap; }
|
</a-card>
|
||||||
.reconcile { display: grid; grid-template-columns: 1fr 1fr auto auto auto; gap: 0.6rem; align-items: end; }
|
</a-col>
|
||||||
.note { grid-column: 1 / -1; color: #0f766e; font-weight: 700; }
|
<a-col :xs="24" :sm="12" :md="6">
|
||||||
.err { color: #b91c1c; font-weight: 700; }
|
<a-card :bordered="false">
|
||||||
@media (max-width: 800px) { .reconcile { grid-template-columns: 1fr; } }
|
<a-statistic title="مانده افتتاحیه" :value="toman(account.openingBalance)" />
|
||||||
</style>
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
<a-col :xs="24" :sm="12" :md="6">
|
||||||
|
<a-card :bordered="false">
|
||||||
|
<a-statistic title="ورودی" :value="toman(account.periodIn)" />
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
<a-col :xs="24" :sm="12" :md="6">
|
||||||
|
<a-card :bordered="false">
|
||||||
|
<a-statistic title="خروجی" :value="toman(account.periodOut)" />
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-card v-if="account" :bordered="false">
|
||||||
|
<a-divider orientation="right">مغایرت و اصلاح</a-divider>
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-row :gutter="[16, 0]">
|
||||||
|
<a-col :xs="24" :md="8">
|
||||||
|
<a-form-item label="موجودی واقعی">
|
||||||
|
<MoneyInput v-model="stated" :decimals="0" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :xs="24" :md="8">
|
||||||
|
<a-form-item label="علت اصلاح">
|
||||||
|
<a-input v-model:value="reason" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :xs="24" :md="8">
|
||||||
|
<a-form-item label=" ">
|
||||||
|
<a-space wrap>
|
||||||
|
<a-button :loading="busy" @click="reconcile">ثبت مغایرت</a-button>
|
||||||
|
<a-button :loading="busy" @click="adjust('in')">اصلاح ورود</a-button>
|
||||||
|
<a-button :loading="busy" @click="adjust('out')">اصلاح خروج</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<a-divider orientation="right">گردش حساب</a-divider>
|
||||||
|
<a-empty v-if="!items.length" description="گردشی ثبت نشده است." />
|
||||||
|
<a-table
|
||||||
|
v-else
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="items"
|
||||||
|
:pagination="false"
|
||||||
|
row-key="id"
|
||||||
|
:scroll="{ x: true }"
|
||||||
|
/>
|
||||||
|
</a-card>
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -20,13 +20,16 @@ import {
|
|||||||
Statistic as AStatistic,
|
Statistic as AStatistic,
|
||||||
Tag as ATag,
|
Tag as ATag,
|
||||||
Typography,
|
Typography,
|
||||||
|
message,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { FinancialAccountDto, FinancialAccountType } from '@mento/shared';
|
import type { FinancialAccountDto, FinancialAccountType } from '@mento/shared';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
|
import { parseMoneyNumber } from '@/lib/money';
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
const AListItem = AList.Item;
|
const AListItem = AList.Item;
|
||||||
const AListItemMeta = AList.Item.Meta;
|
const AListItemMeta = AList.Item.Meta;
|
||||||
const ASelectOption = ASelect.Option;
|
const ASelectOption = ASelect.Option;
|
||||||
@@ -82,14 +85,19 @@ async function load() {
|
|||||||
accounts.value = data;
|
accounts.value = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onFinishFailed() {
|
||||||
|
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
async function createAccount() {
|
async function createAccount() {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
ok.value = '';
|
ok.value = '';
|
||||||
try {
|
try {
|
||||||
|
const openingBalance = parseMoneyNumber(String(form.value.openingBalance ?? ''), 0) ?? 0;
|
||||||
await api.post('/treasury/accounts', {
|
await api.post('/treasury/accounts', {
|
||||||
...form.value,
|
...form.value,
|
||||||
openingBalance: form.value.openingBalance || 0,
|
openingBalance,
|
||||||
openingDate: form.value.openingDate || undefined,
|
openingDate: form.value.openingDate || undefined,
|
||||||
});
|
});
|
||||||
openForm.value = false;
|
openForm.value = false;
|
||||||
@@ -117,11 +125,20 @@ async function sendTransfer() {
|
|||||||
error.value = '';
|
error.value = '';
|
||||||
ok.value = '';
|
ok.value = '';
|
||||||
try {
|
try {
|
||||||
|
const amount = parseMoneyNumber(String(transfer.value.amount ?? ''), 0);
|
||||||
|
if (amount == null || amount <= 0) {
|
||||||
|
error.value = 'مبلغ انتقال معتبر نیست.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
await api.post('/treasury/transfers', {
|
await api.post('/treasury/transfers', {
|
||||||
...transfer.value,
|
fromAccountId: transfer.value.fromAccountId,
|
||||||
|
toAccountId: transfer.value.toAccountId,
|
||||||
|
amount,
|
||||||
|
memo: transfer.value.memo || undefined,
|
||||||
clientKey: crypto.randomUUID(),
|
clientKey: crypto.randomUUID(),
|
||||||
});
|
});
|
||||||
transferOpen.value = false;
|
transferOpen.value = false;
|
||||||
|
transfer.value = { fromAccountId: '', toAccountId: '', amount: '', memo: '' };
|
||||||
ok.value = 'انتقال ثبت شد و در درآمد یا هزینه نیامده است.';
|
ok.value = 'انتقال ثبت شد و در درآمد یا هزینه نیامده است.';
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -139,17 +156,7 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-flex vertical :gap="16">
|
||||||
<a-flex vertical :gap="16">
|
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">صندوق و حسابها</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
موجودی هر حساب از مانده افتتاحیه و گردش قطعی محاسبه میشود.
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||||
|
|
||||||
@@ -192,15 +199,24 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template v-if="openForm">
|
<template v-if="openForm">
|
||||||
<a-divider orientation="right">حساب جدید</a-divider>
|
<a-divider orientation="right">حساب جدید</a-divider>
|
||||||
<a-form layout="vertical" @finish="createAccount">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="form"
|
||||||
|
@finish="createAccount"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="نام حساب" name="name">
|
<a-form-item
|
||||||
|
label="نام حساب"
|
||||||
|
name="name"
|
||||||
|
:rules="[{ required: true, message: 'نام حساب الزامی است' }]"
|
||||||
|
>
|
||||||
<a-input v-model:value="form.name" />
|
<a-input v-model:value="form.name" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="نوع" name="type">
|
<a-form-item label="نوع" name="type" :rules="[{ required: true }]">
|
||||||
<a-select v-model:value="form.type" style="width: 100%">
|
<a-select v-model:value="form.type" style="width: 100%">
|
||||||
<a-select-option value="cash">صندوق نقدی</a-select-option>
|
<a-select-option value="cash">صندوق نقدی</a-select-option>
|
||||||
<a-select-option value="bank">حساب بانکی</a-select-option>
|
<a-select-option value="bank">حساب بانکی</a-select-option>
|
||||||
@@ -225,12 +241,12 @@ onMounted(() => {
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="موجودی اولیه" name="openingBalance">
|
<a-form-item label="موجودی اولیه" name="openingBalance">
|
||||||
<a-input v-model:value="form.openingBalance" />
|
<MoneyInput v-model="form.openingBalance" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="تاریخ شروع" name="openingDate">
|
<a-form-item label="تاریخ شروع" name="openingDate">
|
||||||
<a-input v-model:value="form.openingDate" type="date" style="width: 100%" />
|
<JalaliDateInput v-model="form.openingDate" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
@@ -245,12 +261,25 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template v-if="transferOpen">
|
<template v-if="transferOpen">
|
||||||
<a-divider orientation="right">انتقال بین حسابها</a-divider>
|
<a-divider orientation="right">انتقال بین حسابها</a-divider>
|
||||||
<a-form layout="vertical" @finish="sendTransfer">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="transfer"
|
||||||
|
@finish="sendTransfer"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="از حساب" name="fromAccountId">
|
<a-form-item
|
||||||
<a-select v-model:value="transfer.fromAccountId" style="width: 100%" placeholder="انتخاب">
|
label="از حساب"
|
||||||
<a-select-option value="">انتخاب</a-select-option>
|
name="fromAccountId"
|
||||||
|
:rules="[{ required: true, message: 'حساب مبدأ الزامی است' }]"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="transfer.fromAccountId"
|
||||||
|
allow-clear
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="انتخاب"
|
||||||
|
>
|
||||||
<a-select-option v-for="account in active" :key="account.id" :value="account.id">
|
<a-select-option v-for="account in active" :key="account.id" :value="account.id">
|
||||||
{{ account.name }}
|
{{ account.name }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
@@ -258,18 +287,34 @@ onMounted(() => {
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="به حساب" name="toAccountId">
|
<a-form-item
|
||||||
<a-select v-model:value="transfer.toAccountId" style="width: 100%" placeholder="انتخاب">
|
label="به حساب"
|
||||||
<a-select-option value="">انتخاب</a-select-option>
|
name="toAccountId"
|
||||||
<a-select-option v-for="account in active" :key="`to-${account.id}`" :value="account.id">
|
:rules="[{ required: true, message: 'حساب مقصد الزامی است' }]"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="transfer.toAccountId"
|
||||||
|
allow-clear
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="انتخاب"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="account in active"
|
||||||
|
:key="`to-${account.id}`"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
{{ account.name }}
|
{{ account.name }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="مبلغ" name="amount">
|
<a-form-item
|
||||||
<a-input v-model:value="transfer.amount" />
|
label="مبلغ"
|
||||||
|
name="amount"
|
||||||
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
|
>
|
||||||
|
<MoneyInput v-model="transfer.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
@@ -309,6 +354,5 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import {
|
||||||
|
Alert as AAlert,
|
||||||
|
Card as ACard,
|
||||||
|
Empty as AEmpty,
|
||||||
|
Flex as AFlex,
|
||||||
|
List as AList,
|
||||||
|
Typography,
|
||||||
|
} from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { UiPageHeader } from '@/components/ui';
|
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
|
||||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||||
|
|
||||||
|
const { Paragraph: ATypographyParagraph } = Typography;
|
||||||
|
const AListItem = AList.Item;
|
||||||
|
const AListItemMeta = AList.Item.Meta;
|
||||||
|
|
||||||
type Row = {
|
type Row = {
|
||||||
id: string;
|
id: string;
|
||||||
action: string;
|
action: string;
|
||||||
@@ -32,26 +42,31 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<a-flex vertical :gap="16">
|
||||||
<UiPageHeader title="سوابق تغییرات مالی" description="تسویه سند و نشستن وجه در صندوق، با مقدار ثبتشده." />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<TreasuryNav />
|
|
||||||
<p v-if="error" class="err">{{ error }}</p>
|
|
||||||
<p v-if="!error && !rows.length" class="muted">سابقهای نیست.</p>
|
|
||||||
<article v-for="row in rows" :key="row.id">
|
|
||||||
<header>
|
|
||||||
<strong>{{ actionLabel[row.action] ?? row.action }}</strong>
|
|
||||||
<time>{{ formatJalaliNumeric(new Date(row.createdAt)) }}</time>
|
|
||||||
</header>
|
|
||||||
<pre>{{ JSON.stringify(row.after ?? row.before, null, 2) }}</pre>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<a-card :bordered="false">
|
||||||
.page { display: grid; gap: 0.8rem; }
|
<a-empty v-if="!error && !rows.length" description="سابقهای نیست." />
|
||||||
article { background: #fff; border: 1px solid #e7eef2; border-radius: 14px; padding: 0.8rem 1rem; }
|
<a-list v-else item-layout="vertical" :data-source="rows">
|
||||||
header { display: flex; justify-content: space-between; gap: 0.75rem; }
|
<template #renderItem="{ item }">
|
||||||
time, .muted { color: #64748b; }
|
<a-list-item>
|
||||||
pre { margin: 0.45rem 0 0; white-space: pre-wrap; font: inherit; color: #334155; }
|
<a-list-item-meta>
|
||||||
.err { color: #b91c1c; font-weight: 700; }
|
<template #title>
|
||||||
</style>
|
{{ actionLabel[item.action] ?? item.action }}
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
{{ formatJalaliNumeric(new Date(item.createdAt)) }}
|
||||||
|
</template>
|
||||||
|
</a-list-item-meta>
|
||||||
|
<a-typography-paragraph
|
||||||
|
:copyable="true"
|
||||||
|
:style="{ marginBottom: 0, whiteSpace: 'pre-wrap', fontFamily: 'inherit' }"
|
||||||
|
>
|
||||||
|
{{ JSON.stringify(item.after ?? item.before, null, 2) }}
|
||||||
|
</a-typography-paragraph>
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</a-card>
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -16,20 +16,16 @@ import {
|
|||||||
Row as ARow,
|
Row as ARow,
|
||||||
Select as ASelect,
|
Select as ASelect,
|
||||||
Tag as ATag,
|
Tag as ATag,
|
||||||
Typography,
|
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { FinancialCategoryDto } from '@mento/shared';
|
import type { FinancialCategoryDto } from '@mento/shared';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
|
||||||
const AListItem = AList.Item;
|
const AListItem = AList.Item;
|
||||||
const AListItemMeta = AList.Item.Meta;
|
const AListItemMeta = AList.Item.Meta;
|
||||||
const ASelectOption = ASelect.Option;
|
const ASelectOption = ASelect.Option;
|
||||||
|
|
||||||
const rows = ref<FinancialCategoryDto[]>([]);
|
const rows = ref<FinancialCategoryDto[]>([]);
|
||||||
const name = ref('');
|
const form = ref({ name: '', kind: 'expense' as 'income' | 'expense' });
|
||||||
const kind = ref<'income' | 'expense'>('expense');
|
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -40,8 +36,11 @@ async function load() {
|
|||||||
async function add() {
|
async function add() {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
await api.post('/treasury/categories', { name: name.value, kind: kind.value });
|
await api.post('/treasury/categories', {
|
||||||
name.value = '';
|
name: form.value.name,
|
||||||
|
kind: form.value.kind,
|
||||||
|
});
|
||||||
|
form.value.name = '';
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = axios.isAxiosError(e)
|
error.value = axios.isAxiosError(e)
|
||||||
@@ -56,31 +55,25 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-flex vertical :gap="16">
|
||||||
<a-flex vertical :gap="16">
|
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">دستهبندیهای مالی</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
دستههای پیشفرض با اولین بازدید ساخته میشوند.
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
|
|
||||||
<a-card :bordered="false">
|
<a-card :bordered="false">
|
||||||
<a-divider orientation="right">افزودن دسته</a-divider>
|
<a-divider orientation="right">افزودن دسته</a-divider>
|
||||||
<a-form layout="vertical" @finish="add">
|
<a-form layout="vertical" :model="form" @finish="add">
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="نام" name="name">
|
<a-form-item
|
||||||
<a-input v-model:value="name" />
|
label="نام"
|
||||||
|
name="name"
|
||||||
|
:rules="[{ required: true, message: 'نام الزامی است' }]"
|
||||||
|
>
|
||||||
|
<a-input v-model:value="form.name" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="نوع" name="kind">
|
<a-form-item label="نوع" name="kind" :rules="[{ required: true }]">
|
||||||
<a-select v-model:value="kind" style="width: 100%">
|
<a-select v-model:value="form.kind" style="width: 100%">
|
||||||
<a-select-option value="income">درآمد</a-select-option>
|
<a-select-option value="income">درآمد</a-select-option>
|
||||||
<a-select-option value="expense">هزینه</a-select-option>
|
<a-select-option value="expense">هزینه</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -111,6 +104,5 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ import {
|
|||||||
Row as ARow,
|
Row as ARow,
|
||||||
Select as ASelect,
|
Select as ASelect,
|
||||||
Typography,
|
Typography,
|
||||||
|
message,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { FinancialAccountDto, FinancialCategoryDto, FinancialDocumentDto } from '@mento/shared';
|
import type { FinancialAccountDto, FinancialCategoryDto, FinancialDocumentDto } from '@mento/shared';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
|
import { parseMoneyNumber } from '@/lib/money';
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
const AListItem = AList.Item;
|
const AListItem = AList.Item;
|
||||||
const AListItemMeta = AList.Item.Meta;
|
const AListItemMeta = AList.Item.Meta;
|
||||||
const ASelectOption = ASelect.Option;
|
const ASelectOption = ASelect.Option;
|
||||||
@@ -47,7 +49,6 @@ const form = ref({
|
|||||||
notes: '',
|
notes: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = 'پرداختها و هزینهها';
|
|
||||||
const settleLabel = 'مبلغ پرداختشده';
|
const settleLabel = 'مبلغ پرداختشده';
|
||||||
const statusLabel: Record<string, string> = {
|
const statusLabel: Record<string, string> = {
|
||||||
open: 'پرداختنشده',
|
open: 'پرداختنشده',
|
||||||
@@ -81,19 +82,29 @@ async function load() {
|
|||||||
accounts.value = acc.data.filter((row) => row.isActive);
|
accounts.value = acc.data.filter((row) => row.isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onFinishFailed() {
|
||||||
|
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
async function createDoc() {
|
async function createDoc() {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
ok.value = '';
|
ok.value = '';
|
||||||
try {
|
try {
|
||||||
|
const amount = parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
||||||
|
if (amount == null || amount <= 0) {
|
||||||
|
error.value = 'مبلغ کل معتبر نیست.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const settleAmount = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||||
await api.post('/treasury/documents', {
|
await api.post('/treasury/documents', {
|
||||||
kind,
|
kind,
|
||||||
title: form.value.title,
|
title: form.value.title,
|
||||||
amount: form.value.amount,
|
amount,
|
||||||
categoryId: form.value.categoryId || null,
|
categoryId: form.value.categoryId || null,
|
||||||
partyName: form.value.partyName || null,
|
partyName: form.value.partyName || null,
|
||||||
accountId: form.value.accountId || null,
|
accountId: form.value.accountId || null,
|
||||||
settleAmount: form.value.settleAmount || 0,
|
settleAmount,
|
||||||
method: form.value.method,
|
method: form.value.method,
|
||||||
reference: form.value.reference || null,
|
reference: form.value.reference || null,
|
||||||
notes: form.value.notes || null,
|
notes: form.value.notes || null,
|
||||||
@@ -136,43 +147,50 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-flex vertical :gap="16">
|
||||||
<a-flex vertical :gap="16">
|
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">{{ title }}</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
ثبت سند، موجودی را عوض نمیکند مگر مبلغ دریافت یا پرداخت واقعی وارد شود.
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||||
|
|
||||||
<a-card :bordered="false">
|
<a-card :bordered="false">
|
||||||
<a-divider orientation="right">سند جدید</a-divider>
|
<a-divider orientation="right">سند جدید</a-divider>
|
||||||
<a-form layout="vertical" @finish="createDoc">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="form"
|
||||||
|
@finish="createDoc"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="عنوان" name="title">
|
<a-form-item
|
||||||
|
label="عنوان"
|
||||||
|
name="title"
|
||||||
|
:rules="[{ required: true, message: 'عنوان الزامی است' }]"
|
||||||
|
>
|
||||||
<a-input v-model:value="form.title" />
|
<a-input v-model:value="form.title" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="مبلغ کل" name="amount">
|
<a-form-item
|
||||||
<a-input v-model:value="form.amount" />
|
label="مبلغ کل"
|
||||||
|
name="amount"
|
||||||
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
|
>
|
||||||
|
<MoneyInput v-model="form.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item :label="settleLabel" name="settleAmount">
|
<a-form-item :label="settleLabel" name="settleAmount">
|
||||||
<a-input v-model:value="form.settleAmount" />
|
<MoneyInput v-model="form.settleAmount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="دستهبندی" name="categoryId">
|
<a-form-item label="دستهبندی" name="categoryId">
|
||||||
<a-select v-model:value="form.categoryId" allow-clear style="width: 100%" placeholder="بدون دسته">
|
<a-select
|
||||||
<a-select-option value="">بدون دسته</a-select-option>
|
v-model:value="form.categoryId"
|
||||||
|
allow-clear
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="بدون دسته"
|
||||||
|
>
|
||||||
<a-select-option v-for="cat in categories" :key="cat.id" :value="cat.id">
|
<a-select-option v-for="cat in categories" :key="cat.id" :value="cat.id">
|
||||||
{{ cat.name }}
|
{{ cat.name }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
@@ -186,8 +204,12 @@ onMounted(() => {
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="حساب" name="accountId">
|
<a-form-item label="حساب" name="accountId">
|
||||||
<a-select v-model:value="form.accountId" allow-clear style="width: 100%" placeholder="انتخاب حساب">
|
<a-select
|
||||||
<a-select-option value="">انتخاب حساب</a-select-option>
|
v-model:value="form.accountId"
|
||||||
|
allow-clear
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="انتخاب حساب"
|
||||||
|
>
|
||||||
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
|
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
|
||||||
{{ account.name }}
|
{{ account.name }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
@@ -256,6 +278,5 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ import {
|
|||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { TreasuryDashboardDto } from '@mento/shared';
|
import type { TreasuryDashboardDto } from '@mento/shared';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
const AListItem = AList.Item;
|
const AListItem = AList.Item;
|
||||||
const AListItemMeta = AList.Item.Meta;
|
const AListItemMeta = AList.Item.Meta;
|
||||||
|
|
||||||
@@ -46,17 +45,7 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-flex vertical :gap="16">
|
||||||
<a-flex vertical :gap="16">
|
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">مدیریت مالی</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
دفتر داخلی درآمد، هزینه و موجودی حساب. این بخش جایگزین فاکتور فروش یا حسابداری رسمی نیست.
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
|
|
||||||
<template v-if="data">
|
<template v-if="data">
|
||||||
@@ -157,6 +146,5 @@ onMounted(async () => {
|
|||||||
</a-list>
|
</a-list>
|
||||||
</a-card>
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ import {
|
|||||||
Space as ASpace,
|
Space as ASpace,
|
||||||
Textarea as ATextarea,
|
Textarea as ATextarea,
|
||||||
Typography,
|
Typography,
|
||||||
|
message,
|
||||||
theme,
|
theme,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import type { FormInstance } from 'ant-design-vue';
|
import type { FormInstance } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
ArrowDownOutlined,
|
ArrowDownOutlined,
|
||||||
ArrowLeftOutlined,
|
|
||||||
BankOutlined,
|
BankOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
@@ -48,10 +48,11 @@ import type {
|
|||||||
TreasuryDashboardDto,
|
TreasuryDashboardDto,
|
||||||
} from '@mento/shared';
|
} from '@mento/shared';
|
||||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
|
import { parseMoneyNumber } from '@/lib/money';
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
const AListItem = AList.Item;
|
const AListItem = AList.Item;
|
||||||
const AListItemMeta = AList.Item.Meta;
|
const AListItemMeta = AList.Item.Meta;
|
||||||
const ASelectOption = ASelect.Option;
|
const ASelectOption = ASelect.Option;
|
||||||
@@ -91,7 +92,6 @@ function emptyForm() {
|
|||||||
|
|
||||||
const form = ref(emptyForm());
|
const form = ref(emptyForm());
|
||||||
|
|
||||||
const pageTitle = 'دریافتها و درآمدها';
|
|
||||||
const settleLabel = 'مبلغ دریافتشده';
|
const settleLabel = 'مبلغ دریافتشده';
|
||||||
const statusLabel: Record<string, string> = {
|
const statusLabel: Record<string, string> = {
|
||||||
open: 'دریافتنشده',
|
open: 'دریافتنشده',
|
||||||
@@ -122,11 +122,15 @@ const formTabOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const remainingAmount = computed(() => {
|
const remainingAmount = computed(() => {
|
||||||
const total = Number(form.value.amount) || 0;
|
const total = parseMoneyNumber(String(form.value.amount ?? ''), 0) ?? 0;
|
||||||
const settled = Number(form.value.settleAmount) || 0;
|
const settled = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||||
return Math.max(0, total - settled);
|
return Math.max(0, total - settled);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onFinishFailed() {
|
||||||
|
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
function apiMessage(e: unknown) {
|
function apiMessage(e: unknown) {
|
||||||
if (!axios.isAxiosError(e)) return 'ذخیره ناموفق بود.';
|
if (!axios.isAxiosError(e)) return 'ذخیره ناموفق بود.';
|
||||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||||
@@ -158,14 +162,20 @@ async function createDoc() {
|
|||||||
error.value = '';
|
error.value = '';
|
||||||
ok.value = '';
|
ok.value = '';
|
||||||
try {
|
try {
|
||||||
|
const amount = parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
||||||
|
if (amount == null || amount <= 0) {
|
||||||
|
error.value = 'مبلغ کل معتبر نیست.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const settleAmount = parseMoneyNumber(String(form.value.settleAmount ?? ''), 0) ?? 0;
|
||||||
await api.post('/treasury/documents', {
|
await api.post('/treasury/documents', {
|
||||||
kind,
|
kind,
|
||||||
title: form.value.title,
|
title: form.value.title,
|
||||||
amount: form.value.amount,
|
amount,
|
||||||
categoryId: form.value.categoryId || null,
|
categoryId: form.value.categoryId || null,
|
||||||
partyName: form.value.partyName || null,
|
partyName: form.value.partyName || null,
|
||||||
accountId: form.value.accountId || null,
|
accountId: form.value.accountId || null,
|
||||||
settleAmount: form.value.settleAmount || 0,
|
settleAmount,
|
||||||
method: form.value.method,
|
method: form.value.method,
|
||||||
reference: form.value.reference || null,
|
reference: form.value.reference || null,
|
||||||
notes: form.value.notes || null,
|
notes: form.value.notes || null,
|
||||||
@@ -243,18 +253,6 @@ onMounted(() => {
|
|||||||
<!-- ─── Mobile app experience ─── -->
|
<!-- ─── Mobile app experience ─── -->
|
||||||
<template v-if="isMobile">
|
<template v-if="isMobile">
|
||||||
<a-flex vertical :gap="token.margin">
|
<a-flex vertical :gap="token.margin">
|
||||||
<div>
|
|
||||||
<a-flex align="center" :gap="token.marginXS">
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0, flex: 1 }">
|
|
||||||
ثبت دریافت جدید
|
|
||||||
</a-typography-title>
|
|
||||||
<ArrowLeftOutlined :style="{ color: token.colorTextSecondary, fontSize: '18px' }" />
|
|
||||||
</a-flex>
|
|
||||||
<a-typography-text type="secondary">اطلاعات دریافت را وارد کنید</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-row :gutter="[12, 12]">
|
<a-row :gutter="[12, 12]">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-card
|
<a-card
|
||||||
@@ -418,14 +416,7 @@ onMounted(() => {
|
|||||||
name="amount"
|
name="amount"
|
||||||
:rules="[{ required: true, message: 'مبلغ کل را وارد کنید' }]"
|
:rules="[{ required: true, message: 'مبلغ کل را وارد کنید' }]"
|
||||||
>
|
>
|
||||||
<a-input
|
<MoneyInput v-model="form.amount" :decimals="0" placeholder="۰" />
|
||||||
v-model:value="form.amount"
|
|
||||||
inputmode="decimal"
|
|
||||||
placeholder="۰"
|
|
||||||
dir="ltr"
|
|
||||||
>
|
|
||||||
<template #prefix><NumberOutlined /></template>
|
|
||||||
</a-input>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
@@ -434,14 +425,7 @@ onMounted(() => {
|
|||||||
name="settleAmount"
|
name="settleAmount"
|
||||||
:rules="[{ required: true, message: 'مبلغ دریافتشده را وارد کنید' }]"
|
:rules="[{ required: true, message: 'مبلغ دریافتشده را وارد کنید' }]"
|
||||||
>
|
>
|
||||||
<a-input
|
<MoneyInput v-model="form.settleAmount" :decimals="0" placeholder="۰" />
|
||||||
v-model:value="form.settleAmount"
|
|
||||||
inputmode="decimal"
|
|
||||||
placeholder="۰"
|
|
||||||
dir="ltr"
|
|
||||||
>
|
|
||||||
<template #prefix><NumberOutlined /></template>
|
|
||||||
</a-input>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
@@ -596,35 +580,39 @@ onMounted(() => {
|
|||||||
<!-- ─── Desktop (unchanged interaction model) ─── -->
|
<!-- ─── Desktop (unchanged interaction model) ─── -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-flex vertical :gap="16">
|
<a-flex vertical :gap="16">
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">{{ pageTitle }}</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
ثبت سند، موجودی را عوض نمیکند مگر مبلغ دریافت یا پرداخت واقعی وارد شود.
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||||
|
|
||||||
<a-card :bordered="false">
|
<a-card :bordered="false">
|
||||||
<a-divider orientation="right">سند جدید</a-divider>
|
<a-divider orientation="right">سند جدید</a-divider>
|
||||||
<a-form layout="vertical" @finish="createDoc">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="form"
|
||||||
|
@finish="createDoc"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="عنوان" name="title">
|
<a-form-item
|
||||||
|
label="عنوان"
|
||||||
|
name="title"
|
||||||
|
:rules="[{ required: true, message: 'عنوان الزامی است' }]"
|
||||||
|
>
|
||||||
<a-input v-model:value="form.title" />
|
<a-input v-model:value="form.title" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="مبلغ کل" name="amount">
|
<a-form-item
|
||||||
<a-input v-model:value="form.amount" />
|
label="مبلغ کل"
|
||||||
|
name="amount"
|
||||||
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
|
>
|
||||||
|
<MoneyInput v-model="form.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item :label="settleLabel" name="settleAmount">
|
<a-form-item :label="settleLabel" name="settleAmount">
|
||||||
<a-input v-model:value="form.settleAmount" />
|
<MoneyInput v-model="form.settleAmount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<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 TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
||||||
|
import { provideTreasuryPageChrome } from './treasuryPageChrome';
|
||||||
|
|
||||||
|
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const route = useRoute();
|
||||||
|
const chrome = provideTreasuryPageChrome();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
chrome.clear();
|
||||||
|
const scroller = document.querySelector('main.content');
|
||||||
|
if (scroller instanceof HTMLElement) scroller.scrollTop = 0;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = computed(
|
||||||
|
() =>
|
||||||
|
chrome.override.value.title ??
|
||||||
|
(typeof route.meta.treasuryTitle === 'string' ? route.meta.treasuryTitle : 'مدیریت مالی'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const description = computed(
|
||||||
|
() =>
|
||||||
|
chrome.override.value.description ??
|
||||||
|
(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`,
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-flex vertical :gap="token.margin" :style="{ width: '100%' }">
|
||||||
|
<div :style="headerStyle">
|
||||||
|
<a-typography-title :level="3" :style="{ margin: 0 }">{{ title }}</a-typography-title>
|
||||||
|
<a-typography-text
|
||||||
|
type="secondary"
|
||||||
|
:ellipsis="description ? { tooltip: description } : false"
|
||||||
|
:style="{ display: 'block', margin: 0 }"
|
||||||
|
>
|
||||||
|
{{ description || '\u00a0' }}
|
||||||
|
</a-typography-text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TreasuryNav />
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
@@ -16,13 +16,15 @@ import {
|
|||||||
Row as ARow,
|
Row as ARow,
|
||||||
Select as ASelect,
|
Select as ASelect,
|
||||||
Typography,
|
Typography,
|
||||||
|
message,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { FinancialAccountDto, OrgUserDto } from '@mento/shared';
|
import type { FinancialAccountDto, OrgUserDto } from '@mento/shared';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
|
import { parseMoneyNumber } from '@/lib/money';
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
const AListItem = AList.Item;
|
const AListItem = AList.Item;
|
||||||
const AListItemMeta = AList.Item.Meta;
|
const AListItemMeta = AList.Item.Meta;
|
||||||
const ASelectOption = ASelect.Option;
|
const ASelectOption = ASelect.Option;
|
||||||
@@ -41,8 +43,16 @@ const users = ref<OrgUserDto[]>([]);
|
|||||||
const accounts = ref<FinancialAccountDto[]>([]);
|
const accounts = ref<FinancialAccountDto[]>([]);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const ok = ref('');
|
const ok = ref('');
|
||||||
const form = ref({ userId: '', period: '', baseAmount: '', benefits: '', bonus: '', deductions: '' });
|
const saving = ref(false);
|
||||||
const pay = ref({ id: '', amount: '', accountId: '' });
|
const form = ref({
|
||||||
|
userId: undefined as string | undefined,
|
||||||
|
period: '',
|
||||||
|
baseAmount: '',
|
||||||
|
benefits: '',
|
||||||
|
bonus: '',
|
||||||
|
deductions: '',
|
||||||
|
});
|
||||||
|
const pay = ref({ id: '', amount: '', accountId: undefined as string | undefined });
|
||||||
|
|
||||||
function apiMessage(e: unknown) {
|
function apiMessage(e: unknown) {
|
||||||
if (!axios.isAxiosError(e)) return 'عملیات ناموفق بود.';
|
if (!axios.isAxiosError(e)) return 'عملیات ناموفق بود.';
|
||||||
@@ -50,6 +60,10 @@ function apiMessage(e: unknown) {
|
|||||||
return Array.isArray(msg) ? msg.join('، ') : msg || 'عملیات ناموفق بود.';
|
return Array.isArray(msg) ? msg.join('، ') : msg || 'عملیات ناموفق بود.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onFinishFailed() {
|
||||||
|
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const [payroll, people, acc] = await Promise.all([
|
const [payroll, people, acc] = await Promise.all([
|
||||||
api.get<PayrollRow[]>('/treasury/payroll'),
|
api.get<PayrollRow[]>('/treasury/payroll'),
|
||||||
@@ -63,33 +77,66 @@ async function load() {
|
|||||||
|
|
||||||
async function createRow() {
|
async function createRow() {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
ok.value = '';
|
||||||
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
await api.post('/treasury/payroll', form.value);
|
const baseAmount = parseMoneyNumber(String(form.value.baseAmount ?? ''), 0) ?? 0;
|
||||||
|
await api.post('/treasury/payroll', {
|
||||||
|
userId: form.value.userId,
|
||||||
|
period: form.value.period,
|
||||||
|
baseAmount,
|
||||||
|
benefits: parseMoneyNumber(String(form.value.benefits ?? ''), 0) ?? 0,
|
||||||
|
bonus: parseMoneyNumber(String(form.value.bonus ?? ''), 0) ?? 0,
|
||||||
|
deductions: parseMoneyNumber(String(form.value.deductions ?? ''), 0) ?? 0,
|
||||||
|
});
|
||||||
|
form.value = {
|
||||||
|
userId: undefined,
|
||||||
|
period: '',
|
||||||
|
baseAmount: '',
|
||||||
|
benefits: '',
|
||||||
|
bonus: '',
|
||||||
|
deductions: '',
|
||||||
|
};
|
||||||
ok.value = 'حقوق ثبت شد و تا پرداخت، موجودی حساب تغییر نمیکند.';
|
ok.value = 'حقوق ثبت شد و تا پرداخت، موجودی حساب تغییر نمیکند.';
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = apiMessage(e);
|
error.value = apiMessage(e);
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function payRow() {
|
async function payRow() {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
ok.value = '';
|
||||||
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
|
const amount = parseMoneyNumber(String(pay.value.amount ?? ''), 0);
|
||||||
|
if (amount == null || amount <= 0) {
|
||||||
|
error.value = 'مبلغ پرداخت معتبر نیست.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
await api.post(`/treasury/payroll/${pay.value.id}/pay`, {
|
await api.post(`/treasury/payroll/${pay.value.id}/pay`, {
|
||||||
amount: pay.value.amount,
|
amount,
|
||||||
accountId: pay.value.accountId,
|
accountId: pay.value.accountId,
|
||||||
clientKey: crypto.randomUUID(),
|
clientKey: crypto.randomUUID(),
|
||||||
});
|
});
|
||||||
ok.value = 'پرداخت حقوق از حساب ثبت شد.';
|
ok.value = 'پرداخت حقوق از حساب ثبت شد.';
|
||||||
pay.value = { id: '', amount: '', accountId: '' };
|
pay.value = { id: '', amount: '', accountId: undefined };
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = apiMessage(e);
|
error.value = apiMessage(e);
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPay(row: PayrollRow) {
|
function startPay(row: PayrollRow) {
|
||||||
pay.value = { id: row.id, amount: row.remaining, accountId: accounts.value[0]?.id || '' };
|
pay.value = {
|
||||||
|
id: row.id,
|
||||||
|
amount: row.remaining,
|
||||||
|
accountId: accounts.value[0]?.id,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -100,26 +147,31 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-flex vertical :gap="16">
|
||||||
<a-flex vertical :gap="16">
|
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">حقوق و دستمزد</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">فقط مالک و مدیر این صفحه را میبینند.</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||||
|
|
||||||
<a-card :bordered="false">
|
<a-card :bordered="false">
|
||||||
<a-divider orientation="right">ثبت دوره</a-divider>
|
<a-divider orientation="right">ثبت دوره</a-divider>
|
||||||
<a-form layout="vertical" @finish="createRow">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="form"
|
||||||
|
@finish="createRow"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="پرسنل" name="userId">
|
<a-form-item
|
||||||
<a-select v-model:value="form.userId" style="width: 100%" placeholder="انتخاب">
|
label="پرسنل"
|
||||||
<a-select-option value="">انتخاب</a-select-option>
|
name="userId"
|
||||||
|
:rules="[{ required: true, message: 'پرسنل الزامی است' }]"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="form.userId"
|
||||||
|
allow-clear
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="انتخاب"
|
||||||
|
>
|
||||||
<a-select-option v-for="user in users" :key="user.id" :value="user.id">
|
<a-select-option v-for="user in users" :key="user.id" :value="user.id">
|
||||||
{{ user.fullName }}
|
{{ user.fullName }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
@@ -127,54 +179,84 @@ onMounted(() => {
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="دوره" name="period">
|
<a-form-item
|
||||||
|
label="دوره"
|
||||||
|
name="period"
|
||||||
|
:rules="[{ required: true, message: 'دوره الزامی است' }]"
|
||||||
|
>
|
||||||
<a-input v-model:value="form.period" placeholder="۱۴۰۵-۰۷" />
|
<a-input v-model:value="form.period" placeholder="۱۴۰۵-۰۷" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="پایه" name="baseAmount">
|
<a-form-item
|
||||||
<a-input v-model:value="form.baseAmount" />
|
label="پایه"
|
||||||
|
name="baseAmount"
|
||||||
|
:rules="[{ required: true, message: 'مبلغ پایه الزامی است' }]"
|
||||||
|
>
|
||||||
|
<MoneyInput v-model="form.baseAmount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="مزایا" name="benefits">
|
<a-form-item label="مزایا" name="benefits">
|
||||||
<a-input v-model:value="form.benefits" />
|
<MoneyInput v-model="form.benefits" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="پاداش" name="bonus">
|
<a-form-item label="پاداش" name="bonus">
|
||||||
<a-input v-model:value="form.bonus" />
|
<MoneyInput v-model="form.bonus" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="کسورات" name="deductions">
|
<a-form-item label="کسورات" name="deductions">
|
||||||
<a-input v-model:value="form.deductions" />
|
<MoneyInput v-model="form.deductions" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-button type="primary" html-type="submit">ثبت حقوق</a-button>
|
<a-button type="primary" html-type="submit" :loading="saving">ثبت حقوق</a-button>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<template v-if="pay.id">
|
<template v-if="pay.id">
|
||||||
<a-divider orientation="right">پرداخت حقوق</a-divider>
|
<a-divider orientation="right">پرداخت حقوق</a-divider>
|
||||||
<a-form layout="vertical" @finish="payRow">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="pay"
|
||||||
|
@finish="payRow"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="مبلغ" name="amount">
|
<a-form-item
|
||||||
<a-input v-model:value="pay.amount" />
|
label="مبلغ"
|
||||||
|
name="amount"
|
||||||
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
|
>
|
||||||
|
<MoneyInput v-model="pay.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="حساب" name="accountId">
|
<a-form-item
|
||||||
<a-select v-model:value="pay.accountId" style="width: 100%">
|
label="حساب"
|
||||||
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
|
name="accountId"
|
||||||
|
:rules="[{ required: true, message: 'حساب الزامی است' }]"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="pay.accountId"
|
||||||
|
allow-clear
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="انتخاب حساب"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="account in accounts"
|
||||||
|
:key="account.id"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
{{ account.name }}
|
{{ account.name }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-button type="primary" html-type="submit">ثبت پرداخت</a-button>
|
<a-button type="primary" html-type="submit" :loading="saving">ثبت پرداخت</a-button>
|
||||||
</a-form>
|
</a-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -203,6 +285,5 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,15 +19,18 @@ import {
|
|||||||
Table as ATable,
|
Table as ATable,
|
||||||
Tag as ATag,
|
Tag as ATag,
|
||||||
Typography,
|
Typography,
|
||||||
|
message,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import type { TableColumnsType } from 'ant-design-vue';
|
import type { TableColumnsType } from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { FinancialAccountDto, FinancialReceivableDto } from '@mento/shared';
|
import type { FinancialAccountDto, FinancialReceivableDto } from '@mento/shared';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||||
|
import { parseMoneyNumber } from '@/lib/money';
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
const ASelectOption = ASelect.Option;
|
const ASelectOption = ASelect.Option;
|
||||||
|
|
||||||
type InstallmentRow = FinancialReceivableDto['installments'][number];
|
type InstallmentRow = FinancialReceivableDto['installments'][number];
|
||||||
@@ -109,19 +112,35 @@ async function downloadExcel() {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onFinishFailed() {
|
||||||
|
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
async function createContract() {
|
async function createContract() {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
ok.value = '';
|
ok.value = '';
|
||||||
try {
|
try {
|
||||||
|
const totalAmount = parseMoneyNumber(String(form.value.totalAmount ?? ''), 0);
|
||||||
|
if (totalAmount == null || totalAmount <= 0) {
|
||||||
|
error.value = 'مبلغ کل معتبر نیست.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prepaidAmount = parseMoneyNumber(String(form.value.prepaidAmount ?? ''), 0) ?? 0;
|
||||||
|
const installments = custom.value
|
||||||
|
? customLines.value.map((line) => ({
|
||||||
|
amount: parseMoneyNumber(String(line.amount ?? ''), 0) ?? 0,
|
||||||
|
dueDate: line.dueDate,
|
||||||
|
}))
|
||||||
|
: undefined;
|
||||||
await api.post('/treasury/receivables', {
|
await api.post('/treasury/receivables', {
|
||||||
title: form.value.title,
|
title: form.value.title,
|
||||||
partyName: form.value.partyName || null,
|
partyName: form.value.partyName || null,
|
||||||
totalAmount: form.value.totalAmount,
|
totalAmount,
|
||||||
prepaidAmount: form.value.prepaidAmount || 0,
|
prepaidAmount,
|
||||||
accountId: form.value.accountId || null,
|
accountId: form.value.accountId || null,
|
||||||
...(custom.value
|
...(custom.value
|
||||||
? { installments: customLines.value }
|
? { installments }
|
||||||
: {
|
: {
|
||||||
count: Number(form.value.count),
|
count: Number(form.value.count),
|
||||||
interval: form.value.interval,
|
interval: form.value.interval,
|
||||||
@@ -145,8 +164,13 @@ async function collectInstallment() {
|
|||||||
saving.value = true;
|
saving.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
|
const amount = parseMoneyNumber(String(collect.value.amount ?? ''), 0);
|
||||||
|
if (amount == null || amount <= 0) {
|
||||||
|
error.value = 'مبلغ دریافت معتبر نیست.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
await api.post(`/treasury/installments/${collect.value.installmentId}/collect`, {
|
await api.post(`/treasury/installments/${collect.value.installmentId}/collect`, {
|
||||||
amount: collect.value.amount,
|
amount,
|
||||||
accountId: collect.value.accountId,
|
accountId: collect.value.accountId,
|
||||||
clientKey: crypto.randomUUID(),
|
clientKey: crypto.randomUUID(),
|
||||||
});
|
});
|
||||||
@@ -177,26 +201,25 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-flex vertical :gap="16">
|
||||||
<a-flex vertical :gap="16">
|
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">اقساط و مطالبات</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
دریافت هر قسط فقط یک بار وارد حساب میشود و مانده همان قسط را کم میکند.
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||||
|
|
||||||
<a-card :bordered="false">
|
<a-card :bordered="false">
|
||||||
<a-divider orientation="right">قرارداد جدید</a-divider>
|
<a-divider orientation="right">قرارداد جدید</a-divider>
|
||||||
<a-form layout="vertical" @finish="createContract">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="form"
|
||||||
|
@finish="createContract"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="عنوان" name="title">
|
<a-form-item
|
||||||
|
label="عنوان"
|
||||||
|
name="title"
|
||||||
|
:rules="[{ required: true, message: 'عنوان الزامی است' }]"
|
||||||
|
>
|
||||||
<a-input v-model:value="form.title" />
|
<a-input v-model:value="form.title" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -206,19 +229,27 @@ onMounted(() => {
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="مبلغ کل" name="totalAmount">
|
<a-form-item
|
||||||
<a-input v-model:value="form.totalAmount" />
|
label="مبلغ کل"
|
||||||
|
name="totalAmount"
|
||||||
|
:rules="[{ required: true, message: 'مبلغ کل الزامی است' }]"
|
||||||
|
>
|
||||||
|
<MoneyInput v-model="form.totalAmount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="پیشپرداخت" name="prepaidAmount">
|
<a-form-item label="پیشپرداخت" name="prepaidAmount">
|
||||||
<a-input v-model:value="form.prepaidAmount" />
|
<MoneyInput v-model="form.prepaidAmount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="حساب پیشپرداخت" name="accountId">
|
<a-form-item label="حساب پیشپرداخت" name="accountId">
|
||||||
<a-select v-model:value="form.accountId" allow-clear style="width: 100%" placeholder="انتخاب">
|
<a-select
|
||||||
<a-select-option value="">انتخاب</a-select-option>
|
v-model:value="form.accountId"
|
||||||
|
allow-clear
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="انتخاب"
|
||||||
|
>
|
||||||
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
|
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
|
||||||
{{ account.name }}
|
{{ account.name }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
@@ -240,7 +271,7 @@ onMounted(() => {
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="شروع" name="startDate">
|
<a-form-item label="شروع" name="startDate">
|
||||||
<a-input v-model:value="form.startDate" type="date" :disabled="custom" style="width: 100%" />
|
<JalaliDateInput v-model="form.startDate" :disabled="custom" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24">
|
<a-col :xs="24">
|
||||||
@@ -254,12 +285,12 @@ onMounted(() => {
|
|||||||
<a-row v-for="(line, index) in customLines" :key="index" :gutter="[16, 0]">
|
<a-row v-for="(line, index) in customLines" :key="index" :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item :label="`مبلغ قسط ${index + 1}`">
|
<a-form-item :label="`مبلغ قسط ${index + 1}`">
|
||||||
<a-input v-model:value="line.amount" />
|
<MoneyInput v-model="line.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="سررسید">
|
<a-form-item label="سررسید">
|
||||||
<a-input v-model:value="line.dueDate" type="date" style="width: 100%" />
|
<JalaliDateInput v-model="line.dueDate" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
@@ -276,17 +307,39 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template v-if="collect.installmentId">
|
<template v-if="collect.installmentId">
|
||||||
<a-divider orientation="right">دریافت قسط</a-divider>
|
<a-divider orientation="right">دریافت قسط</a-divider>
|
||||||
<a-form layout="vertical" @finish="collectInstallment">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="collect"
|
||||||
|
@finish="collectInstallment"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="مبلغ" name="amount">
|
<a-form-item
|
||||||
<a-input v-model:value="collect.amount" />
|
label="مبلغ"
|
||||||
|
name="amount"
|
||||||
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
|
>
|
||||||
|
<MoneyInput v-model="collect.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="حساب مقصد" name="accountId">
|
<a-form-item
|
||||||
<a-select v-model:value="collect.accountId" style="width: 100%">
|
label="حساب مقصد"
|
||||||
<a-select-option v-for="account in accounts" :key="account.id" :value="account.id">
|
name="accountId"
|
||||||
|
:rules="[{ required: true, message: 'حساب الزامی است' }]"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="collect.accountId"
|
||||||
|
allow-clear
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="انتخاب حساب"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="account in accounts"
|
||||||
|
:key="account.id"
|
||||||
|
:value="account.id"
|
||||||
|
>
|
||||||
{{ account.name }}
|
{{ account.name }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
@@ -337,6 +390,5 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,21 +16,33 @@ import {
|
|||||||
Row as ARow,
|
Row as ARow,
|
||||||
Select as ASelect,
|
Select as ASelect,
|
||||||
Typography,
|
Typography,
|
||||||
|
message,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||||
|
import { parseMoneyNumber } from '@/lib/money';
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
const AListItem = AList.Item;
|
const AListItem = AList.Item;
|
||||||
const AListItemMeta = AList.Item.Meta;
|
const AListItemMeta = AList.Item.Meta;
|
||||||
const ASelectOption = ASelect.Option;
|
const ASelectOption = ASelect.Option;
|
||||||
|
|
||||||
type Row = { id: string; title: string; kind: string; amount: string; interval: string; nextDue: string; isActive: boolean };
|
type Row = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
kind: string;
|
||||||
|
amount: string;
|
||||||
|
interval: string;
|
||||||
|
nextDue: string;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
const rows = ref<Row[]>([]);
|
const rows = ref<Row[]>([]);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const ok = ref('');
|
const ok = ref('');
|
||||||
|
const saving = ref(false);
|
||||||
const form = ref({ title: '', kind: 'expense', amount: '', interval: 'monthly', startDate: '' });
|
const form = ref({ title: '', kind: 'expense', amount: '', interval: 'monthly', startDate: '' });
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -38,16 +50,34 @@ async function load() {
|
|||||||
rows.value = data;
|
rows.value = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onFinishFailed() {
|
||||||
|
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
async function createItem() {
|
async function createItem() {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
ok.value = '';
|
||||||
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
await api.post('/treasury/recurring', form.value);
|
const amount = parseMoneyNumber(String(form.value.amount ?? ''), 0);
|
||||||
|
if (amount == null || amount <= 0) {
|
||||||
|
error.value = 'مبلغ معتبر نیست.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.post('/treasury/recurring', {
|
||||||
|
...form.value,
|
||||||
|
amount,
|
||||||
|
startDate: form.value.startDate || undefined,
|
||||||
|
});
|
||||||
|
form.value = { title: '', kind: 'expense', amount: '', interval: 'monthly', startDate: '' };
|
||||||
ok.value = 'الگو ثبت شد. در سررسید فقط سند باز ساخته میشود.';
|
ok.value = 'الگو ثبت شد. در سررسید فقط سند باز ساخته میشود.';
|
||||||
await load();
|
await load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = axios.isAxiosError(e)
|
error.value = axios.isAxiosError(e)
|
||||||
? String((e.response?.data as { message?: string })?.message ?? 'ثبت ناموفق بود.')
|
? String((e.response?.data as { message?: string })?.message ?? 'ثبت ناموفق بود.')
|
||||||
: 'ثبت ناموفق بود.';
|
: 'ثبت ناموفق بود.';
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,31 +87,30 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-flex vertical :gap="16">
|
||||||
<a-flex vertical :gap="16">
|
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">درآمد و هزینه تکرارشونده</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
تا تأیید دریافت یا پرداخت، موجودی حساب عوض نمیشود.
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
<a-alert v-if="ok" type="success" show-icon :message="ok" />
|
||||||
|
|
||||||
<a-card :bordered="false">
|
<a-card :bordered="false">
|
||||||
<a-divider orientation="right">الگوی جدید</a-divider>
|
<a-divider orientation="right">الگوی جدید</a-divider>
|
||||||
<a-form layout="vertical" @finish="createItem">
|
<a-form
|
||||||
|
layout="vertical"
|
||||||
|
:model="form"
|
||||||
|
@finish="createItem"
|
||||||
|
@finishFailed="onFinishFailed"
|
||||||
|
>
|
||||||
<a-row :gutter="[16, 0]">
|
<a-row :gutter="[16, 0]">
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<a-form-item label="عنوان" name="title">
|
<a-form-item
|
||||||
|
label="عنوان"
|
||||||
|
name="title"
|
||||||
|
:rules="[{ required: true, message: 'عنوان الزامی است' }]"
|
||||||
|
>
|
||||||
<a-input v-model:value="form.title" />
|
<a-input v-model:value="form.title" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="نوع" name="kind">
|
<a-form-item label="نوع" name="kind" :rules="[{ required: true }]">
|
||||||
<a-select v-model:value="form.kind" style="width: 100%">
|
<a-select v-model:value="form.kind" style="width: 100%">
|
||||||
<a-select-option value="expense">هزینه</a-select-option>
|
<a-select-option value="expense">هزینه</a-select-option>
|
||||||
<a-select-option value="income">درآمد</a-select-option>
|
<a-select-option value="income">درآمد</a-select-option>
|
||||||
@@ -89,12 +118,16 @@ onMounted(() => {
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="مبلغ" name="amount">
|
<a-form-item
|
||||||
<a-input v-model:value="form.amount" />
|
label="مبلغ"
|
||||||
|
name="amount"
|
||||||
|
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
|
||||||
|
>
|
||||||
|
<MoneyInput v-model="form.amount" :decimals="0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="دوره" name="interval">
|
<a-form-item label="دوره" name="interval" :rules="[{ required: true }]">
|
||||||
<a-select v-model:value="form.interval" style="width: 100%">
|
<a-select v-model:value="form.interval" style="width: 100%">
|
||||||
<a-select-option value="monthly">ماهانه</a-select-option>
|
<a-select-option value="monthly">ماهانه</a-select-option>
|
||||||
<a-select-option value="weekly">هفتگی</a-select-option>
|
<a-select-option value="weekly">هفتگی</a-select-option>
|
||||||
@@ -104,11 +137,11 @@ onMounted(() => {
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :md="8">
|
<a-col :xs="24" :md="8">
|
||||||
<a-form-item label="شروع" name="startDate">
|
<a-form-item label="شروع" name="startDate">
|
||||||
<a-input v-model:value="form.startDate" type="date" style="width: 100%" />
|
<JalaliDateInput v-model="form.startDate" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-button type="primary" html-type="submit">ثبت الگو</a-button>
|
<a-button type="primary" html-type="submit" :loading="saving">ثبت الگو</a-button>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<a-divider orientation="right">فهرست الگوها</a-divider>
|
<a-divider orientation="right">فهرست الگوها</a-divider>
|
||||||
@@ -131,6 +164,5 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ import {
|
|||||||
import type { TableColumnsType } from 'ant-design-vue';
|
import type { TableColumnsType } from 'ant-design-vue';
|
||||||
import { DownloadOutlined } from '@ant-design/icons-vue';
|
import { DownloadOutlined } from '@ant-design/icons-vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
|
|
||||||
import { toman } from './treasury-format';
|
import { toman } from './treasury-format';
|
||||||
|
|
||||||
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
|
const { Text: ATypographyText } = Typography;
|
||||||
const AListItem = AList.Item;
|
const AListItem = AList.Item;
|
||||||
const AListItemMeta = AList.Item.Meta;
|
const AListItemMeta = AList.Item.Meta;
|
||||||
|
|
||||||
@@ -100,23 +99,14 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-flex vertical :gap="16">
|
||||||
<a-flex vertical :gap="16">
|
<a-flex justify="flex-end">
|
||||||
<a-flex justify="space-between" align="center" wrap="wrap" :gap="12">
|
|
||||||
<div>
|
|
||||||
<a-typography-title :level="3" :style="{ margin: 0 }">گزارش مالی</a-typography-title>
|
|
||||||
<a-typography-text type="secondary">
|
|
||||||
سود عملیاتی از درآمد و هزینه ثبتشده است، نه از خالص وجه.
|
|
||||||
</a-typography-text>
|
|
||||||
</div>
|
|
||||||
<a-button @click="download">
|
<a-button @click="download">
|
||||||
<template #icon><DownloadOutlined /></template>
|
<template #icon><DownloadOutlined /></template>
|
||||||
خروجی اکسل
|
خروجی اکسل
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
|
|
||||||
<TreasuryNav />
|
|
||||||
|
|
||||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||||
|
|
||||||
<a-card :bordered="false">
|
<a-card :bordered="false">
|
||||||
@@ -179,6 +169,5 @@ onMounted(() => {
|
|||||||
</a-list>
|
</a-list>
|
||||||
</template>
|
</template>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { inject, provide, ref, type InjectionKey, type Ref } from 'vue';
|
||||||
|
|
||||||
|
export type TreasuryChromeOverride = {
|
||||||
|
title: string | null;
|
||||||
|
description: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TreasuryChromeApi = {
|
||||||
|
override: Ref<TreasuryChromeOverride>;
|
||||||
|
set: (next: TreasuryChromeOverride) => void;
|
||||||
|
clear: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY: InjectionKey<TreasuryChromeApi> = Symbol('treasuryPageChrome');
|
||||||
|
|
||||||
|
export function provideTreasuryPageChrome(): TreasuryChromeApi {
|
||||||
|
const override = ref<TreasuryChromeOverride>({ title: null, description: null });
|
||||||
|
const api: TreasuryChromeApi = {
|
||||||
|
override,
|
||||||
|
set(next) {
|
||||||
|
override.value = next;
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
override.value = { title: null, description: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
provide(KEY, api);
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTreasuryPageChrome(): TreasuryChromeApi | null {
|
||||||
|
return inject(KEY, null);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user