Files
gameno-dashboard/src/views/courses/CourseFormView.vue
T

273 lines
10 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- /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="مثال:&#10;مناسب مبتدیان&#10;همراه با گواهی‌نامه&#10;تمرین عملی"
/>
</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>