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.
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 || '';
|
||||
}
|
||||
@@ -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({}), '');
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,14 @@
|
||||
{{ formatJalali(classData.startDate) }} — {{ formatJalali(classData.endDate) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-12 sm:col-4">
|
||||
<span class="text-muted text-xs block mb-1">روزهای برگزاری</span>
|
||||
<span class="font-bold text-color text-sm">{{ formatClassDays(classData.days) || '—' }}</span>
|
||||
</div>
|
||||
<div class="col-12 sm:col-4">
|
||||
<span class="text-muted text-xs block mb-1">ساعت کلاس</span>
|
||||
<span class="font-bold text-color text-sm" dir="ltr">{{ formatClassTime(classData.startTime, classData.endTime) || '—' }}</span>
|
||||
</div>
|
||||
<div class="col-12 sm:col-4">
|
||||
<span class="text-muted text-xs block mb-1">وضعیت</span>
|
||||
<StatusTag :status="classData.isActive !== false" />
|
||||
@@ -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';
|
||||
|
||||
@@ -55,6 +55,28 @@
|
||||
<label class="font-semibold text-sm">کلاس فعال است</label>
|
||||
</div>
|
||||
|
||||
<div class="col-12 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">روزهای برگزاری</label>
|
||||
<SelectButton
|
||||
v-model="form.days"
|
||||
:options="weekdays"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
multiple
|
||||
class="text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">ساعت شروع کلاس</label>
|
||||
<InputText v-model.trim="form.startTime" class="w-full text-sm" dir="ltr" placeholder="19:00" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">ساعت پایان کلاس</label>
|
||||
<InputText v-model.trim="form.endTime" class="w-full text-sm" dir="ltr" placeholder="21:00" />
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<AdminNotesField v-model="form.adminNotes" />
|
||||
</div>
|
||||
@@ -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"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="برنامه کلاس">
|
||||
<template #body="{ data }">
|
||||
<span class="text-sm">{{ formatClassSchedule(data) || '—' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="capacity" header="ظرفیت">
|
||||
<template #body="{ data }">
|
||||
{{ toPersianDigits(data.capacity || 0) }} نفر
|
||||
@@ -68,6 +74,7 @@ import { onMounted } from 'vue';
|
||||
import { useDataTable } from '@/composables/useDataTable';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { classApi } from '@/api/classApi';
|
||||
import { formatClassSchedule } from '@/utils/classSchedule';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import DataTableWrapper from '@/components/common/DataTableWrapper.vue';
|
||||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||||
|
||||
@@ -120,6 +120,11 @@
|
||||
<Column header="دانشجو" style="width: 90px">
|
||||
<template #body="{ data }">{{ toPersianDigits((data.students || []).length) }}</template>
|
||||
</Column>
|
||||
<Column header="برنامه کلاس">
|
||||
<template #body="{ data }">
|
||||
<span class="text-sm">{{ formatClassSchedule(data) || '—' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="وضعیت" style="width: 90px">
|
||||
<template #body="{ data }">
|
||||
<StatusTag :status="data.isActive !== false" />
|
||||
@@ -164,6 +169,7 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { courseApi } from '@/api/courseApi';
|
||||
import { professorApi } from '@/api/professorApi';
|
||||
import { classApi } from '@/api/classApi';
|
||||
import { formatClassSchedule } from '@/utils/classSchedule';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user