diff --git a/src/App.vue b/src/App.vue index fba44b9..4104ff2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -17,6 +17,7 @@ import StudentLayout from '@/layouts/StudentLayout.vue' import ConfirmModal from '@/components/ConfirmModal.vue' import CounselorLayout from '@/layouts/CounselorLayout.vue' import { tokenService } from '@/services/api/token-service' +import MissionaryLayout from '@/layouts/MissionaryLayout.vue' import { VueQueryDevtools } from '@tanstack/vue-query-devtools' const route = useRoute() @@ -26,6 +27,7 @@ const layouts = { admin: AdminLayout, student: StudentLayout, counselor: CounselorLayout, + missionary: MissionaryLayout, } const currentLayout = computed(() => layouts[route.meta?.layout] || PublicLayout) diff --git a/src/assets/icons/clock.svg b/src/assets/icons/clock.svg index 8f50197..0350cd2 100644 --- a/src/assets/icons/clock.svg +++ b/src/assets/icons/clock.svg @@ -1,7 +1,7 @@ - - + + diff --git a/src/assets/icons/monitor.svg b/src/assets/icons/monitor.svg new file mode 100644 index 0000000..f91ab4a --- /dev/null +++ b/src/assets/icons/monitor.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/newspaper.svg b/src/assets/icons/newspaper.svg new file mode 100644 index 0000000..2a0e973 --- /dev/null +++ b/src/assets/icons/newspaper.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/icons/power.svg b/src/assets/icons/power.svg new file mode 100644 index 0000000..491ad8e --- /dev/null +++ b/src/assets/icons/power.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/images/assignment-icon.png b/src/assets/images/assignment-icon.png new file mode 100644 index 0000000..b8dfeb0 Binary files /dev/null and b/src/assets/images/assignment-icon.png differ diff --git a/src/assets/images/course-icon.png b/src/assets/images/course-icon.png new file mode 100644 index 0000000..df2e8fb Binary files /dev/null and b/src/assets/images/course-icon.png differ diff --git a/src/assets/images/session-icon.png b/src/assets/images/session-icon.png new file mode 100644 index 0000000..dbe86f7 Binary files /dev/null and b/src/assets/images/session-icon.png differ diff --git a/src/components/Badge.vue b/src/components/Badge.vue index 7a0aad1..fa4da1b 100644 --- a/src/components/Badge.vue +++ b/src/components/Badge.vue @@ -21,7 +21,7 @@ import { computed } from 'vue' import SvgIcon from '@/components/icons/SvgIcon.vue' const props = defineProps({ - /** @type {import('vue').PropType<'neutral' | 'success' | 'danger' | 'warning' | 'primary' | 'info' | 'cyan'>} */ + /** @type {import('vue').PropType<'neutral' | 'success' | 'danger' | 'warning' | 'primary' | 'info' | 'cyan' | 'blue'>} */ variant: { type: String, default: 'neutral' }, /** @type {import('vue').PropType<'sm' | 'md' | 'lg'>} */ size: { type: String, default: 'md' }, @@ -138,5 +138,10 @@ const onClick = (event) => { background: rgba(104, 104, 104, 10%); color: #686868; } + + &--blue { + background: rgba(0, 72, 255, 8%); + color: #0048ff; + } } diff --git a/src/components/DropdownMenu.vue b/src/components/DropdownMenu.vue index 8839041..747219a 100644 --- a/src/components/DropdownMenu.vue +++ b/src/components/DropdownMenu.vue @@ -29,7 +29,7 @@ v-if="item.icon" :name="item.icon" :size="16" - :color="item.iconColor ?? item.color" + :color="item.iconColor ?? item.color ?? 'currentColor'" class="dropdown-menu__icon" /> {{ item.label }} diff --git a/src/components/HeaderBlock.vue b/src/components/HeaderBlock.vue index 1ccde30..245e779 100644 --- a/src/components/HeaderBlock.vue +++ b/src/components/HeaderBlock.vue @@ -11,7 +11,10 @@
-
{{ pageTitle }}
+
+ {{ pageTitle }} + {{ pageSubtitle || pageTitle }} +
@@ -27,7 +30,10 @@
-
{{ pageTitle }}
+
+ {{ pageTitle }} + {{ pageSubtitle || pageTitle }} +
@@ -43,22 +49,20 @@ + + diff --git a/src/components/blocks/TabsBlock.vue b/src/components/blocks/TabsBlock.vue index 0282968..a334792 100644 --- a/src/components/blocks/TabsBlock.vue +++ b/src/components/blocks/TabsBlock.vue @@ -126,7 +126,7 @@ const onActionClick = (tab) => { gap: 0.375rem; background: rgba(255, 255, 255, 80%); box-shadow: 0 10px 15px -3px rgba(241, 241, 241, 100%); - padding: 0.875rem; + padding: 0.775rem; border-radius: 0.75rem; } @@ -144,7 +144,8 @@ const onActionClick = (tab) => { &--desktop { display: none; min-width: fit-content; - margin-block-end: 0.625rem; + + // margin-block-end: 0.625rem; @media (min-width: 768px) { display: flex; @@ -162,7 +163,7 @@ const onActionClick = (tab) => { } &__action-btn { - padding: 0 1.25rem; + height: 100%; min-width: fit-content; } diff --git a/src/components/form/DatePickerField.vue b/src/components/form/DatePickerField.vue index 8495404..d8897cd 100644 --- a/src/components/form/DatePickerField.vue +++ b/src/components/form/DatePickerField.vue @@ -14,7 +14,7 @@ @click="toggle" > {{ displayValue }} - +
@@ -377,6 +377,7 @@ export default { this.$emit('change', gregorian) this.close() } catch (error) { + // eslint-disable-next-line no-console console.error('Error in handleConfirm:', error) } }, diff --git a/src/components/form/ImageCropper.vue b/src/components/form/ImageCropper.vue index 5ed5280..049cf3d 100644 --- a/src/components/form/ImageCropper.vue +++ b/src/components/form/ImageCropper.vue @@ -325,8 +325,7 @@ onBeforeUnmount(cleanup) margin: 0; @media (min-width: 1024px) { - font-size: 0.75rem; - line-height: 1.85rem; + font-size: 0.7rem; } } } diff --git a/src/components/icons/icon-names.d.ts b/src/components/icons/icon-names.d.ts index 3fb7d0d..f8c4c83 100644 --- a/src/components/icons/icon-names.d.ts +++ b/src/components/icons/icon-names.d.ts @@ -36,13 +36,16 @@ export type IconName = | 'map-pin-simple-area' | 'menu' | 'microphone' + | 'monitor' | 'mood' + | 'newspaper' | 'paper-plane' | 'paper-plane-right' | 'pencil' | 'phone' | 'play' | 'plus' + | 'power' | 'scroll' | 'shopping-bag' | 'smiley-sad' diff --git a/src/constants/fields.js b/src/constants/fields.js index 913180c..5e1b860 100644 --- a/src/constants/fields.js +++ b/src/constants/fields.js @@ -17,6 +17,7 @@ export const fields = { postalCode: 'کد پستی', birthDate: 'تاریخ تولد', + birthday: 'تاریخ تولد', gender: 'جنسیت', maritalStatus: 'وضعیت تأهل', diff --git a/src/enums/index.js b/src/enums/index.js index bf16596..3453321 100644 --- a/src/enums/index.js +++ b/src/enums/index.js @@ -206,3 +206,20 @@ export const SERVICE_TYPE = Object.freeze({ educational: 'آموزشی', other: 'سایر', }) + +// Lifecycle of a dispatch request assigned to a missionary. `pending` is the +// initial backend state; the missionary may set seen, accepted, or rejected. +export const MISSIONARY_REQUEST_STATUS = Object.freeze({ + pending: 'در انتظار بررسی', + seen: 'دیده شده', + accepted: 'پذیرفته شده', + rejected: 'رد شده', +}) + +// Landing route per role after login / when a logged-in user hits a guest +// page. Roles not listed here fall back to the student dashboard. +export const ROLE_HOME_ROUTES = Object.freeze({ + admin: 'admin-dashboard', + counselor: 'counselor-consultations', + missionary: 'missionary-requests', +}) diff --git a/src/features/admin/assignments/components/AssignmentsFilters.vue b/src/features/admin/assignments/components/AssignmentsFilters.vue index 465f5a0..88e064c 100644 --- a/src/features/admin/assignments/components/AssignmentsFilters.vue +++ b/src/features/admin/assignments/components/AssignmentsFilters.vue @@ -3,7 +3,7 @@
@@ -20,7 +20,7 @@
@@ -50,6 +50,7 @@ diff --git a/src/features/missionary/components/modals/MissionaryRequestDetailsModal.vue b/src/features/missionary/components/modals/MissionaryRequestDetailsModal.vue new file mode 100644 index 0000000..aa31d3a --- /dev/null +++ b/src/features/missionary/components/modals/MissionaryRequestDetailsModal.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/src/features/missionary/index.js b/src/features/missionary/index.js new file mode 100644 index 0000000..588598c --- /dev/null +++ b/src/features/missionary/index.js @@ -0,0 +1 @@ +export { default as MissionaryRequestDetailsModal } from './components/modals/MissionaryRequestDetailsModal.vue' diff --git a/src/features/missionary/pages/MissionaryRequestsPage.vue b/src/features/missionary/pages/MissionaryRequestsPage.vue new file mode 100644 index 0000000..88975b9 --- /dev/null +++ b/src/features/missionary/pages/MissionaryRequestsPage.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/features/missionary/router.js b/src/features/missionary/router.js new file mode 100644 index 0000000..c9a7ed5 --- /dev/null +++ b/src/features/missionary/router.js @@ -0,0 +1,24 @@ +export default [ + { + path: '/missionary-panel/requests', + name: 'missionary-requests', + component: () => import('@/features/missionary/pages/MissionaryRequestsPage.vue'), + meta: { + layout: 'missionary', + role: 'missionary', + title: 'سیستم مدیریت اعزام', + subtitle: 'ساده‌ترین راه برای برنامه‌ریزی و پایش مأموریت‌ها', + }, + }, + { + path: '/missionary-panel/tickets', + name: 'missionary-tickets', + component: () => import('@/features/student/tickets/pages/StudentTicketsPage.vue'), + meta: { + layout: 'missionary', + role: 'missionary', + title: 'تیکت‌های شما', + subtitle: 'اینجا پل ارتباطی شما با پشتیبانی است.', + }, + }, +] diff --git a/src/features/student/router.js b/src/features/student/router.js index c2cab36..8150bfb 100644 --- a/src/features/student/router.js +++ b/src/features/student/router.js @@ -3,90 +3,165 @@ export default [ path: '/dashboard', name: 'student-dashboard', component: () => import('@/features/student/pages/StudentDashboardPage.vue'), - meta: { layout: 'student', role: 'student', title: 'پیشخوان' }, + meta: { + layout: 'student', + role: 'student', + title: 'سلام بر شما', + subtitle: 'به سامانه موسسه بانو خوش آمدید.', + }, }, { path: '/profile', name: 'student-profile', component: () => import('@/features/student/profile/pages/EditProfilePage.vue'), - meta: { layout: 'student', role: 'student', title: 'ویرایش پروفایل' }, + meta: { + layout: 'student', + role: 'student', + title: 'پروفایل شما', + subtitle: 'مشخصاتتان را ویرایش نمایید.', + }, }, { path: '/my-courses', name: 'student-courses', component: () => import('@/features/student/courses/pages/StudentCoursesPage.vue'), - meta: { layout: 'student', role: 'student', title: 'دوره‌های من' }, + meta: { + layout: 'student', + role: 'student', + title: 'یادگیری شما', + subtitle: 'مهارت‌ها و دوره‌های خود را دنبال کنید.', + }, }, { path: '/my-courses/:id', name: 'student-course-details', component: () => import('@/features/student/courses/pages/StudentCourseDetailsPage.vue'), - meta: { layout: 'student', role: 'student', title: 'جزئیات دوره' }, + meta: { + layout: 'student', + role: 'student', + title: 'یادگیری شما', + subtitle: 'مهارت‌ها و دوره‌های خود را دنبال کنید.', + }, }, { path: '/my-terms', name: 'student-terms', component: () => import('@/features/student/terms/pages/StudentTermsPage.vue'), - meta: { layout: 'student', role: 'student', title: 'ترم‌های آموزشی' }, + meta: { + layout: 'student', + role: 'student', + title: 'یادگیری شما', + subtitle: 'مهارت‌ها و دوره‌های خود را دنبال کنید.', + }, }, { path: '/my-terms/:id', name: 'student-term-details', component: () => import('@/features/student/terms/pages/StudentTermDetailsPage.vue'), - meta: { layout: 'student', role: 'student', title: 'جزئیات دوره' }, + meta: { + layout: 'student', + role: 'student', + title: 'یادگیری شما', + subtitle: 'مهارت‌ها و دوره‌های خود را دنبال کنید.', + }, }, { path: '/my-lessons/:id', name: 'student-lesson-details', component: () => import('@/features/student/lessons/pages/StudentLessonDetailsPage.vue'), - meta: { layout: 'student', role: 'student', title: 'جزئیات درس' }, + meta: { + layout: 'student', + role: 'student', + title: 'یادگیری شما', + subtitle: 'مهارت‌ها و دوره‌های خود را دنبال کنید.', + }, }, { path: '/my-sessions/:id', name: 'student-session-details', component: () => import('@/features/student/sessions/pages/StudentSessionDetailsPage.vue'), - meta: { layout: 'student', role: 'student', title: 'جزئیات جلسه' }, + meta: { + layout: 'student', + role: 'student', + title: 'یادگیری شما', + subtitle: 'مهارت‌ها و دوره‌های خود را دنبال کنید.', + }, }, { path: '/my-exams/:id', name: 'student-exam', component: () => import('@/features/student/exams/pages/StudentExamPage.vue'), - meta: { layout: 'student', role: 'student', title: 'آزمون' }, + meta: { + layout: 'student', + role: 'student', + title: 'یادگیری شما', + subtitle: 'مهارت‌ها و دوره‌های خود را دنبال کنید.', + }, }, { path: '/my-exams/:id/take', name: 'student-take-exam', component: () => import('@/features/student/exams/pages/StudentTakeExamPage.vue'), - meta: { layout: 'student', role: 'student', title: 'شروع آزمون' }, + meta: { + layout: 'student', + role: 'student', + title: 'یادگیری شما', + subtitle: 'مهارت‌ها و دوره‌های خود را دنبال کنید.', + }, }, { path: '/missionary', name: 'student-missionary', component: () => import('@/features/student/missionary/pages/StudentMissionaryPage.vue'), - meta: { layout: 'student', role: 'student', title: 'سامانه اعزام' }, + meta: { + layout: 'student', + role: 'student', + title: 'سیستم مدیریت اعزام', + subtitle: 'ساده‌ترین راه برای برنامه‌ریزی و پایش مأموریت‌ها', + }, }, { path: '/user/services', name: 'student-services', component: () => import('@/features/student/services/pages/StudentServicesPage.vue'), - meta: { layout: 'student', role: 'student', title: 'خدمات' }, + meta: { + layout: 'student', + role: 'student', + title: 'درخواست خدمت', + subtitle: 'فقط یک مرحله تا شروع کار شما.', + }, }, { path: '/user/consultants', name: 'student-consultants', component: () => import('@/features/student/consultants/pages/StudentConsultantsPage.vue'), - meta: { layout: 'student', role: 'student', title: 'مشاوره' }, + meta: { + layout: 'student', + role: 'student', + title: 'مشاوره سریع', + subtitle: 'چند ثانیه برای دریافت مشاوره کافیست.', + }, }, { path: '/user/certificates', name: 'student-certificates', component: () => import('@/features/student/certificates/pages/StudentCertificatesPage.vue'), - meta: { layout: 'student', role: 'student', title: 'گواهینامه ها' }, + meta: { + layout: 'student', + role: 'student', + title: 'گواهینامه معتبر', + subtitle: 'نشان اعتماد و تخصص ما در هر حوزه', + }, }, { path: '/my-tickets', name: 'student-tickets', component: () => import('@/features/student/tickets/pages/StudentTicketsPage.vue'), - meta: { layout: 'student', role: 'student', title: 'تیکت‌های من' }, + meta: { + layout: 'student', + role: 'student', + title: 'تیکت‌های شما', + subtitle: 'اینجا پل ارتباطی شما با پشتیبانی است.', + }, }, ] diff --git a/src/features/student/tickets/components/modals/TicketDetailsModal.vue b/src/features/student/tickets/components/modals/TicketDetailsModal.vue index c613ea6..0e132b4 100644 --- a/src/features/student/tickets/components/modals/TicketDetailsModal.vue +++ b/src/features/student/tickets/components/modals/TicketDetailsModal.vue @@ -14,7 +14,17 @@
-
+
+
+
+ + {{ messageTime(ticket) }} +
+
+

{{ message.message }}

{{ messageTime(message) }}
@@ -60,6 +71,7 @@ import BasicModal from '@/components/BasicModal.vue' import SvgIcon from '@/components/icons/SvgIcon.vue' import NoItems from '@/components/blocks/NoItems.vue' import { formatJalaaliDate } from '@/utils/date-utils' +import MessageAttachments from '@/components/blocks/MessageAttachments.vue' import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue' import { studentTicketsKeys, @@ -81,6 +93,8 @@ const { data: ticket, isLoading } = useStudentTicketQuery(ticketId, { const messages = computed(() => ticket.value?.messages ?? []) +const ticketMedia = computed(() => ticket.value?.media ?? []) + const ticketDate = computed(() => formatJalaaliDate(ticket.value?.createdAt) || '—') // Student viewing their own ticket — their own messages get the highlighted diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 897ea22..e5c6d6a 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -6,7 +6,11 @@
- +
@@ -53,6 +57,7 @@ onMounted(() => window.addEventListener('resize', onResize)) onBeforeUnmount(() => window.removeEventListener('resize', onResize)) const pageTitle = computed(() => route.meta?.title || 'پیشخوان') +const pageSubtitle = computed(() => route.meta?.subtitle || '') const educationRoutes = new Set([ 'admin-terms', diff --git a/src/layouts/CounselorLayout.vue b/src/layouts/CounselorLayout.vue index fd8ecd6..c9dfaa9 100644 --- a/src/layouts/CounselorLayout.vue +++ b/src/layouts/CounselorLayout.vue @@ -6,7 +6,11 @@
- +
@@ -53,6 +57,7 @@ onMounted(() => window.addEventListener('resize', onResize)) onBeforeUnmount(() => window.removeEventListener('resize', onResize)) const pageTitle = computed(() => route.meta?.title || 'مشاوره') +const pageSubtitle = computed(() => route.meta?.subtitle || '') const menuItems = computed(() => [ { diff --git a/src/layouts/MissionaryLayout.vue b/src/layouts/MissionaryLayout.vue new file mode 100644 index 0000000..f7d35b3 --- /dev/null +++ b/src/layouts/MissionaryLayout.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/src/layouts/StudentLayout.vue b/src/layouts/StudentLayout.vue index b1121ab..bdf9f48 100644 --- a/src/layouts/StudentLayout.vue +++ b/src/layouts/StudentLayout.vue @@ -21,7 +21,11 @@
- +