feat: improve bill flow, user columns, and admin notes UI
Select class before user when creating bills, fix national ID and registered classes display, remove course ratings, fix notification badge, and show Persian datetime in the topbar.
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
<!-- Reusable admin notes list (string[]) for users, classes, sessions -->
|
||||||
|
<template>
|
||||||
|
<div class="admin-notes-field flex flex-column gap-2">
|
||||||
|
<label v-if="label" class="font-semibold text-sm">{{ label }}</label>
|
||||||
|
<p v-if="hint" class="text-muted text-xs m-0">{{ hint }}</p>
|
||||||
|
|
||||||
|
<div v-for="(note, index) in model" :key="index" class="flex gap-2 align-items-start">
|
||||||
|
<Textarea
|
||||||
|
:modelValue="note"
|
||||||
|
rows="2"
|
||||||
|
class="w-full text-sm flex-grow-1"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@update:modelValue="updateNote(index, $event)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-trash"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
severity="danger"
|
||||||
|
:aria-label="'حذف یادداشت'"
|
||||||
|
@click="removeNote(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
label="افزودن یادداشت ادمین"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
class="align-self-start"
|
||||||
|
@click="addNote"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Textarea from 'primevue/textarea';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
|
||||||
|
const model = defineModel({ type: Array, default: () => [] });
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
label: { type: String, default: 'یادداشتهای ادمین' },
|
||||||
|
hint: {
|
||||||
|
type: String,
|
||||||
|
default: 'برای موارد غیرعادی یا نکات داخلی، یادداشت اضافه کنید'
|
||||||
|
},
|
||||||
|
placeholder: { type: String, default: 'یادداشت…' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const addNote = () => {
|
||||||
|
model.value = [...(model.value || []), ''];
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateNote = (index, value) => {
|
||||||
|
const next = [...(model.value || [])];
|
||||||
|
next[index] = value;
|
||||||
|
model.value = next;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNote = (index) => {
|
||||||
|
model.value = (model.value || []).filter((_, i) => i !== index);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -20,6 +20,10 @@
|
|||||||
<i class="pi pi-building text-2xl"></i>
|
<i class="pi pi-building text-2xl"></i>
|
||||||
<span>{{ $t('app.title') }}</span>
|
<span>{{ $t('app.title') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="persian-datetime hidden md:flex flex-column align-items-start line-height-2 mr-2">
|
||||||
|
<span class="text-sm font-semibold text-color">{{ persianDate }}</span>
|
||||||
|
<span class="text-xs text-color-secondary" dir="ltr">{{ persianTime }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex align-items-center gap-3">
|
<div class="flex align-items-center gap-3">
|
||||||
@@ -63,12 +67,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import moment from 'jalali-moment';
|
||||||
import { useAuthStore } from '@/stores/authStore';
|
import { useAuthStore } from '@/stores/authStore';
|
||||||
import { useThemeStore } from '@/stores/themeStore';
|
import { useThemeStore } from '@/stores/themeStore';
|
||||||
import { useUiStore } from '@/stores/uiStore';
|
import { useUiStore } from '@/stores/uiStore';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { usePersianDate } from '@/composables/usePersianDate';
|
||||||
|
import { notificationApi } from '@/api/notificationApi';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import Avatar from 'primevue/avatar';
|
import Avatar from 'primevue/avatar';
|
||||||
import Menu from 'primevue/menu';
|
import Menu from 'primevue/menu';
|
||||||
@@ -78,9 +85,30 @@ const themeStore = useThemeStore();
|
|||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { toPersianDigits } = usePersianDate();
|
||||||
|
|
||||||
const userMenu = ref(null);
|
const userMenu = ref(null);
|
||||||
const notificationCount = ref(3);
|
const notificationCount = ref(0);
|
||||||
|
const persianDate = ref('');
|
||||||
|
const persianTime = ref('');
|
||||||
|
let clockTimer = null;
|
||||||
|
|
||||||
|
const updateClock = () => {
|
||||||
|
const now = moment().locale('fa');
|
||||||
|
persianDate.value = toPersianDigits(now.format('dddd jD jMMMM jYYYY'));
|
||||||
|
persianTime.value = toPersianDigits(now.format('HH:mm:ss'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchNotificationCount = async () => {
|
||||||
|
try {
|
||||||
|
const res = await notificationApi.getAll({ limit: 1, page: 1 });
|
||||||
|
// axios interceptor returns body: { success, data, meta }
|
||||||
|
const total = res?.meta?.totalCount ?? 0;
|
||||||
|
notificationCount.value = Number(total) || 0;
|
||||||
|
} catch {
|
||||||
|
notificationCount.value = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleUserMenu = (event) => {
|
const toggleUserMenu = (event) => {
|
||||||
userMenu.value.toggle(event);
|
userMenu.value.toggle(event);
|
||||||
@@ -111,6 +139,16 @@ const menuItems = ref([
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateClock();
|
||||||
|
clockTimer = setInterval(updateClock, 1000);
|
||||||
|
fetchNotificationCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (clockTimer) clearInterval(clockTimer);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -121,6 +159,11 @@ const menuItems = ref([
|
|||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.persian-datetime {
|
||||||
|
padding-inline-start: 0.75rem;
|
||||||
|
border-inline-start: 1px solid var(--surface-border, #e2e8f0);
|
||||||
|
}
|
||||||
|
|
||||||
.notification-trigger {
|
.notification-trigger {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -55,6 +55,10 @@
|
|||||||
<label class="font-semibold text-sm">کلاس فعال است</label>
|
<label class="font-semibold text-sm">کلاس فعال است</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<AdminNotesField v-model="form.adminNotes" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 flex justify-content-end gap-2 mt-4 pt-3 border-top-1 border-color">
|
<div class="col-12 flex justify-content-end gap-2 mt-4 pt-3 border-top-1 border-color">
|
||||||
<Button label="انصراف" text severity="secondary" @click="goBack" />
|
<Button label="انصراف" text severity="secondary" @click="goBack" />
|
||||||
<Button type="submit" :label="$t('app.save')" icon="pi pi-check" :loading="isSubmitting" />
|
<Button type="submit" :label="$t('app.save')" icon="pi pi-check" :loading="isSubmitting" />
|
||||||
@@ -129,6 +133,7 @@ import { userApi } from '@/api/userApi';
|
|||||||
import { usePersianDate } from '@/composables/usePersianDate';
|
import { usePersianDate } from '@/composables/usePersianDate';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import PageHeader from '@/components/common/PageHeader.vue';
|
import PageHeader from '@/components/common/PageHeader.vue';
|
||||||
|
import AdminNotesField from '@/components/common/AdminNotesField.vue';
|
||||||
import ClassSessionsSection from '@/components/classes/ClassSessionsSection.vue';
|
import ClassSessionsSection from '@/components/classes/ClassSessionsSection.vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import InputNumber from 'primevue/inputnumber';
|
import InputNumber from 'primevue/inputnumber';
|
||||||
@@ -167,7 +172,8 @@ const form = reactive({
|
|||||||
tuitionFee: 0,
|
tuitionFee: 0,
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
adminNotes: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const availableUsers = computed(() => {
|
const availableUsers = computed(() => {
|
||||||
@@ -242,7 +248,8 @@ const fetchData = async () => {
|
|||||||
tuitionFee: data.tuitionFee || 0,
|
tuitionFee: data.tuitionFee || 0,
|
||||||
startDate: toJalaliDisplay(data.startDate),
|
startDate: toJalaliDisplay(data.startDate),
|
||||||
endDate: toJalaliDisplay(data.endDate),
|
endDate: toJalaliDisplay(data.endDate),
|
||||||
isActive: data.isActive !== false
|
isActive: data.isActive !== false,
|
||||||
|
adminNotes: Array.isArray(data.adminNotes) ? [...data.adminNotes] : []
|
||||||
});
|
});
|
||||||
students.value = data.students || [];
|
students.value = data.students || [];
|
||||||
if (data.course?.sectionCount) defaultSessionCount.value = data.course.sectionCount;
|
if (data.course?.sectionCount) defaultSessionCount.value = data.course.sectionCount;
|
||||||
@@ -277,7 +284,8 @@ const handleSubmit = async () => {
|
|||||||
tuitionFee: form.tuitionFee,
|
tuitionFee: form.tuitionFee,
|
||||||
startDate: toGregorianIso(form.startDate),
|
startDate: toGregorianIso(form.startDate),
|
||||||
endDate: toGregorianIso(form.endDate),
|
endDate: toGregorianIso(form.endDate),
|
||||||
isActive: form.isActive
|
isActive: form.isActive,
|
||||||
|
adminNotes: (form.adminNotes || []).map((n) => String(n).trim()).filter(Boolean)
|
||||||
};
|
};
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await classApi.update(classId, payload);
|
await classApi.update(classId, payload);
|
||||||
|
|||||||
@@ -39,12 +39,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<Column field="rating" header="امتیاز">
|
|
||||||
<template #body="{ data }">
|
|
||||||
<Rating :modelValue="data.rating || 5" readonly :cancel="false" />
|
|
||||||
</template>
|
|
||||||
</Column>
|
|
||||||
|
|
||||||
<Column field="isOfficial" header="مدرک رسمی">
|
<Column field="isOfficial" header="مدرک رسمی">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Tag :value="data.isOfficial ? $t('courses.official') : $t('courses.unofficial')" :severity="data.isOfficial ? 'success' : 'secondary'" />
|
<Tag :value="data.isOfficial ? $t('courses.official') : $t('courses.unofficial')" :severity="data.isOfficial ? 'success' : 'secondary'" />
|
||||||
@@ -98,7 +92,6 @@ import PermissionGate from '@/components/common/PermissionGate.vue';
|
|||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import Column from 'primevue/column';
|
import Column from 'primevue/column';
|
||||||
import Tag from 'primevue/tag';
|
import Tag from 'primevue/tag';
|
||||||
import Rating from 'primevue/rating';
|
|
||||||
|
|
||||||
const { toPersianDigits } = usePersianDate();
|
const { toPersianDigits } = usePersianDate();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="payment-list-view">
|
<div class="payment-list-view">
|
||||||
<PageHeader :title="$t('payments.title')" :subtitle="$t('payments.subtitle')">
|
<PageHeader :title="$t('payments.title')" :subtitle="$t('payments.subtitle')">
|
||||||
<PermissionGate permission="payments:create">
|
<PermissionGate permission="payments:create">
|
||||||
<Button :label="$t('payments.addPayment')" icon="pi pi-plus" severity="success" @click="showCreateModal = true" />
|
<Button :label="$t('payments.addPayment')" icon="pi pi-plus" severity="success" @click="openCreateModal" />
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -80,24 +80,37 @@
|
|||||||
<Dialog v-model:visible="showCreateModal" header="ایجاد صورتحساب جدید" modal :style="{ width: '480px' }">
|
<Dialog v-model:visible="showCreateModal" header="ایجاد صورتحساب جدید" modal :style="{ width: '480px' }">
|
||||||
<div class="flex flex-column gap-3 py-2">
|
<div class="flex flex-column gap-3 py-2">
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">انتخاب کاربر / دانشجو *</label>
|
<label class="font-semibold text-sm">انتخاب یک یا چند کلاس *</label>
|
||||||
<Dropdown v-model="createForm.user" :options="usersList" optionLabel="fullName" optionValue="_id" filter placeholder="کاربر را انتخاب کنید" class="w-full text-sm" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-column gap-2">
|
|
||||||
<label class="font-semibold text-sm">انتخاب یک یا چند کلاس مربوطه *</label>
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="createForm.classes"
|
v-model="createForm.classes"
|
||||||
:options="classesList"
|
:options="classesList"
|
||||||
optionLabel="name"
|
optionLabel="name"
|
||||||
optionValue="_id"
|
optionValue="_id"
|
||||||
display="chip"
|
display="chip"
|
||||||
placeholder="کلاسها را انتخاب کنید"
|
filter
|
||||||
|
placeholder="ابتدا کلاسها را انتخاب کنید"
|
||||||
class="w-full text-sm"
|
class="w-full text-sm"
|
||||||
@change="onClassesSelected"
|
@change="onClassesSelected"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-column gap-2">
|
||||||
|
<label class="font-semibold text-sm">انتخاب کاربر / دانشجو *</label>
|
||||||
|
<Dropdown
|
||||||
|
v-model="createForm.user"
|
||||||
|
:options="eligibleUsers"
|
||||||
|
optionLabel="fullName"
|
||||||
|
optionValue="_id"
|
||||||
|
filter
|
||||||
|
:placeholder="createForm.classes?.length ? 'دانشجوی ثبتنامشده را انتخاب کنید' : 'ابتدا کلاس را انتخاب کنید'"
|
||||||
|
class="w-full text-sm"
|
||||||
|
:disabled="!createForm.classes?.length"
|
||||||
|
/>
|
||||||
|
<small v-if="createForm.classes?.length && !eligibleUsers.length" class="text-orange-500">
|
||||||
|
هیچ دانشجوی ثبتنامشدهای در کلاسهای انتخابشده یافت نشد
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">مبلغ کل صورتحساب (تومان) *</label>
|
<label class="font-semibold text-sm">مبلغ کل صورتحساب (تومان) *</label>
|
||||||
<InputNumber v-model="createForm.amount" class="w-full text-sm" suffix=" تومان" />
|
<InputNumber v-model="createForm.amount" class="w-full text-sm" suffix=" تومان" />
|
||||||
@@ -123,7 +136,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import { ref, reactive, computed, onMounted } from 'vue';
|
||||||
import { useDataTable } from '@/composables/useDataTable';
|
import { useDataTable } from '@/composables/useDataTable';
|
||||||
import { usePersianDate } from '@/composables/usePersianDate';
|
import { usePersianDate } from '@/composables/usePersianDate';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
@@ -174,11 +187,43 @@ const deleteDialogVisible = ref(false);
|
|||||||
const selectedPayment = ref(null);
|
const selectedPayment = ref(null);
|
||||||
const isDeleting = ref(false);
|
const isDeleting = ref(false);
|
||||||
|
|
||||||
|
const studentIdOf = (student) => String(student?._id || student?.id || student);
|
||||||
|
|
||||||
|
const eligibleUsers = computed(() => {
|
||||||
|
if (!createForm.classes?.length) return [];
|
||||||
|
|
||||||
|
const selectedIds = new Set(createForm.classes.map(String));
|
||||||
|
const selectedClasses = classesList.value.filter((c) =>
|
||||||
|
selectedIds.has(String(c._id || c.id))
|
||||||
|
);
|
||||||
|
if (!selectedClasses.length) return [];
|
||||||
|
|
||||||
|
// Intersection: user must be registered in every selected class
|
||||||
|
let eligibleIds = null;
|
||||||
|
selectedClasses.forEach((cls) => {
|
||||||
|
const ids = new Set((cls.students || []).map(studentIdOf));
|
||||||
|
if (eligibleIds === null) {
|
||||||
|
eligibleIds = ids;
|
||||||
|
} else {
|
||||||
|
eligibleIds = new Set([...eligibleIds].filter((id) => ids.has(id)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return usersList.value.filter((u) => eligibleIds?.has(String(u._id || u.id)));
|
||||||
|
});
|
||||||
|
|
||||||
const onClassesSelected = () => {
|
const onClassesSelected = () => {
|
||||||
if (!createForm.classes || createForm.classes.length === 0) return;
|
if (createForm.user && !eligibleUsers.value.some((u) => (u._id || u.id) === createForm.user)) {
|
||||||
|
createForm.user = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createForm.classes || createForm.classes.length === 0) {
|
||||||
|
createForm.amount = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
let totalFee = 0;
|
let totalFee = 0;
|
||||||
createForm.classes.forEach(classId => {
|
createForm.classes.forEach((classId) => {
|
||||||
const c = classesList.value.find(item => (item._id || item.id) === classId);
|
const c = classesList.value.find((item) => (item._id || item.id) === classId);
|
||||||
if (c) {
|
if (c) {
|
||||||
totalFee += (c.tuitionFee || c.course?.price || 0);
|
totalFee += (c.tuitionFee || c.course?.price || 0);
|
||||||
}
|
}
|
||||||
@@ -188,15 +233,27 @@ const onClassesSelected = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetCreateForm = () => {
|
||||||
|
createForm.user = null;
|
||||||
|
createForm.classes = [];
|
||||||
|
createForm.amount = 0;
|
||||||
|
createForm.dueDate = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
resetCreateForm();
|
||||||
|
showCreateModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
const fetchDropdownData = async () => {
|
const fetchDropdownData = async () => {
|
||||||
try {
|
try {
|
||||||
const [uRes, cRes] = await Promise.all([
|
const [uRes, cRes] = await Promise.all([
|
||||||
userApi.getAll({ limit: 150 }),
|
userApi.getAll({ limit: 200 }),
|
||||||
classApi.getAll({ limit: 100 })
|
classApi.getAll({ limit: 100 })
|
||||||
]);
|
]);
|
||||||
const uData = uRes.data || uRes;
|
const uData = uRes.data || uRes;
|
||||||
const rawUsers = uData.items || uData.users || uData || [];
|
const rawUsers = uData.items || uData.users || uData || [];
|
||||||
usersList.value = rawUsers.map(u => ({ ...u, fullName: u.name || '' }));
|
usersList.value = rawUsers.map((u) => ({ ...u, fullName: u.name || '' }));
|
||||||
|
|
||||||
const cData = cRes.data || cRes;
|
const cData = cRes.data || cRes;
|
||||||
classesList.value = cData.items || cData.classes || cData || [];
|
classesList.value = cData.items || cData.classes || cData || [];
|
||||||
@@ -206,6 +263,7 @@ const fetchDropdownData = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreatePayment = async () => {
|
const handleCreatePayment = async () => {
|
||||||
|
if (!createForm.classes?.length) { showError('لطفا کلاس را انتخاب کنید'); return; }
|
||||||
if (!createForm.user) { showError('لطفا کاربر را انتخاب کنید'); return; }
|
if (!createForm.user) { showError('لطفا کاربر را انتخاب کنید'); return; }
|
||||||
if (!createForm.amount) { showError('لطفا مبلغ را وارد کنید'); return; }
|
if (!createForm.amount) { showError('لطفا مبلغ را وارد کنید'); return; }
|
||||||
if (!createForm.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
|
if (!createForm.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
|
||||||
|
|||||||
@@ -78,6 +78,10 @@
|
|||||||
<Textarea v-model="form.note" rows="3" class="w-full text-sm" placeholder="یادداشت داخلی درباره این جلسه…" />
|
<Textarea v-model="form.note" rows="3" class="w-full text-sm" placeholder="یادداشت داخلی درباره این جلسه…" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<AdminNotesField v-model="form.adminNotes" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 flex justify-content-end gap-2 mt-4 pt-3 border-top-1 border-color">
|
<div class="col-12 flex justify-content-end gap-2 mt-4 pt-3 border-top-1 border-color">
|
||||||
<Button label="انصراف" text severity="secondary" @click="$router.push('/sessions')" />
|
<Button label="انصراف" text severity="secondary" @click="$router.push('/sessions')" />
|
||||||
<Button type="submit" :label="$t('app.save')" icon="pi pi-check" :loading="isSubmitting" />
|
<Button type="submit" :label="$t('app.save')" icon="pi pi-check" :loading="isSubmitting" />
|
||||||
@@ -98,6 +102,7 @@ import { professorApi } from '@/api/professorApi';
|
|||||||
import { usePersianDate } from '@/composables/usePersianDate';
|
import { usePersianDate } from '@/composables/usePersianDate';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import PageHeader from '@/components/common/PageHeader.vue';
|
import PageHeader from '@/components/common/PageHeader.vue';
|
||||||
|
import AdminNotesField from '@/components/common/AdminNotesField.vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import Dropdown from 'primevue/select';
|
import Dropdown from 'primevue/select';
|
||||||
@@ -133,7 +138,8 @@ const form = reactive({
|
|||||||
status: 'scheduled',
|
status: 'scheduled',
|
||||||
topic: '',
|
topic: '',
|
||||||
place: '',
|
place: '',
|
||||||
note: ''
|
note: '',
|
||||||
|
adminNotes: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const toGregorianIso = (jalaliValue) => {
|
const toGregorianIso = (jalaliValue) => {
|
||||||
@@ -195,7 +201,8 @@ const fetchData = async () => {
|
|||||||
status: data.status || 'scheduled',
|
status: data.status || 'scheduled',
|
||||||
topic: data.topic || '',
|
topic: data.topic || '',
|
||||||
place: data.place || '',
|
place: data.place || '',
|
||||||
note: data.note || ''
|
note: data.note || '',
|
||||||
|
adminNotes: Array.isArray(data.adminNotes) ? [...data.adminNotes] : []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -225,7 +232,8 @@ const handleSubmit = async () => {
|
|||||||
status: form.status,
|
status: form.status,
|
||||||
topic: form.topic,
|
topic: form.topic,
|
||||||
place: form.place,
|
place: form.place,
|
||||||
note: form.note
|
note: form.note,
|
||||||
|
adminNotes: (form.adminNotes || []).map((n) => String(n).trim()).filter(Boolean)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- /src/views/users/UserDetailView.vue -->
|
<!-- /src/views/users/UserDetailView.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="user-detail-view" v-if="user">
|
<div class="user-detail-view" v-if="user">
|
||||||
<PageHeader :title="user.name || ''" :subtitle="`کد ملی: ${toPersianDigits(user.nationalId)}`">
|
<PageHeader :title="user.name || ''" :subtitle="`کد ملی: ${toPersianDigits(user.nationalIdCode || user.nationalId)}`">
|
||||||
<PermissionGate permission="users:update">
|
<PermissionGate permission="users:update">
|
||||||
<Button :label="$t('app.edit')" icon="pi pi-pencil" severity="warning" @click="$router.push(`/users/edit/${user._id || user.id}`)" />
|
<Button :label="$t('app.edit')" icon="pi pi-pencil" severity="warning" @click="$router.push(`/users/edit/${user._id || user.id}`)" />
|
||||||
</PermissionGate>
|
</PermissionGate>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
<span class="text-muted text-xs block mb-1">کد ملی</span>
|
<span class="text-muted text-xs block mb-1">کد ملی</span>
|
||||||
<span class="font-bold text-color text-base" dir="ltr">{{ toPersianDigits(user.nationalId) }}</span>
|
<span class="font-bold text-color text-base" dir="ltr">{{ toPersianDigits(user.nationalIdCode || user.nationalId) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
<div class="col-12 sm:col-6 md:col-4 mb-3">
|
||||||
<span class="text-muted text-xs block mb-1">شماره همراه</span>
|
<span class="text-muted text-xs block mb-1">شماره همراه</span>
|
||||||
@@ -84,6 +84,12 @@
|
|||||||
<span class="text-muted text-xs block mb-1">آدرس سکونت</span>
|
<span class="text-muted text-xs block mb-1">آدرس سکونت</span>
|
||||||
<span class="text-color text-sm">{{ user.address || '-' }}</span>
|
<span class="text-color text-sm">{{ user.address || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 mb-3" v-if="user.adminNotes?.length">
|
||||||
|
<span class="text-muted text-xs block mb-1">یادداشتهای ادمین</span>
|
||||||
|
<ul class="m-0 pr-3 text-sm line-height-3">
|
||||||
|
<li v-for="(note, i) in user.adminNotes" :key="i">{{ note }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,10 @@
|
|||||||
<InputText v-model.trim="form.address" class="w-full text-sm" />
|
<InputText v-model.trim="form.address" class="w-full text-sm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<AdminNotesField v-model="form.adminNotes" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6 flex flex-column gap-2" v-if="isEditMode">
|
<div class="col-12 md:col-6 flex flex-column gap-2" v-if="isEditMode">
|
||||||
<label class="font-semibold text-sm">{{ $t('auth.username') }}</label>
|
<label class="font-semibold text-sm">{{ $t('auth.username') }}</label>
|
||||||
<InputText
|
<InputText
|
||||||
@@ -166,6 +170,7 @@ import { userApi } from '@/api/userApi';
|
|||||||
import { roleApi } from '@/api/roleApi';
|
import { roleApi } from '@/api/roleApi';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import PageHeader from '@/components/common/PageHeader.vue';
|
import PageHeader from '@/components/common/PageHeader.vue';
|
||||||
|
import AdminNotesField from '@/components/common/AdminNotesField.vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import Dropdown from 'primevue/select';
|
import Dropdown from 'primevue/select';
|
||||||
import MultiSelect from 'primevue/multiselect';
|
import MultiSelect from 'primevue/multiselect';
|
||||||
@@ -225,7 +230,8 @@ const form = reactive({
|
|||||||
parentPhoneNumber: '',
|
parentPhoneNumber: '',
|
||||||
address: '',
|
address: '',
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: '',
|
||||||
|
adminNotes: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildPayload = () => {
|
const buildPayload = () => {
|
||||||
@@ -243,7 +249,8 @@ const buildPayload = () => {
|
|||||||
postalCode: form.postalCode || undefined,
|
postalCode: form.postalCode || undefined,
|
||||||
education: form.education || undefined,
|
education: form.education || undefined,
|
||||||
parentPhoneNumber: form.parentPhoneNumber || undefined,
|
parentPhoneNumber: form.parentPhoneNumber || undefined,
|
||||||
address: form.address || undefined
|
address: form.address || undefined,
|
||||||
|
adminNotes: (form.adminNotes || []).map((n) => String(n).trim()).filter(Boolean)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (form.preferredMessenger?.length) {
|
if (form.preferredMessenger?.length) {
|
||||||
@@ -295,7 +302,8 @@ const fetchUser = async () => {
|
|||||||
parentPhoneNumber: data.parentPhoneNumber || '',
|
parentPhoneNumber: data.parentPhoneNumber || '',
|
||||||
address: data.address || '',
|
address: data.address || '',
|
||||||
username: data.username || '',
|
username: data.username || '',
|
||||||
password: ''
|
password: '',
|
||||||
|
adminNotes: Array.isArray(data.adminNotes) ? [...data.adminNotes] : []
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err);
|
showError(err);
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<Column field="nationalId" header="کد ملی">
|
<Column field="nationalIdCode" header="کد ملی">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ toPersianDigits(data.nationalId) || '-' }}
|
{{ toPersianDigits(data.nationalIdCode || data.nationalId) || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<Column field="activeCoursesCount" header="دورههای فعال">
|
<Column field="registeredClassesCount" header="کلاسهای ثبتنامی">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ toPersianDigits(data.activeCoursesCount || data.enrolledCourses?.length || 0) }}
|
{{ toPersianDigits(data.registeredClassesCount ?? 0) }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user