Files
CRM_Montzar/apps/web/src/views/treasury/TreasuryRecurringView.vue
T
MahdiandCursor 5ef693990c feat(finance): edit payroll with month ranges, reasons, and toman
Let salary and payments be corrected after they are saved, pick a Jalali period as a month range, and show toman on finance amounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-08 16:53:45 +03:30

169 lines
5.8 KiB
Vue

<script setup lang="ts">
import { onMounted, ref } from 'vue';
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,
Typography,
message,
} from 'ant-design-vue';
import { api } from '@/lib/api';
import MoneyInput from '@/components/MoneyInput.vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import { toman } from './treasury-format';
import { formatJalaliNumeric } from '@/lib/jalali';
import { parseMoneyNumber } from '@/lib/money';
const { Text: ATypographyText } = Typography;
const AListItem = AList.Item;
const AListItemMeta = AList.Item.Meta;
const ASelectOption = ASelect.Option;
type Row = {
id: string;
title: string;
kind: string;
amount: string;
interval: string;
nextDue: string;
isActive: boolean;
};
const rows = ref<Row[]>([]);
const error = ref('');
const ok = ref('');
const saving = ref(false);
const form = ref({ title: '', kind: 'expense', amount: '', interval: 'monthly', startDate: '' });
async function load() {
const { data } = await api.get<Row[]>('/treasury/recurring');
rows.value = data;
}
function onFinishFailed() {
message.warning('لطفاً فیلدهای الزامی را کامل کنید.');
}
async function createItem() {
error.value = '';
ok.value = '';
saving.value = true;
try {
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 = 'الگو ثبت شد. در سررسید فقط سند باز ساخته می‌شود.';
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'ثبت ناموفق بود.')
: 'ثبت ناموفق بود.';
} finally {
saving.value = false;
}
}
onMounted(() => {
void load();
});
</script>
<template>
<a-flex vertical :gap="16">
<a-alert v-if="error" type="error" show-icon :message="error" />
<a-alert v-if="ok" type="success" show-icon :message="ok" />
<a-card :bordered="false">
<a-divider orientation="right">الگوی جدید</a-divider>
<a-form
layout="vertical"
:model="form"
@finish="createItem"
@finishFailed="onFinishFailed"
>
<a-row :gutter="[16, 0]">
<a-col :xs="24" :md="12">
<a-form-item
label="عنوان"
name="title"
:rules="[{ required: true, message: 'عنوان الزامی است' }]"
>
<a-input v-model:value="form.title" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="نوع" name="kind" :rules="[{ required: true }]">
<a-select show-search option-filter-prop="label" v-model:value="form.kind" style="width: 100%">
<a-select-option value="expense" label="هزینه">هزینه</a-select-option>
<a-select-option value="income" label="درآمد">درآمد</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item
label="مبلغ"
name="amount"
:rules="[{ required: true, message: 'مبلغ الزامی است' }]"
>
<MoneyInput addon-after="تومان" v-model="form.amount" :decimals="0" />
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="دوره" name="interval" :rules="[{ required: true }]">
<a-select show-search option-filter-prop="label" v-model:value="form.interval" style="width: 100%">
<a-select-option value="monthly" label="ماهانه">ماهانه</a-select-option>
<a-select-option value="weekly" label="هفتگی">هفتگی</a-select-option>
<a-select-option value="yearly" label="سالانه">سالانه</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :xs="24" :md="8">
<a-form-item label="شروع" name="startDate">
<JalaliDateInput v-model="form.startDate" />
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" html-type="submit" :loading="saving">ثبت الگو</a-button>
</a-form>
<a-divider orientation="right">فهرست الگوها</a-divider>
<a-empty v-if="!rows.length" description="الگویی ثبت نشده است." />
<a-list v-else item-layout="horizontal" :data-source="rows">
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a-typography-text strong>{{ toman(item.amount) }}</a-typography-text>
</template>
<a-list-item-meta>
<template #title>{{ item.title }}</template>
<template #description>
{{ item.kind === 'income' ? 'درآمد' : 'هزینه' }}
· {{ item.interval }}
· بعدی {{ formatJalaliNumeric(new Date(item.nextDue)) }}
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-flex>
</template>