feat: add messaging toggles, password reset UI, and payment discounts

Add notification channel toggles to settings, password reset SMS action on user form, and discount/payable amount display on payment views.
This commit is contained in:
2026-08-16 06:58:09 +03:30
parent 5fe7eb090e
commit 92eb394466
8 changed files with 361 additions and 21 deletions
+38 -4
View File
@@ -27,7 +27,15 @@
</div>
<div>
<span class="text-muted text-xs block mb-1">مبلغ کل صورتحساب</span>
<span class="font-bold text-color text-xl">{{ toPersianDigits(payment.amount?.toLocaleString()) }} تومان</span>
<span class="font-bold text-color text-xl">{{ toPersianDigits((payment.amount || 0).toLocaleString()) }} تومان</span>
</div>
<div v-if="payment.discount">
<span class="text-muted text-xs block mb-1">تخفیف</span>
<span class="font-semibold text-orange-500 text-lg">{{ toPersianDigits((payment.discount || 0).toLocaleString()) }} تومان</span>
</div>
<div>
<span class="text-muted text-xs block mb-1">مبلغ قابل پرداخت</span>
<span class="font-bold text-color text-xl">{{ toPersianDigits(payableAmount.toLocaleString()) }} تومان</span>
</div>
<div>
<span class="text-muted text-xs block mb-1">مبلغ کل دریافتی</span>
@@ -35,12 +43,16 @@
</div>
<div>
<span class="text-muted text-xs block mb-1">باقیمانده</span>
<span class="font-bold text-red-500 text-lg">{{ toPersianDigits((payment.amount - (payment.paidAmount || 0)).toLocaleString()) }} تومان</span>
<span class="font-bold text-red-500 text-lg">{{ toPersianDigits(remainingAmount.toLocaleString()) }} تومان</span>
</div>
<div>
<span class="text-muted text-xs block mb-1">وضعیت پرداخت</span>
<StatusTag :status="payment.status" type="payment" />
</div>
<div>
<span class="text-muted text-xs block mb-1">یادداشت</span>
<p class="text-sm text-color m-0 white-space-pre-wrap line-height-3">{{ payment.notes || '—' }}</p>
</div>
</div>
</div>
</div>
@@ -76,6 +88,11 @@
{{ formatJalali(data.createdAt || data.date) }}
</template>
</Column>
<Column field="notes" header="یادداشت">
<template #body="{ data }">
<span class="white-space-pre-wrap">{{ data.notes || '—' }}</span>
</template>
</Column>
</DataTable>
</div>
</div>
@@ -96,6 +113,16 @@
<label class="font-semibold text-sm">شماره فیش / پیگیری *</label>
<InputText v-model.trim="trxForm.receiptNumber" class="w-full text-sm" dir="ltr" />
</div>
<div class="flex flex-column gap-2">
<label class="font-semibold text-sm" for="trx-notes">یادداشت</label>
<Textarea
id="trx-notes"
v-model="trxForm.notes"
rows="3"
class="w-full text-sm"
placeholder="یادداشت داخلی درباره این تراکنش…"
/>
</div>
</div>
<template #footer>
<Button label="انصراف" text severity="secondary" @click="showTransactionModal = false" />
@@ -106,11 +133,12 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { paymentApi } from '@/api/paymentApi';
import { usePersianDate } from '@/composables/usePersianDate';
import { useToast } from '@/composables/useToast';
import { getPayableAmount } from '@/utils/paymentAmount';
import PageHeader from '@/components/common/PageHeader.vue';
import PermissionGate from '@/components/common/PermissionGate.vue';
import StatusTag from '@/components/common/StatusTag.vue';
@@ -122,6 +150,7 @@ import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/select';
import InputText from 'primevue/inputtext';
import InputNumber from 'primevue/inputnumber';
import Textarea from 'primevue/textarea';
const route = useRoute();
const paymentId = route.params.id;
@@ -141,9 +170,13 @@ const methodOptions = [
const trxForm = reactive({
amount: 500000,
method: 'card',
receiptNumber: ''
receiptNumber: '',
notes: ''
});
const payableAmount = computed(() => getPayableAmount(payment.value || {}));
const remainingAmount = computed(() => Math.max(0, payableAmount.value - (payment.value?.paidAmount || 0)));
const fetchPayment = async () => {
try {
const res = await paymentApi.getOne(paymentId);
@@ -162,6 +195,7 @@ const handleRecordTransaction = async () => {
await paymentApi.recordTransaction(paymentId, trxForm);
showSuccess('تراکنش جدید با موفقیت ثبت شد');
showTransactionModal.value = false;
trxForm.notes = '';
fetchPayment();
} catch (err) {
showError(err);
+61 -5
View File
@@ -39,7 +39,10 @@
<Column field="amount" header="مبلغ کل (تومان)" sortable>
<template #body="{ data }">
{{ toPersianDigits((data.amount || 0).toLocaleString()) }} تومان
<div>{{ toPersianDigits(getPayableAmount(data).toLocaleString()) }} تومان</div>
<small v-if="data.discount" class="text-muted text-xs">
تخفیف {{ toPersianDigits((data.discount || 0).toLocaleString()) }} تومان
</small>
</template>
</Column>
@@ -77,7 +80,7 @@
</DataTableWrapper>
<!-- Create Payment Modal -->
<Dialog v-model:visible="showCreateModal" header="ایجاد صورتحساب جدید" modal :style="{ width: '480px' }">
<Dialog v-model:visible="showCreateModal" header="ایجاد صورتحساب جدید" modal :style="{ width: '520px' }">
<div class="flex flex-column gap-3 py-2">
<div class="flex flex-column gap-2">
<label class="font-semibold text-sm">انتخاب یک یا چند کلاس *</label>
@@ -112,8 +115,28 @@
</div>
<div class="flex flex-column gap-2">
<label class="font-semibold text-sm">مبلغ کل صورتحساب (تومان) *</label>
<InputNumber v-model="createForm.amount" class="w-full text-sm" suffix=" تومان" />
<label class="font-semibold text-sm" for="invoice-amount">مبلغ کل صورتحساب (تومان) *</label>
<InputNumber inputId="invoice-amount" v-model="createForm.amount" class="w-full text-sm" suffix=" تومان" :min="0" />
</div>
<div class="flex align-items-center gap-2">
<Checkbox v-model="hasDiscount" binary inputId="invoice-discount" />
<label for="invoice-discount" class="font-semibold text-sm cursor-pointer">تخفیف ؟</label>
</div>
<div v-if="hasDiscount" class="flex flex-column gap-2">
<label class="font-semibold text-sm" for="invoice-discount-amount">مبلغ تخفیف (تومان)</label>
<InputNumber
inputId="invoice-discount-amount"
v-model="createForm.discount"
class="w-full text-sm"
suffix=" تومان"
:min="0"
:max="createForm.amount || 0"
/>
<small class="font-semibold text-color">
مبلغ نهایی: {{ toPersianDigits(createPayableAmount.toLocaleString()) }} تومان
</small>
</div>
<div class="flex flex-column gap-2">
@@ -121,6 +144,17 @@
<DatePicker v-model="createForm.dueDate" class="w-full text-sm" placeholder="1403/01/01" />
</div>
<div class="flex flex-column gap-2">
<label class="font-semibold text-sm" for="invoice-notes">یادداشت</label>
<Textarea
id="invoice-notes"
v-model="createForm.notes"
rows="3"
class="w-full text-sm"
placeholder="یادداشت داخلی درباره این صورتحساب…"
/>
</div>
<NotifyChannelsField :notify="createNotify" />
</div>
<template #footer>
@@ -151,6 +185,7 @@ import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue';
import PermissionGate from '@/components/common/PermissionGate.vue';
import StatusTag from '@/components/common/StatusTag.vue';
import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue';
import { getPayableAmount } from '@/utils/paymentAmount';
import Button from 'primevue/button';
import Column from 'primevue/column';
import Tag from 'primevue/tag';
@@ -158,6 +193,8 @@ import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/select';
import MultiSelect from 'primevue/multiselect';
import InputNumber from 'primevue/inputnumber';
import Checkbox from 'primevue/checkbox';
import Textarea from 'primevue/textarea';
import DatePicker from 'vue3-persian-datetime-picker';
const { toPersianDigits, formatJalali } = usePersianDate();
@@ -183,9 +220,16 @@ const createForm = reactive({
user: null,
classes: [],
amount: 0,
discount: 0,
notes: '',
dueDate: ''
});
const hasDiscount = ref(false);
const createNotify = reactive({ sms: true, email: true, bot: true });
const createPayableAmount = computed(() => getPayableAmount({
amount: createForm.amount,
discount: hasDiscount.value ? createForm.discount : 0
}));
const deleteDialogVisible = ref(false);
const selectedPayment = ref(null);
@@ -241,7 +285,10 @@ const resetCreateForm = () => {
createForm.user = null;
createForm.classes = [];
createForm.amount = 0;
createForm.discount = 0;
createForm.notes = '';
createForm.dueDate = '';
hasDiscount.value = false;
createNotify.sms = true;
createNotify.email = true;
createNotify.bot = true;
@@ -275,9 +322,18 @@ const handleCreatePayment = async () => {
if (!createForm.amount) { showError('لطفا مبلغ را وارد کنید'); return; }
if (!createForm.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
if (hasDiscount.value && (createForm.discount || 0) > createForm.amount) {
showError('مبلغ تخفیف نمی‌تواند بیشتر از مبلغ کل باشد');
return;
}
isCreating.value = true;
try {
await paymentApi.create({ ...createForm, notify: { ...createNotify } });
await paymentApi.create({
...createForm,
discount: hasDiscount.value ? (createForm.discount || 0) : 0,
notify: { ...createNotify }
});
showSuccess('صورتحساب جدید با موفقیت ایجاد شد');
showCreateModal.value = false;
loadData();