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:
@@ -7,6 +7,11 @@ export const userApi = {
|
||||
getOne: (id) => axiosInstance.get(`/users/admin/get-one/${id}`),
|
||||
create: (data) => axiosInstance.post('/users/admin/create', data),
|
||||
update: (id, data) => axiosInstance.put(`/users/admin/update/${id}`, data),
|
||||
resetPasswordAndSms: (id) => axiosInstance.post(
|
||||
`/users/admin/${id}/reset-password-sms`,
|
||||
{},
|
||||
{ timeout: 30000 }
|
||||
),
|
||||
delete: (id) => axiosInstance.delete(`/users/admin/delete/${id}`),
|
||||
enroll: (userId, data) => axiosInstance.post(`/users/admin/${userId}/enroll`, data)
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const getPayableAmount = (payment = {}) => {
|
||||
const amount = Number(payment.amount);
|
||||
const discount = Number(payment.discount);
|
||||
const safeAmount = Number.isFinite(amount) && amount > 0 ? amount : 0;
|
||||
const safeDiscount = Number.isFinite(discount) && discount > 0 ? discount : 0;
|
||||
return Math.max(0, safeAmount - Math.min(safeDiscount, safeAmount));
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { getPayableAmount } from './paymentAmount.js';
|
||||
|
||||
describe('getPayableAmount', () => {
|
||||
it('returns the original amount when there is no discount', () => {
|
||||
assert.equal(getPayableAmount({ amount: 1_000_000 }), 1_000_000);
|
||||
assert.equal(getPayableAmount({ amount: 1_000_000, discount: 0 }), 1_000_000);
|
||||
});
|
||||
|
||||
it('subtracts a discount from the total', () => {
|
||||
assert.equal(getPayableAmount({ amount: 1_000_000, discount: 150_000 }), 850_000);
|
||||
});
|
||||
|
||||
it('never returns a negative payable amount', () => {
|
||||
assert.equal(getPayableAmount({ amount: 100, discount: 250 }), 0);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3,9 +3,76 @@
|
||||
<div class="settings-view w-full max-w-4xl mx-auto">
|
||||
<PageHeader
|
||||
title="تنظیمات سیستم"
|
||||
subtitle="قالبهای پیامک، راهاندازی پایگاه داده و وارد کردن دادهها"
|
||||
subtitle="کانالهای اطلاعرسانی، قالبهای پیامک، راهاندازی پایگاه داده و وارد کردن دادهها"
|
||||
/>
|
||||
|
||||
<div class="surface-card p-4 sm:p-5 border-round-xl border-1 border-color shadow-sm mb-4">
|
||||
<div class="flex align-items-start gap-3 mb-4">
|
||||
<div class="w-3rem h-3rem border-round-xl flex align-items-center justify-content-center flex-shrink-0 bg-primary-light text-primary">
|
||||
<i class="pi pi-bell text-xl"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<h2 class="text-lg font-bold text-color m-0 mb-1">کانالهای اطلاعرسانی</h2>
|
||||
<p class="text-sm text-muted m-0 line-height-3">
|
||||
ارسال پیامک، ایمیل و پیام ربات را از داشبورد فعال یا غیرفعال کنید. هر کانال فقط وقتی ارسال میشود که هم اینجا و هم متغیر محیطی سرور فعال باشد.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isSettingsLoading" class="flex align-items-center gap-2 text-muted text-sm">
|
||||
<i class="pi pi-spin pi-spinner"></i>
|
||||
<span>در حال بارگذاری تنظیمات کانالها…</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-column gap-3">
|
||||
<div
|
||||
v-for="channel in messagingChannels"
|
||||
:key="channel.key"
|
||||
class="p-4 border-round-lg surface-ground border-1 border-color flex flex-column sm:flex-row sm:align-items-center justify-content-between gap-3"
|
||||
>
|
||||
<div class="flex align-items-start gap-3">
|
||||
<div class="w-2rem h-2rem border-round-lg flex align-items-center justify-content-center flex-shrink-0 bg-primary-light text-primary">
|
||||
<i :class="channel.icon"></i>
|
||||
</div>
|
||||
<div>
|
||||
<label class="font-bold text-base text-color cursor-pointer" :for="`messaging-${channel.key}`">
|
||||
{{ channel.label }}
|
||||
</label>
|
||||
<p class="text-xs text-muted m-0 mt-1 line-height-3">
|
||||
متغیر محیطی: <code dir="ltr">{{ channel.envName }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center gap-3 flex-shrink-0">
|
||||
<Tag
|
||||
:value="channelStatus(channel.key).label"
|
||||
:severity="channelStatus(channel.key).severity"
|
||||
class="text-xs"
|
||||
/>
|
||||
<InputSwitch
|
||||
:inputId="`messaging-${channel.key}`"
|
||||
v-model="messagingForm[channel.flag]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-muted m-0 line-height-3">
|
||||
اگر وضعیت «غیرفعال در سرور» باشد، روشن کردن سوییچ داشبورد بهتنهایی کافی نیست و باید متغیر محیطی مربوط هم فعال شود.
|
||||
</p>
|
||||
|
||||
<div class="flex justify-content-end">
|
||||
<Button
|
||||
label="ذخیره کانالها"
|
||||
icon="pi pi-save"
|
||||
class="font-bold"
|
||||
:loading="isSavingMessaging"
|
||||
@click="saveMessaging"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="surface-card p-4 sm:p-5 border-round-xl border-1 border-color shadow-sm mb-4">
|
||||
<div class="flex align-items-start gap-3 mb-4">
|
||||
<div class="w-3rem h-3rem border-round-xl flex align-items-center justify-content-center flex-shrink-0 bg-primary-light text-primary">
|
||||
@@ -253,6 +320,7 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import Button from 'primevue/button';
|
||||
import InputSwitch from 'primevue/toggleswitch';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import Tag from 'primevue/tag';
|
||||
@@ -266,8 +334,34 @@ const { showSuccess, showError } = useToast();
|
||||
|
||||
const isSettingsLoading = ref(true);
|
||||
const isSavingSettings = ref(false);
|
||||
const isSavingMessaging = ref(false);
|
||||
const smsTemplates = ref([]);
|
||||
const templateForm = ref({});
|
||||
const messagingForm = ref({
|
||||
smsEnabled: true,
|
||||
emailEnabled: true,
|
||||
botEnabled: true
|
||||
});
|
||||
const messagingEnv = ref({
|
||||
smsEnabled: false,
|
||||
emailEnabled: true,
|
||||
botEnabled: true
|
||||
});
|
||||
|
||||
const messagingChannels = [
|
||||
{ key: 'sms', flag: 'smsEnabled', label: 'پیامک', icon: 'pi pi-mobile', envName: 'SMS_ENABLED' },
|
||||
{ key: 'email', flag: 'emailEnabled', label: 'ایمیل', icon: 'pi pi-envelope', envName: 'EMAIL_ENABLED' },
|
||||
{ key: 'bot', flag: 'botEnabled', label: 'پیام ربات', icon: 'pi pi-comments', envName: 'BOT_ENABLED' }
|
||||
];
|
||||
|
||||
const channelStatus = (channelKey) => {
|
||||
const flag = `${channelKey}Enabled`;
|
||||
const dbOn = messagingForm.value[flag] !== false;
|
||||
const envOn = Boolean(messagingEnv.value[flag]);
|
||||
if (envOn && dbOn) return { label: 'ارسال فعال', severity: 'success' };
|
||||
if (!envOn) return { label: 'غیرفعال در سرور', severity: 'warn' };
|
||||
return { label: 'غیرفعال از داشبورد', severity: 'secondary' };
|
||||
};
|
||||
|
||||
const isStatusLoading = ref(true);
|
||||
const isSeeding = ref(false);
|
||||
@@ -485,11 +579,31 @@ const applySmsTemplates = (payload) => {
|
||||
templateForm.value = newForm;
|
||||
};
|
||||
|
||||
const applyMessaging = (payload) => {
|
||||
const data = payload?.data?.data || payload?.data || payload || {};
|
||||
const messaging = data.messaging || {};
|
||||
messagingForm.value = {
|
||||
smsEnabled: messaging.smsEnabled !== false,
|
||||
emailEnabled: messaging.emailEnabled !== false,
|
||||
botEnabled: messaging.botEnabled !== false
|
||||
};
|
||||
messagingEnv.value = {
|
||||
smsEnabled: Boolean(messaging.env?.smsEnabled),
|
||||
emailEnabled: Boolean(messaging.env?.emailEnabled),
|
||||
botEnabled: Boolean(messaging.env?.botEnabled)
|
||||
};
|
||||
};
|
||||
|
||||
const applySettings = (payload) => {
|
||||
applySmsTemplates(payload);
|
||||
applyMessaging(payload);
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
isSettingsLoading.value = true;
|
||||
try {
|
||||
const response = await settingsApi.get();
|
||||
applySmsTemplates(response);
|
||||
applySettings(response);
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
@@ -497,6 +611,25 @@ const loadSettings = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const saveMessaging = async () => {
|
||||
isSavingMessaging.value = true;
|
||||
try {
|
||||
const response = await settingsApi.save({
|
||||
messaging: {
|
||||
smsEnabled: Boolean(messagingForm.value.smsEnabled),
|
||||
emailEnabled: Boolean(messagingForm.value.emailEnabled),
|
||||
botEnabled: Boolean(messagingForm.value.botEnabled)
|
||||
}
|
||||
});
|
||||
applySettings(response);
|
||||
showSuccess('تنظیمات کانالهای اطلاعرسانی با موفقیت ذخیره شد.');
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isSavingMessaging.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveSmsTemplates = async () => {
|
||||
isSavingSettings.value = true;
|
||||
try {
|
||||
@@ -521,7 +654,7 @@ const saveSmsTemplates = async () => {
|
||||
};
|
||||
});
|
||||
const response = await settingsApi.save({ smsTemplates: smsTemplatesPayload });
|
||||
applySmsTemplates(response);
|
||||
applySettings(response);
|
||||
showSuccess('قالبهای پیامک با موفقیت ذخیره شدند.');
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
|
||||
@@ -123,6 +123,24 @@
|
||||
<InputText v-model="form.password" type="password" class="w-full text-sm" dir="ltr" placeholder="در صورت عدم تغییر خالی بگذارید" />
|
||||
</div>
|
||||
|
||||
<div v-if="isEditMode" class="col-12">
|
||||
<div class="flex flex-column sm:flex-row sm:align-items-center sm:justify-content-between gap-3 surface-ground p-3 border-round">
|
||||
<p class="text-sm text-color-secondary m-0 line-height-3">
|
||||
رمز عبور را بازنشانی کنید و نام کاربری و رمز جدید را به شماره همراه کاربر پیامک کنید. نشستهای فعال او بسته میشود.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
label="بازنشانی رمز و ارسال پیامک"
|
||||
icon="pi pi-send"
|
||||
severity="warning"
|
||||
outlined
|
||||
:loading="isResettingPassword"
|
||||
:disabled="!form.phone"
|
||||
@click="confirmResetPasswordSms"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isEditMode" class="col-12">
|
||||
<NotifyChannelsField :notify="createNotify" />
|
||||
</div>
|
||||
@@ -147,12 +165,12 @@
|
||||
<Dialog
|
||||
v-model:visible="credentialsVisible"
|
||||
modal
|
||||
header="حساب ساخته شد"
|
||||
:header="credentialsDialog.header"
|
||||
:style="{ width: '28rem' }"
|
||||
:closable="false"
|
||||
>
|
||||
<p class="text-sm mb-3 line-height-3">
|
||||
اطلاعات ورود ساخته شد و در صورت فعال بودن پیامک، برای کاربر ارسال میشود. این رمز فقط یکبار نمایش داده میشود:
|
||||
{{ credentialsDialog.message }}
|
||||
</p>
|
||||
<div class="flex flex-column gap-2 surface-ground p-3 border-round">
|
||||
<div class="flex justify-content-between gap-2">
|
||||
@@ -177,6 +195,7 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { userApi } from '@/api/userApi';
|
||||
import { roleApi } from '@/api/roleApi';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import AdminNotesField from '@/components/common/AdminNotesField.vue';
|
||||
import UserFilesSection from '@/components/uploader/UserFilesSection.vue';
|
||||
@@ -189,17 +208,33 @@ import Dialog from 'primevue/dialog';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { showSuccess, showError } = useToast();
|
||||
const confirm = useConfirm();
|
||||
const { showSuccess, showError, showWarn } = useToast();
|
||||
|
||||
const userId = route.params.id;
|
||||
const isEditMode = computed(() => !!userId);
|
||||
const isSubmitting = ref(false);
|
||||
const isResettingPassword = ref(false);
|
||||
const filesSection = ref(null);
|
||||
const roles = ref([]);
|
||||
const credentialsVisible = ref(false);
|
||||
const credentialsMode = ref('create');
|
||||
const createdCredentials = reactive({ username: '', password: '' });
|
||||
const createNotify = reactive({ sms: true, email: true, bot: true });
|
||||
|
||||
const credentialsDialog = computed(() => {
|
||||
if (credentialsMode.value === 'reset') {
|
||||
return {
|
||||
header: 'رمز عبور بازنشانی شد',
|
||||
message: 'رمز جدید ساخته شد و در صورت موفقیت پیامک، برای کاربر ارسال میشود. این رمز فقط یکبار نمایش داده میشود:'
|
||||
};
|
||||
}
|
||||
return {
|
||||
header: 'حساب ساخته شد',
|
||||
message: 'اطلاعات ورود ساخته شد و در صورت فعال بودن پیامک، برای کاربر ارسال میشود. این رمز فقط یکبار نمایش داده میشود:'
|
||||
};
|
||||
});
|
||||
|
||||
const messengerOptions = [
|
||||
{ label: 'پیامک (SMS)', value: 'SMS' },
|
||||
{ label: 'بله', value: 'Bale' },
|
||||
@@ -324,9 +359,62 @@ const fetchUser = async () => {
|
||||
|
||||
const finishAfterCreate = () => {
|
||||
credentialsVisible.value = false;
|
||||
if (credentialsMode.value === 'reset') {
|
||||
form.password = '';
|
||||
return;
|
||||
}
|
||||
router.push('/users');
|
||||
};
|
||||
|
||||
const showGeneratedCredentials = (creds = {}, mode = 'create') => {
|
||||
createdCredentials.username = creds.username || '';
|
||||
createdCredentials.password = creds.password || '';
|
||||
if (!createdCredentials.username || !createdCredentials.password) return false;
|
||||
credentialsMode.value = mode;
|
||||
credentialsVisible.value = true;
|
||||
return true;
|
||||
};
|
||||
|
||||
const confirmResetPasswordSms = () => {
|
||||
if (!form.phone) {
|
||||
showError('شماره همراه کاربر برای ارسال پیامک الزامی است');
|
||||
return;
|
||||
}
|
||||
|
||||
confirm.require({
|
||||
header: 'بازنشانی رمز عبور',
|
||||
message: `رمز عبور فعلی باطل میشود و نام کاربری بههمراه رمز جدید به شماره ${form.phone} پیامک میگردد. نشستهای فعال کاربر بسته خواهد شد. ادامه میدهید؟`,
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptLabel: 'بازنشانی و ارسال',
|
||||
rejectLabel: 'انصراف',
|
||||
acceptClass: 'p-button-warning',
|
||||
accept: () => {
|
||||
resetPasswordAndSendSms();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const resetPasswordAndSendSms = async () => {
|
||||
isResettingPassword.value = true;
|
||||
try {
|
||||
const res = await userApi.resetPasswordAndSms(userId);
|
||||
const data = res.data || res;
|
||||
const creds = data.generatedCredentials || {};
|
||||
if (data.smsSent) {
|
||||
showSuccess('رمز عبور بازنشانی شد و پیامک اطلاعات ورود ارسال گردید');
|
||||
} else {
|
||||
showWarn('رمز عبور بازنشانی شد، اما ارسال پیامک انجام نشد. رمز جدید را از پنجره زیر یادداشت کنید.');
|
||||
}
|
||||
if (!showGeneratedCredentials(creds, 'reset')) {
|
||||
showError('رمز جدید ساخته شد اما برای نمایش در دسترس نیست');
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isResettingPassword.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name) {
|
||||
showError('نام الزامی است');
|
||||
@@ -351,12 +439,11 @@ const handleSubmit = async () => {
|
||||
const res = await userApi.create({ ...payload, notify: { ...createNotify } });
|
||||
const data = res.data || res;
|
||||
const creds = data.generatedCredentials || {};
|
||||
createdCredentials.username = creds.username || data.username || '';
|
||||
createdCredentials.password = creds.password || '';
|
||||
showSuccess('کاربر جدید با موفقیت ایجاد شد');
|
||||
if (createdCredentials.username && createdCredentials.password) {
|
||||
credentialsVisible.value = true;
|
||||
} else {
|
||||
if (!showGeneratedCredentials({
|
||||
username: creds.username || data.username || '',
|
||||
password: creds.password || ''
|
||||
}, 'create')) {
|
||||
router.push('/users');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user