273 lines
10 KiB
Vue
273 lines
10 KiB
Vue
<!-- /src/views/courses/CourseFormView.vue -->
|
||
<template>
|
||
<div class="course-form-view w-full max-w-5xl mx-auto">
|
||
<PageHeader
|
||
:title="isEditMode ? $t('courses.editCourse') : $t('courses.addCourse')"
|
||
:subtitle="isEditMode ? 'ویرایش مشخصات دوره و مدیریت کلاسها' : 'تعریف دوره آموزشی جدید'"
|
||
>
|
||
<Button label="انصراف" text severity="secondary" @click="$router.push('/courses')" />
|
||
</PageHeader>
|
||
|
||
<!-- Course Details -->
|
||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm mb-4">
|
||
<h2 class="text-lg font-bold text-color mb-4 pb-2 border-bottom-1 border-color">مشخصات اصلی دوره</h2>
|
||
<form @submit.prevent="handleSubmitCourse" class="grid">
|
||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||
<label class="font-semibold text-sm">{{ $t('courses.courseTitle') }} *</label>
|
||
<InputText v-model.trim="form.title" class="w-full text-sm" :class="{ 'p-invalid': errors.title }" />
|
||
<small v-if="errors.title" class="text-red-500 text-xs">{{ errors.title }}</small>
|
||
</div>
|
||
|
||
<div class="col-12 md:col-3 flex flex-column gap-2">
|
||
<label class="font-semibold text-sm">{{ $t('courses.type') }}</label>
|
||
<Dropdown v-model="form.type" :options="typeOptions" optionLabel="label" optionValue="value" class="w-full text-sm" />
|
||
</div>
|
||
|
||
<div class="col-12 md:col-3 flex flex-column gap-2">
|
||
<label class="font-semibold text-sm">{{ $t('courses.price') }} *</label>
|
||
<InputNumber v-model="form.price" class="w-full text-sm" :class="{ 'p-invalid': errors.price }" suffix=" تومان" />
|
||
<small v-if="errors.price" class="text-red-500 text-xs">{{ errors.price }}</small>
|
||
</div>
|
||
|
||
<div class="col-12 md:col-6 flex align-items-center gap-4 mt-2">
|
||
<div class="flex align-items-center gap-2">
|
||
<InputSwitch v-model="form.isOfficial" />
|
||
<label class="font-semibold text-sm">{{ $t('courses.isOfficial') }}</label>
|
||
</div>
|
||
<div class="flex align-items-center gap-2">
|
||
<InputSwitch v-model="form.showOnFrontend" />
|
||
<label class="font-semibold text-sm">نمایش در وبسایت</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-12 flex flex-column gap-2">
|
||
<label class="font-semibold text-sm">{{ $t('courses.description') }}</label>
|
||
<Textarea v-model="form.description" rows="3" class="w-full text-sm" />
|
||
</div>
|
||
|
||
<div class="col-12 flex flex-column gap-2">
|
||
<label class="font-semibold text-sm">نکات برجسته (هر خط یک مورد)</label>
|
||
<Textarea
|
||
v-model="highlightsText"
|
||
rows="4"
|
||
class="w-full text-sm"
|
||
placeholder="مثال: مناسب مبتدیان همراه با گواهینامه تمرین عملی"
|
||
/>
|
||
</div>
|
||
|
||
<div class="col-12 flex justify-content-end mt-3">
|
||
<Button type="submit" label="ذخیره مشخصات دوره" icon="pi pi-check" :loading="isSubmitting" />
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Classes of this course (edit mode only) -->
|
||
<div v-if="isEditMode" class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm">
|
||
<div class="flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||
<div>
|
||
<h2 class="text-lg font-bold m-0">کلاسهای این دوره</h2>
|
||
<p class="text-muted text-sm m-0 mt-1">برای هر کلاس میتوانید دانشجویان و جلسات را جداگانه مدیریت کنید</p>
|
||
</div>
|
||
<Button
|
||
label="تعریف کلاس جدید"
|
||
icon="pi pi-plus"
|
||
@click="$router.push(`/classes/create?courseId=${courseId}`)"
|
||
/>
|
||
</div>
|
||
|
||
<TableSkeleton v-if="loadingClasses" :rows="5" :columns="6" />
|
||
<DataTable
|
||
v-else
|
||
:value="courseClasses"
|
||
class="p-datatable-sm text-sm"
|
||
emptyMessage="هنوز کلاسی برای این دوره تعریف نشده است"
|
||
>
|
||
<Column header="#" style="width: 50px">
|
||
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
|
||
</Column>
|
||
<Column header="نام کلاس">
|
||
<template #body="{ data }">
|
||
<span class="font-bold">{{ data.name }}</span>
|
||
</template>
|
||
</Column>
|
||
<Column header="استاد">
|
||
<template #body="{ data }">
|
||
{{ data.professor ? `${data.professor.name || ''} ${data.professor.surname || ''}`.trim() : '—' }}
|
||
</template>
|
||
</Column>
|
||
<Column header="ظرفیت" style="width: 90px">
|
||
<template #body="{ data }">{{ toPersianDigits(data.capacity || 0) }}</template>
|
||
</Column>
|
||
<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" />
|
||
</template>
|
||
</Column>
|
||
<Column header="عملیات" style="width: 120px">
|
||
<template #body="{ data }">
|
||
<div class="flex align-items-center gap-1">
|
||
<Button
|
||
icon="pi pi-eye"
|
||
text
|
||
rounded
|
||
size="small"
|
||
severity="info"
|
||
v-tooltip.top="'مشاهده'"
|
||
@click="$router.push(`/classes/view/${data._id || data.id}`)"
|
||
/>
|
||
<Button
|
||
icon="pi pi-pencil"
|
||
text
|
||
rounded
|
||
size="small"
|
||
severity="warning"
|
||
v-tooltip.top="'ویرایش / جلسات'"
|
||
@click="$router.push(`/classes/edit/${data._id || data.id}`)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</Column>
|
||
</DataTable>
|
||
</div>
|
||
|
||
<div v-else class="surface-card p-4 border-round border-1 border-color shadow-sm">
|
||
<p class="text-muted text-sm m-0">پس از ذخیره دوره، میتوانید کلاسهای آن را در همین صفحه تعریف کنید. جلسات از داخل هر کلاس ساخته میشوند.</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue';
|
||
import { useRoute, useRouter } from 'vue-router';
|
||
import { courseApi } from '@/api/courseApi';
|
||
import { classApi } from '@/api/classApi';
|
||
import { formatClassSchedule } from '@/utils/classSchedule';
|
||
import { usePersianDate } from '@/composables/usePersianDate';
|
||
import { useToast } from '@/composables/useToast';
|
||
|
||
import PageHeader from '@/components/common/PageHeader.vue';
|
||
import StatusTag from '@/components/common/StatusTag.vue';
|
||
import TableSkeleton from '@/components/common/TableSkeleton.vue';
|
||
import InputText from 'primevue/inputtext';
|
||
import InputNumber from 'primevue/inputnumber';
|
||
import Dropdown from 'primevue/select';
|
||
import Textarea from 'primevue/textarea';
|
||
import InputSwitch from 'primevue/toggleswitch';
|
||
import Button from 'primevue/button';
|
||
import DataTable from 'primevue/datatable';
|
||
import Column from 'primevue/column';
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const { toPersianDigits } = usePersianDate();
|
||
const { showSuccess, showError } = useToast();
|
||
|
||
const courseId = route.params.id;
|
||
const isEditMode = computed(() => !!courseId);
|
||
|
||
const isSubmitting = ref(false);
|
||
const loadingClasses = ref(false);
|
||
const courseClasses = ref([]);
|
||
|
||
const typeOptions = [
|
||
{ label: 'عمومی', value: 'General' },
|
||
{ label: 'خصوصی', value: 'Private' }
|
||
];
|
||
|
||
const form = reactive({
|
||
title: '',
|
||
description: '',
|
||
type: 'General',
|
||
price: 1500000,
|
||
isOfficial: true,
|
||
showOnFrontend: true
|
||
});
|
||
|
||
const highlightsText = ref('');
|
||
|
||
const errors = reactive({
|
||
title: '',
|
||
price: ''
|
||
});
|
||
|
||
const fetchClassesForCourse = async () => {
|
||
if (!courseId) return;
|
||
loadingClasses.value = true;
|
||
try {
|
||
const res = await classApi.getAll({ limit: 100, courseId });
|
||
const data = res.data || res;
|
||
courseClasses.value = Array.isArray(data) ? data : (data.items || data.data || []);
|
||
} catch (e) {
|
||
console.warn('Classes fetch error:', e);
|
||
} finally {
|
||
loadingClasses.value = false;
|
||
}
|
||
};
|
||
|
||
const fetchCourse = async () => {
|
||
if (!courseId) return;
|
||
try {
|
||
const res = await courseApi.getOne(courseId);
|
||
const data = res.data || res;
|
||
Object.assign(form, {
|
||
title: data.title || '',
|
||
description: data.description || '',
|
||
type: data.type || 'General',
|
||
price: data.price || 0,
|
||
isOfficial: data.isOfficial !== false,
|
||
showOnFrontend: data.showOnFrontend !== false
|
||
});
|
||
highlightsText.value = Array.isArray(data.highlights) ? data.highlights.join('\n') : '';
|
||
} catch (err) {
|
||
showError(err);
|
||
}
|
||
};
|
||
|
||
const handleSubmitCourse = async () => {
|
||
if (!form.title) { errors.title = 'عنوان دوره الزامی است'; return; }
|
||
if (form.price === null) { errors.price = 'شهریه الزامی است'; return; }
|
||
|
||
isSubmitting.value = true;
|
||
try {
|
||
const payload = {
|
||
...form,
|
||
highlights: highlightsText.value
|
||
.split('\n')
|
||
.map((line) => line.trim())
|
||
.filter(Boolean)
|
||
};
|
||
if (isEditMode.value) {
|
||
await courseApi.update(courseId, payload);
|
||
showSuccess('مشخصات دوره با موفقیت ویرایش شد');
|
||
} else {
|
||
const res = await courseApi.create(payload);
|
||
const newCourse = res.data || res;
|
||
const newId = newCourse._id || newCourse.id || (newCourse.data && (newCourse.data._id || newCourse.data.id));
|
||
showSuccess('دوره جدید با موفقیت ایجاد شد. اکنون میتوانید کلاس جدید تعریف کنید.');
|
||
if (newId) {
|
||
router.push(`/courses/edit/${newId}`);
|
||
} else {
|
||
router.push('/courses');
|
||
}
|
||
}
|
||
} catch (err) {
|
||
showError(err);
|
||
} finally {
|
||
isSubmitting.value = false;
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
fetchCourse();
|
||
fetchClassesForCourse();
|
||
});
|
||
</script>
|