From 91527e64bbe719f4c7a1270de3e7a4ef34bd1472 Mon Sep 17 00:00:00 2001 From: Kavehhn174 Date: Sat, 15 Aug 2026 23:59:08 +0330 Subject: [PATCH] feat(classes): add class days and meeting times to class forms Let admins set the weekly schedule on the class and show it in lists, details, and session generation. --- package.json | 2 +- .../classes/ClassSessionsSection.vue | 22 ++++++++-- src/utils/classSchedule.js | 41 +++++++++++++++++++ src/utils/classSchedule.test.js | 22 ++++++++++ src/views/classes/ClassDetailView.vue | 9 ++++ src/views/classes/ClassFormView.vue | 37 +++++++++++++++++ src/views/classes/ClassListView.vue | 7 ++++ src/views/courses/CourseFormView.vue | 6 +++ 8 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/utils/classSchedule.js create mode 100644 src/utils/classSchedule.test.js diff --git a/package.json b/package.json index ba19928..8310faa 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "test": "node --test src/utils/uploadResponse.test.js" + "test": "node --test src/utils/uploadResponse.test.js src/utils/classSchedule.test.js" }, "dependencies": { "@fontsource/vazirmatn": "^5.3.0", diff --git a/src/components/classes/ClassSessionsSection.vue b/src/components/classes/ClassSessionsSection.vue index 770aa81..d56e9d9 100644 --- a/src/components/classes/ClassSessionsSection.vue +++ b/src/components/classes/ClassSessionsSection.vue @@ -254,7 +254,10 @@ const props = defineProps({ classId: { type: [String, Object], required: true }, courseId: { type: [String, Object], required: true }, professorId: { type: [String, Object], default: null }, - defaultSessionCount: { type: Number, default: 12 } + defaultSessionCount: { type: Number, default: 12 }, + days: { type: Array, default: () => [] }, + startTime: { type: String, default: '' }, + endTime: { type: String, default: '' } }); const { toPersianDigits, toLatinDigits, formatJalali } = usePersianDate(); @@ -339,11 +342,22 @@ const weekdays = [ const autoForm = reactive({ sessionCount: props.defaultSessionCount || 12, startDate: todayJalali(), - startTime: '19:00', - endTime: '20:30', - selectedDays: [6, 2] + startTime: props.startTime || '19:00', + endTime: props.endTime || '20:30', + selectedDays: props.days?.length ? [...props.days] : [6, 2] }); +watch( + () => [props.days, props.startTime, props.endTime, props.defaultSessionCount], + () => { + if (props.days?.length) autoForm.selectedDays = [...props.days]; + if (props.startTime) autoForm.startTime = props.startTime; + if (props.endTime) autoForm.endTime = props.endTime; + if (props.defaultSessionCount) autoForm.sessionCount = props.defaultSessionCount; + }, + { deep: true } +); + const toSessionDayIso = (value) => { if (!value) return null; const latin = toLatinDigits(String(value)); diff --git a/src/utils/classSchedule.js b/src/utils/classSchedule.js new file mode 100644 index 0000000..4271712 --- /dev/null +++ b/src/utils/classSchedule.js @@ -0,0 +1,41 @@ +export const WEEKDAYS = [ + { label: 'شنبه', value: 6 }, + { label: 'یکشنبه', value: 0 }, + { label: 'دوشنبه', value: 1 }, + { label: 'سه‌شنبه', value: 2 }, + { label: 'چهارشنبه', value: 3 }, + { label: 'پنجشنبه', value: 4 }, + { label: 'جمعه', value: 5 } +]; + +const IRAN_WEEK_ORDER = [6, 0, 1, 2, 3, 4, 5]; +const LABEL_BY_VALUE = Object.fromEntries(WEEKDAYS.map((day) => [day.value, day.label])); + +const joinFaList = (items) => { + if (!items.length) return ''; + if (items.length === 1) return items[0]; + if (items.length === 2) return `${items[0]} و ${items[1]}`; + return `${items.slice(0, -1).join('، ')} و ${items[items.length - 1]}`; +}; + +export function formatClassDays(days = []) { + const unique = [...new Set((days || []).map(Number).filter((day) => day >= 0 && day <= 6))]; + unique.sort((a, b) => IRAN_WEEK_ORDER.indexOf(a) - IRAN_WEEK_ORDER.indexOf(b)); + return joinFaList(unique.map((day) => LABEL_BY_VALUE[day]).filter(Boolean)); +} + +export function formatClassTime(startTime, endTime) { + const start = String(startTime || '').trim(); + const end = String(endTime || '').trim(); + if (!start && !end) return ''; + if (!end) return start; + if (!start) return end; + return `${start} – ${end}`; +} + +export function formatClassSchedule(cls) { + const days = formatClassDays(cls?.days); + const time = formatClassTime(cls?.startTime, cls?.endTime); + if (days && time) return `${days} | ${time}`; + return days || time || ''; +} diff --git a/src/utils/classSchedule.test.js b/src/utils/classSchedule.test.js new file mode 100644 index 0000000..63ba01c --- /dev/null +++ b/src/utils/classSchedule.test.js @@ -0,0 +1,22 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { formatClassDays, formatClassTime, formatClassSchedule } from './classSchedule.js'; + +describe('class schedule display', () => { + it('formats weekdays in Iran week order', () => { + assert.equal(formatClassDays([1, 6]), 'شنبه و دوشنبه'); + }); + + it('formats start and end times', () => { + assert.equal(formatClassTime('19:00', '21:00'), '19:00 – 21:00'); + assert.equal(formatClassTime('19:00', ''), '19:00'); + }); + + it('joins days and time for a class schedule label', () => { + assert.equal( + formatClassSchedule({ days: [6, 1], startTime: '19:00', endTime: '21:00' }), + 'شنبه و دوشنبه | 19:00 – 21:00' + ); + assert.equal(formatClassSchedule({}), ''); + }); +}); diff --git a/src/views/classes/ClassDetailView.vue b/src/views/classes/ClassDetailView.vue index e7dfd99..720aac0 100644 --- a/src/views/classes/ClassDetailView.vue +++ b/src/views/classes/ClassDetailView.vue @@ -33,6 +33,14 @@ {{ formatJalali(classData.startDate) }} — {{ formatJalali(classData.endDate) }} +
+ روزهای برگزاری + {{ formatClassDays(classData.days) || '—' }} +
+
+ ساعت کلاس + {{ formatClassTime(classData.startTime, classData.endTime) || '—' }} +
وضعیت @@ -125,6 +133,7 @@ import { ref, computed, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { classApi } from '@/api/classApi'; +import { formatClassDays, formatClassTime } from '@/utils/classSchedule'; import { sessionApi } from '@/api/sessionApi'; import { usePersianDate } from '@/composables/usePersianDate'; import { useToast } from '@/composables/useToast'; diff --git a/src/views/classes/ClassFormView.vue b/src/views/classes/ClassFormView.vue index a1349ef..7ddfeb4 100644 --- a/src/views/classes/ClassFormView.vue +++ b/src/views/classes/ClassFormView.vue @@ -55,6 +55,28 @@
+
+ + +
+ +
+ + +
+ +
+ + +
+
@@ -137,6 +159,9 @@ :course-id="form.course ? String(form.course) : ''" :professor-id="form.professor ? String(form.professor) : null" :default-session-count="defaultSessionCount" + :days="form.days" + :start-time="form.startTime" + :end-time="form.endTime" /> @@ -177,6 +202,8 @@ import Tag from 'primevue/tag'; import DataTable from 'primevue/datatable'; import Column from 'primevue/column'; import DatePicker from 'vue3-persian-datetime-picker'; +import SelectButton from 'primevue/selectbutton'; +import { WEEKDAYS } from '@/utils/classSchedule'; const route = useRoute(); const router = useRouter(); @@ -210,9 +237,13 @@ const form = reactive({ tuitionFee: 0, startDate: '', endDate: '', + days: [], + startTime: '', + endTime: '', isActive: true, adminNotes: [] }); +const weekdays = WEEKDAYS; const removeDialogMessage = computed(() => { const name = studentToRemove.value?.name; @@ -293,6 +324,9 @@ const fetchData = async () => { tuitionFee: data.tuitionFee || 0, startDate: toJalaliDisplay(data.startDate), endDate: toJalaliDisplay(data.endDate), + days: Array.isArray(data.days) ? [...data.days] : [], + startTime: data.startTime || '', + endTime: data.endTime || '', isActive: data.isActive !== false, adminNotes: Array.isArray(data.adminNotes) ? [...data.adminNotes] : [] }); @@ -329,6 +363,9 @@ const handleSubmit = async () => { tuitionFee: form.tuitionFee, startDate: toGregorianIso(form.startDate), endDate: toGregorianIso(form.endDate), + days: form.days || [], + startTime: form.startTime || '', + endTime: form.endTime || '', isActive: form.isActive, adminNotes: (form.adminNotes || []).map((n) => String(n).trim()).filter(Boolean) }; diff --git a/src/views/classes/ClassListView.vue b/src/views/classes/ClassListView.vue index 26b5b55..c5b336c 100644 --- a/src/views/classes/ClassListView.vue +++ b/src/views/classes/ClassListView.vue @@ -29,6 +29,12 @@ + + + +