diff --git a/src/api/classApi.js b/src/api/classApi.js
index 95900e6..7b18046 100644
--- a/src/api/classApi.js
+++ b/src/api/classApi.js
@@ -12,5 +12,7 @@ export const classApi = {
registerUsers: (classId, userIds, notify) =>
axiosInstance.post(`/classes/admin/${classId}/register-users`, { userIds, notify }),
removeUser: (classId, userId) =>
- axiosInstance.delete(`/classes/admin/${classId}/students/${userId}`)
+ axiosInstance.delete(`/classes/admin/${classId}/students/${userId}`),
+ sendPlanToProfessor: (classId) =>
+ axiosInstance.post(`/classes/admin/${classId}/send-plan-professor`)
};
diff --git a/src/api/employeeTimingApi.js b/src/api/employeeTimingApi.js
new file mode 100644
index 0000000..31191fe
--- /dev/null
+++ b/src/api/employeeTimingApi.js
@@ -0,0 +1,11 @@
+// /src/api/employeeTimingApi.js
+import axiosInstance from './axiosInstance';
+
+export const employeeTimingApi = {
+ getAll: (params) => axiosInstance.get('/employee-timings/admin/get-all', { params }),
+ getSummary: (params) => axiosInstance.get('/employee-timings/admin/summary', { params }),
+ getOne: (id) => axiosInstance.get(`/employee-timings/admin/get-one/${id}`),
+ create: (data) => axiosInstance.post('/employee-timings/admin/create', data),
+ update: (id, data) => axiosInstance.put(`/employee-timings/admin/update/${id}`, data),
+ delete: (id) => axiosInstance.delete(`/employee-timings/admin/delete/${id}`)
+};
diff --git a/src/components/common/SmsPreviewConfirmDialog.vue b/src/components/common/SmsPreviewConfirmDialog.vue
new file mode 100644
index 0000000..9bb5336
--- /dev/null
+++ b/src/components/common/SmsPreviewConfirmDialog.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
diff --git a/src/components/layout/AppSidebar.vue b/src/components/layout/AppSidebar.vue
index 0fd3ff8..29e2ff1 100644
--- a/src/components/layout/AppSidebar.vue
+++ b/src/components/layout/AppSidebar.vue
@@ -142,6 +142,7 @@ const menuGroups = computed(() => {
key: 'system',
label: 'مدیریت سیستم',
items: [
+ { label: 'ورود و خروج کارمندان', icon: 'pi pi-clock', to: '/employee-timings', permission: PERMISSIONS.EMPLOYEE_TIMINGS_READ },
{ label: 'گزارش فعالیتها', icon: 'pi pi-history', to: '/logs', permission: PERMISSIONS.LOGS_READ },
{ label: 'نقشها و دسترسیها', icon: 'pi pi-shield', to: '/roles' }
]
@@ -166,6 +167,7 @@ const menuGroups = computed(() => {
if (permissionStore.roleName === 'SuperAdmin') {
const systemGroup = groups.find((g) => g.key === 'system');
+ systemGroup.items.push({ label: 'قالبهای اعلان', icon: 'pi pi-send', to: '/notification-templates' });
systemGroup.items.push({ label: 'تنظیمات', icon: 'pi pi-cog', to: '/settings' });
}
diff --git a/src/constants/permissions.js b/src/constants/permissions.js
index ecd1bb2..4aad899 100644
--- a/src/constants/permissions.js
+++ b/src/constants/permissions.js
@@ -89,7 +89,12 @@ export const PERMISSIONS = {
WAITLIST_CREATE: 'waitlist:create',
WAITLIST_READ: 'waitlist:read',
WAITLIST_UPDATE: 'waitlist:update',
- WAITLIST_DELETE: 'waitlist:delete'
+ WAITLIST_DELETE: 'waitlist:delete',
+
+ EMPLOYEE_TIMINGS_CREATE: 'employee_timings:create',
+ EMPLOYEE_TIMINGS_READ: 'employee_timings:read',
+ EMPLOYEE_TIMINGS_UPDATE: 'employee_timings:update',
+ EMPLOYEE_TIMINGS_DELETE: 'employee_timings:delete'
};
export const PERMISSION_GROUPS = [
@@ -106,6 +111,17 @@ export const PERMISSION_GROUPS = [
{ key: PERMISSIONS.USERS_ENROLL, label: 'ثبتنام کاربر در دوره' }
]
},
+ {
+ key: 'employee_timings',
+ label: 'تردد و ساعت کارمندان',
+ icon: 'pi pi-clock',
+ permissions: [
+ { key: PERMISSIONS.EMPLOYEE_TIMINGS_CREATE, label: 'ثبت تردد جدید' },
+ { key: PERMISSIONS.EMPLOYEE_TIMINGS_READ, label: 'مشاهده تردد کارمندان' },
+ { key: PERMISSIONS.EMPLOYEE_TIMINGS_UPDATE, label: 'ویرایش تردد' },
+ { key: PERMISSIONS.EMPLOYEE_TIMINGS_DELETE, label: 'حذف تردد' }
+ ]
+ },
{
key: 'professors',
label: 'مدیریت اساتید',
diff --git a/src/locales/fa.json b/src/locales/fa.json
index 835cbd7..486bee3 100644
--- a/src/locales/fa.json
+++ b/src/locales/fa.json
@@ -156,5 +156,15 @@
"settings": {
"title": "تنظیمات سیستم",
"subtitle": "عملیات راهاندازی و پیکربندی اولیه پایگاه داده"
+ },
+ "employeeTimings": {
+ "title": "ورود و خروج کارمندان",
+ "subtitle": "ثبت، ویرایش و مدیریت زمان تردد و ساعت کاری کارمندان",
+ "addTiming": "ثبت تردد جدید",
+ "editTiming": "ویرایش رکورد تردد"
+ },
+ "notificationTemplates": {
+ "title": "قالبهای اعلان",
+ "subtitle": "مدیریت متن، شناسهها و متغیرهای قالبهای پیامک، ایمیل و بازوی بله"
}
}
diff --git a/src/router/routes.js b/src/router/routes.js
index 94a40bf..74abc46 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -241,6 +241,22 @@ export const routes = [
meta: { permission: 'logs:read' }
},
+ // Employee timings
+ {
+ path: 'employee-timings',
+ name: 'EmployeeTimingList',
+ component: () => import('@/views/employeeTimings/EmployeeTimingListView.vue'),
+ meta: { title: 'ورود و خروج کارمندان', permission: 'employee_timings:read' }
+ },
+
+ // Notification templates
+ {
+ path: 'notification-templates',
+ name: 'NotificationTemplates',
+ component: () => import('@/views/notifications/NotificationTemplatesView.vue'),
+ meta: { title: 'قالبهای اعلان', superAdminOnly: true }
+ },
+
{
path: 'settings',
name: 'Settings',
diff --git a/src/views/classes/ClassFormView.vue b/src/views/classes/ClassFormView.vue
index 61df616..3725f60 100644
--- a/src/views/classes/ClassFormView.vue
+++ b/src/views/classes/ClassFormView.vue
@@ -5,12 +5,23 @@
:title="isEditMode ? 'ویرایش کلاس' : 'تعریف کلاس جدید'"
:subtitle="isEditMode ? 'ویرایش کلاس، دانشجویان و جلسات' : 'تعریف کلاس جدید برای دوره'"
>
-
+
+
+
+
@@ -304,12 +315,23 @@
+
+
@@ -329,6 +351,7 @@ import AdminNotesField from '@/components/common/AdminNotesField.vue';
import ClassSessionsSection from '@/components/classes/ClassSessionsSection.vue';
import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue';
import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue';
+import SmsPreviewConfirmDialog from '@/components/common/SmsPreviewConfirmDialog.vue';
import PermissionGate from '@/components/common/PermissionGate.vue';
import InputText from 'primevue/inputtext';
import InputNumber from 'primevue/inputnumber';
@@ -643,5 +666,54 @@ watch(() => form.course, (courseId) => {
selectedCourseHoursPerSection.value = course?.hoursPerSection ?? null;
});
+const isProfessorSmsDialogVisible = ref(false);
+const isSendingProfessorSms = ref(false);
+const professorSmsText = ref('');
+
+const selectedProfessor = computed(() => {
+ if (!form.professor) return null;
+ return professors.value.find((p) => String(p._id) === String(form.professor)) || null;
+});
+
+const openProfessorSmsDialog = () => {
+ const prof = selectedProfessor.value;
+ if (!prof) {
+ showError('استاد این کلاس مشخص نشده است.');
+ return;
+ }
+ const profName = `${prof.name || ''} ${prof.surname || ''}`.trim() || 'استاد';
+ const className = form.name || 'کلاس';
+ const selectedDayLabels = (form.days || [])
+ .map((d) => weekdays.find((w) => w.value === d)?.label)
+ .filter(Boolean)
+ .join('، ') || 'طبق هماهنگی';
+ const classTimes = (form.startTime && form.endTime)
+ ? `${form.startTime} الی ${form.endTime}`
+ : (form.startTime || form.endTime || 'طبق هماهنگی');
+ const startDateStr = form.startDate ? toPersianDigits(form.startDate) : '—';
+ const endDateStr = calculatedEndDateDisplay.value !== '—' ? calculatedEndDateDisplay.value : (form.endDate ? toPersianDigits(form.endDate) : '—');
+
+ professorSmsText.value = `با سلام و وقت بخیر، استاد ${profName}،
+برنامه کلاس ${className} شما به شرح زیر می باشد:
+${selectedDayLabels}، ${classTimes}
+از ${startDateStr} الی ${endDateStr}`;
+
+ isProfessorSmsDialogVisible.value = true;
+};
+
+const sendProfessorClassPlanSms = async () => {
+ if (!classId) return;
+ isSendingProfessorSms.value = true;
+ try {
+ await classApi.sendPlanToProfessor(classId);
+ showSuccess('برنامه کلاس با موفقیت برای استاد پیامک شد.');
+ isProfessorSmsDialogVisible.value = false;
+ } catch (err) {
+ showError(err);
+ } finally {
+ isSendingProfessorSms.value = false;
+ }
+};
+
onMounted(fetchData);
diff --git a/src/views/employeeTimings/EmployeeTimingListView.vue b/src/views/employeeTimings/EmployeeTimingListView.vue
new file mode 100644
index 0000000..1ecbda6
--- /dev/null
+++ b/src/views/employeeTimings/EmployeeTimingListView.vue
@@ -0,0 +1,664 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ کل رکوردهای تردد
+ {{ toPersianDigits(summary.total || 0) }}
+
+
+
+
+
+
+
+
+
+
+ ترددهای کامل
+ {{ toPersianDigits(summary.complete || 0) }}
+
+
+
+
+
+
+
+
+
+
+ فاقد زمان خروج (هشدار)
+ {{ toPersianDigits(summary.missingExit || 0) }}
+
+
+
+
+
+
+
+
+
+
+ فاقد زمان ورود (هشدار)
+ {{ toPersianDigits(summary.missingEntry || 0) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ option.name }}
+ {{ option.phoneNumber || option.nationalIdCode || '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (data.user?.name || 'ک').charAt(0) }}
+
+
+
{{ data.user?.name || '—' }}
+
+ کد ملی: {{ data.user.nationalIdCode }}
+ {{ data.user.phoneNumber }}
+
+
+
+
+
+
+
+
+
+
+ {{ formatJalali(data.date) }}
+ {{ formatJalaliDayName(data.date) }}
+
+
+
+
+
+
+
+
+ {{ toPersianDigits(data.entryTime) }}
+
+
+ ثبت نشده
+
+
+
+
+
+
+
+
+ {{ toPersianDigits(data.exitTime) }}
+
+
+ ثبت نشده
+
+
+
+
+
+
+
+
+ {{ data.durationFormatted ? toPersianDigits(data.durationFormatted) : '—' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ data.note }}
+
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/notifications/NotificationTemplatesView.vue b/src/views/notifications/NotificationTemplatesView.vue
new file mode 100644
index 0000000..07d2eff
--- /dev/null
+++ b/src/views/notifications/NotificationTemplatesView.vue
@@ -0,0 +1,566 @@
+
+
+
+
+
+
+
+
+
+
+ پیامک
+
+
+
+
+
+ ایمیل
+
+
+
+
+
+ بازوی بله
+
+
+
+
+
+
+
+
+
+
+
+
+
+
قالبهای پیامک (sms.ir)
+
+ شناسه قالب، متن و متغیرهای ارسالی پنل sms.ir را در این بخش تنظیم کنید. هر متغیر با فرمت #NAME# در متن قالب جایگذاری و هنگام ارسال جایگزین میشود.
+
+
+
+
+
+
+ در حال بارگذاری قالبهای پیامک…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ template.key }}
+
+
+
+
+
+ {{ templateForm[template.key].enabled ? 'ارسال پیامک فعال' : 'ارسال غیرفعال' }}
+
+
+
+
+
+
+
+ ارسال این نوع پیامک غیرفعال است و با وقوع این رویداد پیامکی ارسال نخواهد شد.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ متغیرها را به فرمت #NAME# بنویسید
+
+
+
+
+
+
+
+
+
+ متغیرهای ارسالی در قالب
+ متغیرهایی که در پنل sms.ir در متن قالب قرار دادهاید
+
+
+
+
+
+ هیچ متغیری برای این قالب تعریف نشده است. پیامک بدون متغیر ارسال خواهد شد.
+
+
+
+
+
+ مقدار داده در سیستم:
+
+
+
+
+
نام متغیر در sms.ir:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
قالبهای اعلان ایمیل
+
+ امکان ویرایش و سفارشیسازی قالبهای HTML ایمیل برای رویدادهای مختلف سیستم در بهروزرسانیهای آینده فعال خواهد شد.
+
+
+
+
+
+
+
+
+
+
+
+
قالبهای بازوی بله
+
+ امکان تنظیم و شخصیسازی پیامها و کلیدهای شیشهای بازوی پیامرسان بله در بهروزرسانیهای آینده فعال خواهد شد.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/settings/SettingsView.vue b/src/views/settings/SettingsView.vue
index 7811461..6e3e7ce 100644
--- a/src/views/settings/SettingsView.vue
+++ b/src/views/settings/SettingsView.vue
@@ -3,16 +3,15 @@
کانالها
اطلاعرسانی
- قالبهای پیامک
- راهاندازی
- وارد کردن داده
+ راهاندازی
+ وارد کردن داده
@@ -161,181 +160,6 @@
-
-
-
-
-
-
قالبهای پیامک
-
- شناسه قالب و تعداد و نام متغیرهای sms.ir را بر اساس قالب پنل پیامک تنظیم کنید. میتوانید هر تعداد متغیر که میخواهید تعریف یا حذف نمایید.
-
-
-
-
-
-
- در حال بارگذاری تنظیمات پیامک…
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ template.key }}
-
-
-
-
-
- {{ templateForm[template.key].enabled ? 'فعال' : 'غیرفعال' }}
-
-
-
-
-
-
-
- ارسال این نوع پیامک غیرفعال است و با وقوع رویداد مربوطه پیامکی ارسال نخواهد شد.
-
-
-
-
-
-
-
-
-
-
-
-
- متغیرهای ارسالی در قالب
- متغیرهایی که در پنل sms.ir در متن قالب قرار دادهاید
-
-
-
-
-
- هیچ متغیری برای این قالب تعریف نشده است. پیامک بدون متغیر ارسال خواهد شد.
-
-
-
-
-
- مقدار ارسالی از سیستم:
-
-
-
-
-
نام متغیر در sms.ir:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-