Files
CRM_Montzar/apps/web/src/views/treasury/TreasuryAccountsView.vue
T

315 lines
11 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { RouterLink } from 'vue-router';
import axios from 'axios';
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,
List as AList,
Row as ARow,
Select as ASelect,
Space as ASpace,
Statistic as AStatistic,
Tag as ATag,
Typography,
} from 'ant-design-vue';
import { api } from '@/lib/api';
import type { FinancialAccountDto, FinancialAccountType } from '@mento/shared';
import TreasuryNav from '@/components/treasury/TreasuryNav.vue';
import { toman } from './treasury-format';
const { Title: ATypographyTitle, Text: ATypographyText } = Typography;
const AListItem = AList.Item;
const AListItemMeta = AList.Item.Meta;
const ASelectOption = ASelect.Option;
const accounts = ref<FinancialAccountDto[]>([]);
const error = ref('');
const ok = ref('');
const openForm = ref(false);
const saving = ref(false);
const transferOpen = ref(false);
const form = ref({
name: '',
type: 'cash' as FinancialAccountType,
bankName: '',
accountRef: '',
responsibleName: '',
openingBalance: '',
openingDate: '',
notes: '',
});
const transfer = ref({
fromAccountId: '',
toAccountId: '',
amount: '',
memo: '',
});
const active = computed(() => accounts.value.filter((row) => row.isActive));
const total = computed(() =>
active.value.reduce((sum, row) => sum + Number(row.balance), 0),
);
const periodIn = computed(() =>
active.value.reduce((sum, row) => sum + Number(row.periodIn), 0),
);
const periodOut = computed(() =>
active.value.reduce((sum, row) => sum + Number(row.periodOut), 0),
);
const typeLabel: Record<string, string> = {
bank: 'حساب بانکی',
cash: 'صندوق نقدی',
petty: 'تنخواه',
};
function apiMessage(e: unknown) {
if (!axios.isAxiosError(e)) return 'عملیات ناموفق بود.';
const msg = (e.response?.data as { message?: string | string[] })?.message;
return Array.isArray(msg) ? msg.join('، ') : msg || 'عملیات ناموفق بود.';
}
async function load() {
const { data } = await api.get<FinancialAccountDto[]>('/treasury/accounts');
accounts.value = data;
}
async function createAccount() {
saving.value = true;
error.value = '';
ok.value = '';
try {
await api.post('/treasury/accounts', {
...form.value,
openingBalance: form.value.openingBalance || 0,
openingDate: form.value.openingDate || undefined,
});
openForm.value = false;
form.value = {
name: '',
type: 'cash',
bankName: '',
accountRef: '',
responsibleName: '',
openingBalance: '',
openingDate: '',
notes: '',
};
ok.value = 'حساب ثبت شد. موجودی اولیه درآمد دوره نیست.';
await load();
} catch (e) {
error.value = apiMessage(e);
} finally {
saving.value = false;
}
}
async function sendTransfer() {
saving.value = true;
error.value = '';
ok.value = '';
try {
await api.post('/treasury/transfers', {
...transfer.value,
clientKey: crypto.randomUUID(),
});
transferOpen.value = false;
ok.value = 'انتقال ثبت شد و در درآمد یا هزینه نیامده است.';
await load();
} catch (e) {
error.value = apiMessage(e);
} finally {
saving.value = false;
}
}
onMounted(() => {
void load().catch(() => {
error.value = 'بارگذاری حساب‌ها ناموفق بود.';
});
});
</script>
<template>
<div>
<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="ok" type="success" show-icon :message="ok" />
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="12" :md="6">
<a-card :bordered="false">
<a-statistic title="مجموع موجودی حساب‌ها" :value="toman(total)" />
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-card :bordered="false">
<a-statistic
title="ورودی این فهرست"
:value="toman(periodIn)"
:value-style="{ color: 'var(--ant-color-success)' }"
/>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-card :bordered="false">
<a-statistic
title="خروجی این فهرست"
:value="toman(periodOut)"
:value-style="{ color: 'var(--ant-color-error)' }"
/>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :md="6">
<a-card :bordered="false">
<a-statistic title="حساب‌های فعال" :value="`${active.length} حساب`" />
</a-card>
</a-col>
</a-row>
<a-card :bordered="false">
<a-space>
<a-button type="primary" @click="openForm = !openForm">افزودن حساب جدید</a-button>
<a-button @click="transferOpen = !transferOpen">انتقال وجه</a-button>
</a-space>
<template v-if="openForm">
<a-divider orientation="right">حساب جدید</a-divider>
<a-form layout="vertical" @finish="createAccount">
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item label="نام حساب" name="name">
<a-input v-model:value="form.name" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="نوع" name="type">
<a-select v-model:value="form.type" style="width: 100%">
<a-select-option value="cash">صندوق نقدی</a-select-option>
<a-select-option value="bank">حساب بانکی</a-select-option>
<a-select-option value="petty">تنخواه</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-if="form.type === 'bank'" :xs="24" :md="8">
<a-form-item label="نام بانک" name="bankName">
<a-input v-model:value="form.bankName" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="شماره یا شناسه" name="accountRef">
<a-input v-model:value="form.accountRef" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="مسئول" name="responsibleName">
<a-input v-model:value="form.responsibleName" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="موجودی اولیه" name="openingBalance">
<a-input v-model:value="form.openingBalance" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="تاریخ شروع" name="openingDate">
<a-input v-model:value="form.openingDate" type="date" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="توضیحات" name="notes">
<a-input v-model:value="form.notes" />
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" html-type="submit" :loading="saving">ثبت حساب</a-button>
</a-form>
</template>
<template v-if="transferOpen">
<a-divider orientation="right">انتقال بین حساب‌ها</a-divider>
<a-form layout="vertical" @finish="sendTransfer">
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item label="از حساب" name="fromAccountId">
<a-select v-model:value="transfer.fromAccountId" style="width: 100%" placeholder="انتخاب">
<a-select-option value="">انتخاب</a-select-option>
<a-select-option v-for="account in active" :key="account.id" :value="account.id">
{{ account.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="به حساب" name="toAccountId">
<a-select v-model:value="transfer.toAccountId" style="width: 100%" placeholder="انتخاب">
<a-select-option value="">انتخاب</a-select-option>
<a-select-option v-for="account in active" :key="`to-${account.id}`" :value="account.id">
{{ account.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="مبلغ" name="amount">
<a-input v-model:value="transfer.amount" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="12">
<a-form-item label="شرح" name="memo">
<a-input v-model:value="transfer.memo" />
</a-form-item>
</a-col>
</a-row>
<a-typography-text type="secondary">
این انتقال درآمد یا هزینه نیست. اگر موجودی مبدأ کافی نباشد ثبت نمی‌شود.
</a-typography-text>
<div>
<a-button type="primary" html-type="submit" :loading="saving">تأیید انتقال</a-button>
</div>
</a-form>
</template>
<a-divider orientation="right">فهرست حساب‌ها</a-divider>
<a-empty v-if="!accounts.length" description="حسابی ثبت نشده است." />
<a-list v-else item-layout="horizontal" :data-source="accounts">
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a-typography-text strong>{{ toman(item.balance) }}</a-typography-text>
<RouterLink :to="`/finance/accounts/${item.id}`">جزئیات حساب</RouterLink>
</template>
<a-list-item-meta>
<template #title>
{{ item.name }}
<a-tag v-if="!item.isActive">غیرفعال</a-tag>
</template>
<template #description>
{{ typeLabel[item.type] }} · مشاهده گردش حساب و جزئیات
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-flex>
</div>
</template>