Files
enlight-lms/frontend/src/pages/StudentProfile.vue
Alexandrina-Kuzeleva ef4321586c TEST UPD
2025-12-05 16:13:38 +03:00

947 lines
40 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="min-h-screen bg-white">
<NoPermission v-if="!$user.data" />
<div v-else-if="profile.error" class="p-6">
<div class="max-w-4xl mx-auto bg-white rounded-xl shadow-sm p-6 border border-red-200">
<p class="text-red-500 text-lg font-medium">{{__('Error loading profile:')}} {{ profile.error.message }}</p>
</div>
</div>
<div v-else-if="profile.data && schoolProfileNotFound">
<header class="sticky top-0 z-10 flex items-center justify-between bg-white px-6 py-4">
<Breadcrumbs class="h-7" :items="breadcrumbs" />
</header>
<div class="mx-auto max-w-6xl px-4 py-6">
<!-- Profile Header -->
<div class="bg-gradient-to-r from-teal-100 to-teal-600 rounded-2xl shadow-sm border border-gray-200 p-6 -mt-4 relative">
<div class="flex flex-col md:flex-row md:items-center gap-6">
<div class="flex-1">
<h2 class="text-3xl font-bold text-gray-900">{{ displayName }}</h2>
<div
v-if="profile.data.bio"
v-html="
DOMPurify.sanitize(decodeEntities(profile.data.bio), {
ALLOWED_TAGS: [
'b',
'i',
'em',
'strong',
'a',
'p',
'br',
'ul',
'ol',
'li',
'img',
],
ALLOWED_ATTR: ['href', 'target', 'rel', 'src'],
})
"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal"
></div>
</div>
<div v-if="$user.data && isSessionUser() && !schoolProfileNotFound" class="md:ml-auto">
<Button @click="toggleEdit()" class="bg-white hover:bg-gray-100 px-5 py-2.5 rounded-lg transition-colors duration-200">
<template #prefix>
<Edit class="w-4 h-4 stroke-1.5" />
</template>
{{ editMode ? __('Cancel editing') : __('Edit profile') }}
</Button>
</div>
</div>
</div>
<!-- VIEW MODE -->
<div v-if="!editMode" class="mt-6">
<!-- Пустой профиль -->
<div v-if="schoolProfileNotFound" class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="p-8 text-center">
<div class="max-w-md mx-auto">
<div class="mx-auto w-20 h-20 bg-teal-100 rounded-full flex items-center justify-center mb-6">
<svg class="w-10 h-10 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 mb-3">Профиль студента еще не заполнен</h3>
<p class="text-gray-600 mb-6">
Чтобы получить доступ ко всем возможностям платформы, заполните информацию о себе.
Это поможет наставникам лучше понять ваши интересы и цели.
</p>
<div class="bg-teal-50 border border-teal-100 rounded-lg p-5 mb-6 text-left">
<h4 class="font-semibold text-teal-800 mb-3 flex items-center gap-2">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
Заполнив профиль, вы получите:
</h4>
<ul class="space-y-2 text-sm text-gray-700">
<li class="flex items-start gap-2">
<svg class="w-4 h-4 text-teal-500 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<span>Персонализированные рекомендации по обучению</span>
</li>
<li class="flex items-start gap-2">
<svg class="w-4 h-4 text-teal-500 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<span>Подбор наставников по вашим интересам</span>
</li>
<li class="flex items-start gap-2">
<svg class="w-4 h-4 text-teal-500 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<span>Доступ к закрытым мероприятиям и курсам</span>
</li>
</ul>
</div>
<Button
@click="toggleEdit()"
class="bg-teal-600 hover:bg-teal-700 text-white px-8 py-3 rounded-lg font-medium transition-colors duration-200 shadow-sm hover:shadow-md"
>
<template #prefix>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</template>
Заполнить профиль студента
</Button>
</div>
</div>
</div>
<!-- Загружающийся профиль -->
<div v-else-if="schoolProfile.loading" class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
<div class="flex items-center justify-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
</div>
<!-- Ошибка загрузки (кроме DoesNotExistError) -->
<div v-else-if="schoolProfile.error && !schoolProfileNotFound" class="bg-white rounded-2xl shadow-sm border border-red-200 p-6">
<p class="text-red-500 text-lg font-medium">{{__('Error loading student data:')}} {{ schoolProfile.error.message }}</p>
</div>
<!-- Загруженный профиль -->
<div v-else-if="schoolProfile.data && !schoolProfileNotFound" class="space-y-6">
<!-- Основная информация -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-teal-400">
<h3 class="text-xl font-semibold text-white">{{__('Basic Information')}}</h3>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="space-y-4">
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('Last name:')}}:</span>
<span class="text-gray-900">{{ schoolProfile.data.last_name || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('Name:')}}:</span>
<span class="text-gray-900">{{ schoolProfile.data.first_name || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('Middle name:')}}</span>
<span class="text-gray-900">{{ schoolProfile.data.middle_name || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('Date of birth:')}}</span>
<span class="text-gray-900">{{ formattedDate(schoolProfile.data.birth_date) || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('Phone:')}}</span>
<span class="text-gray-900 font-mono">{{ maskPrivate(schoolProfile.data.phone) }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">Email:</span>
<span class="text-gray-900 font-mono">{{ maskPrivate(schoolProfile.data.email_private) }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">Telegram:</span>
<div class="flex-1">
<a v-if="schoolProfile.data.telegram"
:href="formatTelegram(schoolProfile.data.telegram)"
target="_blank"
class="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium transition-colors">
<span>@{{ schoolProfile.data.telegram.replace('@', '') }}</span>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.06-.2-.07-.06-.17-.04-.24-.02-.1.02-1.69 1.09-4.78 3.2-.45.31-.86.46-1.23.45-.41-.01-1.2-.23-1.79-.42-.72-.23-1.29-.36-1.24-.76.03-.24.37-.48 1.01-.74 3.97-1.67 6.62-2.77 7.94-3.31 3.26-1.33 3.94-1.56 4.38-1.56.08 0 .27.02.39.12.1.08.13.19.14.27-.01.06.01.24 0 .38z"/>
</svg>
</a>
<span v-else class="text-gray-500">—</span>
</div>
</div>
</div>
<div class="space-y-4">
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('University:')}}</span>
<span class="text-gray-900">{{ schoolProfile.data.school || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('Education level:')}}</span>
<span class="text-gray-900">{{ schoolProfile.data.education_level || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('The direction of training:')}}</span>
<span class="text-gray-900">{{ schoolProfile.data.major || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('Educational program:')}}</span>
<span class="text-gray-900">{{ schoolProfile.data.program || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('Course:')}}</span>
<span class="text-gray-900">{{ schoolProfile.data.course || '—' }}</span>
</div>
<div class="flex items-start">
<span class="inline-block w-40 text-gray-700 font-medium">{{__('The head of the group:')}}</span>
<span class="text-gray-900">
<span :class="schoolProfile.data.group_leader === 'Да' ? 'text-green-600' : 'text-gray-600'">
{{ schoolProfile.data.group_leader || '—' }}
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- О себе -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden lg:col-span-2">
<div class="px-6 py-4 border-b border-gray-100 bg-teal-400">
<h3 class="text-xl font-semibold text-white">{{__('Briefly about your interests')}}</h3>
</div>
<div class="p-6">
<p class="text-gray-700 leading-relaxed whitespace-pre-line">{{ schoolProfile.data.interests || __('No information is provided') }}</p>
</div>
</div>
<div class="space-y-6">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-teal-400">
<h3 class="text-xl font-semibold text-white">{{__('About me')}}</h3>
</div>
<div class="p-6">
<p class="text-gray-700 leading-relaxed whitespace-pre-line">{{ schoolProfile.data.about_me || __('No information is provided') }}</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-teal-400">
<h3 class="text-xl font-semibold text-white">{{__('About dreams')}}</h3>
</div>
<div class="p-6">
<p class="text-gray-700 leading-relaxed whitespace-pre-line">{{ schoolProfile.data.dreams || __('No information is provided') }}</p>
</div>
</div>
</div>
</div>
</div>
<div v-else class="bg-white rounded-2xl shadow-sm border border-gray-200 p-8">
<div class="text-center py-12">
<div class="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">{{__('Profile data was not found')}}</h3>
<p class="text-gray-600">{{__('There is no information about the student')}}</p>
</div>
</div>
</div>
<!-- EDIT MODE -->
<div v-else class="mt-6">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-teal-400">
<h3 class="text-xl font-semibold text-white">{{__('Profile Editing')}}</h3>
<p class="text-sm text-gray-200 mt-1">{{__('Fill in the information about yourself')}}</p>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Левая колонка -->
<div class="space-y-6">
<h4 class="text-lg font-semibold text-gray-900 border-b pb-2">{{__('Personal information')}}</h4>
<Input
v-model="form.last_name"
:label="__('Last name:')"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
<Input
v-model="form.first_name"
:label="__('Name:')"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
<Input
v-model="form.middle_name"
:label="__('Middle name:')"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{__('Date of birth:')}}</label>
<DatePicker
v-model="form.birth_date"
class="w-full bg-gray-50 border-gray-300 rounded-lg focus:border-primary-500 focus:ring-primary-500"
/>
</div>
<Input
v-model="form.phone"
:label="__('Phone:')"
placeholder="+7 (XXX) XXX-XX-XX"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
<Input
v-model="form.email_private"
label="Email:"
type="email"
placeholder="example@email.com"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
<Input
v-model="form.telegram"
label="Telegram:"
placeholder="username или t.me/username"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
</div>
<!-- Правая колонка -->
<div class="space-y-6">
<h4 class="text-lg font-semibold text-gray-900 border-b pb-2">Образование</h4>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{__('University:')}}</label>
<input
type="text"
v-model="schoolQuery"
@input="debouncedSearchSchool"
class="w-full bg-gray-50 border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors"
placeholder="Начните вводить название университета"
/>
<div v-if="schoolResults.length" class="mt-2 border border-gray-300 rounded-lg overflow-hidden shadow-lg bg-white">
<div
v-for="s in schoolResults"
:key="s.school"
class="p-3 cursor-pointer hover:bg-primary-50 border-b border-gray-100 last:border-b-0 transition-colors"
@click="selectSchool(s)"
>
<div class="font-medium text-gray-900">{{ s.school }}</div>
<div class="text-xs text-gray-500 mt-1">{{ s.adress }}</div>
</div>
</div>
<div v-if="form.school_name && !schoolResults.length" class="mt-2 text-sm text-gray-600">
<span class="font-medium">Выбрана:</span> {{ form.school }}
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{__('Education level:')}}</label>
<Select
v-model="form.education_level"
:options="['Бакалавриат','Магистратура','Аспирантура','Базовое высшее образование','Специализированное высшее образование','Профессиональная переподготовка','Повышение квалификации']"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{__('Educational program:')}}</label>
<input
type="text"
v-model="majorQuery"
@input="debouncedSearchMajor"
class="w-full bg-gray-50 border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-colors"
placeholder="Начните вводить название направления"
/>
<div v-if="majorResults.length" class="mt-2 border border-gray-300 rounded-lg overflow-hidden shadow-lg bg-white">
<div
v-for="m in majorResults"
:key="m.major"
class="p-3 cursor-pointer hover:bg-primary-50 border-b border-gray-100 last:border-b-0 transition-colors"
@click="selectMajor(m)"
>
<div class="font-medium text-gray-900">{{ m.major_name }}</div>
</div>
</div>
<div v-if="form.major_name && !majorResults.length" class="mt-2 text-sm text-gray-600">
<span class="font-medium">Выбрано:</span> {{ form.major_name }}
</div>
</div>
<Input
v-model="form.program"
label="Образовательная программа"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{__('Course:')}}</label>
<Select
v-model="form.course"
:options="['1','2','3','4','5','6']"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500 w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{__('The head of the group:')}}</label>
<Select
v-model="form.group_leader"
:options="['Да','Нет']"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500 w-full"
/>
</div>
</div>
</div>
</div>
<!-- Текстовые поля -->
<div class="mt-8 space-y-6">
<h4 class="text-lg font-semibold text-gray-900 border-b pb-2">Дополнительная информация</h4>
<Textarea
v-model="form.interests"
label="Коротко о своих интересах (2-3 предложения)"
rows="4"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
<Textarea
v-model="form.about_me"
label="Коротко о себе"
rows="4"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
<Textarea
v-model="form.dreams"
label="Коротко о своих мечтах"
rows="4"
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
/>
</div>
<!-- Кнопки действий -->
<div class="mt-8 pt-6 border-t border-gray-200 flex gap-3">
<Button
@click="saveProfile"
:loading="saving"
class="bg-teal-400 hover:bg-teal-700 text-white px-8 py-3 rounded-lg font-medium transition-colors duration-200 flex items-center gap-2"
>
<!-- <svg v-if="!saving" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg> -->
{{ saving ? 'Сохранение...' : 'Сохранить изменения' }}
</Button>
<Button
variant="outline"
@click="toggleEdit()"
class="border-gray-300 text-gray-700 hover:bg-gray-50 px-6 py-3 rounded-lg font-medium transition-colors duration-200"
>
Отмена
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="flex items-center justify-center min-h-screen">
<div class="text-center">
<div class="animate-spin rounded-full h-16 w-16 border-b-2 border-primary-600 mx-auto"></div>
<p class="mt-4 text-lg text-gray-600">Загрузка профиля...</p>
</div>
</div>
</div>
</template>
<style scoped>
/* Плавные переходы для интерактивных элементов */
.border-gray-300 {
transition: border-color 0.2s ease;
}
.bg-primary-50 {
background-color: rgba(59, 130, 246, 0.05);
}
/* Стилизация скроллбара для выпадающих списков */
.overflow-auto::-webkit-scrollbar {
width: 6px;
}
.overflow-auto::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.overflow-auto::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.overflow-auto::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
</style>
<script setup>
import { ref, computed, inject, watch, onMounted } from 'vue';
import { Breadcrumbs, createResource, Button, Input, DatePicker, Select, Textarea } from 'frappe-ui';
import { sessionStore } from '@/stores/session';
import NoPermission from '@/components/NoPermission.vue';
import { Edit } from 'lucide-vue-next';
import { convertToTitleCase, updateDocumentTitle } from '@/utils';
import debounce from 'lodash/debounce';
import { decodeEntities } from '@/utils'
import DOMPurify from 'dompurify'
const { user } = sessionStore();
const $user = inject('$user');
const schoolProfileNotFound = ref(false);
// Логирование инициализации
console.log('[DEBUG] Инициализация компонента:', {
user: user,
$user: $user.data,
username: $user.data?.username,
});
const props = defineProps({
username: {
type: String,
required: false,
default: '',
},
});
const effectiveUsername = computed(() => {
const username = props.username || $user.data?.username || '';
console.log('[DEBUG] Вычисление effectiveUsername:', { propsUsername: props.username, sessionUsername: $user.data?.username, result: username });
return username;
});
const editMode = ref(false);
const saving = ref(false);
const profile = createResource({
url: 'frappe.client.get',
makeParams(values) {
const username = effectiveUsername.value;
console.log('[DEBUG] Запрос profile:', { doctype: 'User', filters: { username } });
return {
doctype: 'User',
filters: { username },
};
},
onSuccess(data) {
console.log('[DEBUG] Профиль загружен:', data);
},
onError(error) {
console.error('[DEBUG] Ошибка загрузки профиля:', error);
window.frappe?.msgprint({
title: 'Ошибка',
message: 'Не удалось загрузить профиль пользователя: ' + (error.message || 'Неизвестная ошибка'),
indicator: 'red',
});
},
});
const schoolProfile = createResource({
url: 'frappe.client.get',
params: {
doctype: 'Schoolchildren Profile',
filters: { user:user },
},
auto: false,
onSuccess(data) {
console.log('[DEBUG] Профиль школьника загружен:', data);
},
onError(error) {
// Проверяем, является ли ошибка "не найдено"
if (error.exc_type === 'DoesNotExistError' || error.message?.includes('DoesNotExist')) {
console.log('[DEBUG] Профиль школьника не найден, создаем новый');
schoolProfileNotFound.value = true;
} else {
console.error('[DEBUG] Ошибка загрузки профиля школьника:', error);
window.frappe?.msgprint({
title: 'Ошибка',
message: 'Не удалось загрузить профиль школьника: ' + (error.message || 'Неизвестная ошибка'),
indicator: 'red',
});
}
},
});
const form = ref({
first_name: '',
last_name: '',
middle_name: '',
birth_date: '',
phone: '',
email_private: '',
telegram: '',
school: '',
education_level: '',
major: '',
program: '',
course: '',
group_leader: '',
interests: '',
about_me: '',
dreams: ''
});
const breadcrumbs = computed(() => {
const username = effectiveUsername.value;
const crumbs = [
{
label: 'People',
route: { name: 'People' },
},
{
label: profile.data?.full_name || 'Профиль',
route: username ? {
name: 'Profile',
params: { username },
} : undefined,
},
];
console.log('[DEBUG] Хлебные крошки:', crumbs);
return crumbs;
});
const pageMeta = computed(() => {
const meta = {
title: profile.data?.full_name || 'Профиль',
description: profile.data?.headline || '',
};
console.log('[DEBUG] Мета-данные страницы:', meta);
return meta;
});
const displayName = computed(() => {
if (!profile.data) {
console.log('[DEBUG] displayName: profile.data не загружен');
return 'Загрузка...';
}
const name = profile.data?.full_name || `${form.value.first_name || ''} ${form.value.last_name || ''}`.trim();
console.log('[DEBUG] Отображаемое имя:', name);
return name;
});
const isSessionUser = () => {
const sessionUser = $user.data?.username;
const profileUser = effectiveUsername.value;
const isSession = sessionUser === profileUser;
console.log('[DEBUG] Проверка isSessionUser:', { sessionUser, profileUser, isSession });
return isSession;
};
function formattedDate(d) {
if (!d) return '';
try {
return new Date(d).toLocaleDateString('ru-RU');
} catch (e) {
console.error('[DEBUG] Ошибка форматирования даты:', e, { date: d });
return d;
}
}
function maskPrivate(val) {
if (!val) return '-';
if (val.includes('@')) {
const parts = val.split('@');
return parts[0].slice(0, 1) + '***@' + parts[1];
}
return val.slice(0, 3) + '***' + val.slice(-2);
}
function formatTelegram(t) {
if (!t) return '';
if (t.startsWith('t.me/') || t.startsWith('https://t.me/')) return (t.startsWith('http') ? t : 'https://' + t);
return 'https://t.me/' + t.replace(/^@/, '');
}
function fillFormFromProfile() {
console.log('[DEBUG] Заполнение формы:', {
schoolProfile: schoolProfile.data,
profile: profile.data,
currentForm: JSON.stringify(form.value, null, 2),
});
form.value.first_name = schoolProfile.data?.first_name || profile.data?.first_name || '';
form.value.last_name = schoolProfile.data?.last_name || profile.data?.last_name || '';
form.value.middle_name = schoolProfile.data?.middle_name || '';
form.value.birth_date = schoolProfile.data?.birth_date || '';
form.value.phone = schoolProfile.data?.phone || '';
form.value.email_private = schoolProfile.data?.email_private || '';
form.value.telegram = schoolProfile.data?.telegram || '';
form.value.school = schoolProfile.data?.school || '';
form.value.education_level = schoolProfile.data?.education_level || '';
form.value.major = schoolProfile.data?.major || '';
form.value.program = schoolProfile.data?.program || '';
form.value.course = schoolProfile.data?.course || '';
form.value.group_leader = schoolProfile.data?.group_leader || '';
form.value.interests = schoolProfile.data?.interests || '';
form.value.about_me = schoolProfile.data?.about_me || '';
form.value.dreams = schoolProfile.data?.dreams || '';
console.log('[DEBUG] Форма после заполнения:', JSON.stringify(form.value, null, 2));
}
function toggleEdit() {
editMode.value = !editMode.value;
if (editMode.value) fillFormFromProfile();
console.log('[DEBUG] Переключение режима редактирования:', { editMode: editMode.value });
}
function validateExams(exams) {
console.log('[DEBUG] Валидация exams:', { exams, validOptions: examOptions });
return exams.every(exam => examOptions.includes(exam));
}
function validateLearnSubjects(subjects) {
console.log('[DEBUG] Валидация learn_subjects:', { subjects, validOptions: learnOptions });
return subjects.every(subject => learnOptions.includes(subject));
}
async function saveProfile() {
console.log('[DEBUG] Сохранение профиля:', { form: form.value });
saving.value = true;
try {
// Создаём копию данных формы
const formData = { ...form.value };
console.log('[DEBUG] Копия formData:', JSON.stringify(formData, null, 2));
// Обновление full_name в User, если нужно
if (formData.first_name || formData.last_name) {
const fullName = `${formData.first_name || ''} ${formData.last_name || ''}`.trim();
console.log('[DEBUG] Обновление User.full_name:', { name: profile.data?.name, fullName });
await createResource({
url: 'frappe.client.set_value',
params: {
doctype: 'User',
name: profile.data?.name,
fieldname: 'full_name',
value: fullName,
},
}).submit();
}
// Получаем docname
let docname = '';
try {
await schoolProfile.reload();
console.log('[DEBUG] Schoolprofile:', { schoolProfile });
docname = schoolProfile?.data?.name;
console.log('[DEBUG] Выбранное имя документа:', docname);
} catch (error) {
console.log('[DEBUG] Ошибка загрузки schoolProfile, продолжаем с profile:', error.message);
}
// Формируем payload из копии данных формы
let payload = {
doctype: 'Schoolchildren Profile',
user: profile.data?.name,
first_name: formData.first_name,
last_name: formData.last_name,
middle_name: formData.middle_name,
birth_date: formData.birth_date,
phone: formData.phone,
email_private: formData.email_private,
telegram: formData.telegram,
school: formData.school || '',
education_level: formData.education_level,
major: formData.major || '',
program: formData.program,
course: formData.course,
group_leader: formData.group_leader,
interests: formData.interests,
about_me: formData.about_me,
dreams: formData.dreams,
last_updated: new Date().toISOString(),
};
console.log('[DEBUG] Сохранение Schoolchildren Profile (payload):', { docname, payload });
// Сохранение или создание документа
if (docname) {
await createResource({
url: 'frappe.client.save',
params: { doc: { ...schoolProfile.data, ...payload } },
}).submit();
} else {
await createResource({
url: 'frappe.client.insert',
params: { doc: payload },
}).submit();
}
editMode.value = false;
schoolProfileNotFound.value = false;
if (window.frappe && window.frappe.msgprint) window.frappe.msgprint('Профиль сохранён');
console.log('[DEBUG] Профиль успешно сохранён');
} catch (e) {
console.error('[DEBUG] Ошибка при сохранении профиля:', e);
if (window.frappe && window.frappe.msgprint) window.frappe.msgprint({
title: 'Ошибка',
message: (e && e.message) || 'Ошибка при сохранении',
indicator: 'red',
});
} finally {
saving.value = false;
}
await schoolProfile.reload();
}
const schoolQuery = ref('');
const schoolResults = ref([]);
async function searchSchool(q) {
if (!q) {
schoolResults.value = [];
return;
}
try {
console.log('[DEBUG] Поиск школы:', { query: q });
const res = await createResource({
url: 'frappe.client.get_list',
params: {
doctype: 'Schools',
fields: ['school', 'address'],
filters: [['school', 'like', '%' + q + '%']],
limit_page_length: 20,
},
}).submit();
schoolResults.value = res || [];
console.log('[DEBUG] Результаты поиска школы:', schoolResults.value);
} catch (e) {
schoolResults.value = [];
console.error('[DEBUG] Ошибка поиска школы:', e);
}
}
const debouncedSearchSchool = debounce(() => searchSchool(schoolQuery.value), 300);
function selectSchool(s) {
form.value.school = s.school;
//form.value.school_name = s.school_name;
schoolResults.value = [];
schoolQuery.value = s.school;
console.log('[DEBUG] Выбрана школа:', { school: s });
console.log('[DEBUG] Форма после заполнения:', JSON.stringify(form.value, null, 2));
}
const majorQuery = ref('');
const majorResults = ref([]);
async function searchMajor(q) {
if (!q) {
majorResults.value = [];
return;
}
try {
console.log('[DEBUG] Поиск направления:', { query: q });
const res = await createResource({
url: 'frappe.client.get_list',
params: {
doctype: 'Majors',
fields: ['code', 'major_name'],
filters: [['major_name', 'like', '%' + q + '%']],
limit_page_length: 20,
},
}).submit();
majorResults.value = res || [];
console.log('[DEBUG] Результаты поиска направления:', majorResults.value);
} catch (e) {
majorResults.value = [];
console.error('[DEBUG] Ошибка поиска направления:', e);
}
}
const debouncedSearchMajor = debounce(() => searchMajor(majorQuery.value), 300);
function selectMajor(m) {
form.value.major = m.major_name;
//form.value.school_name = s.school_name;
majorResults.value = [];
majorQuery.value = m.major_name;
console.log('[DEBUG] Выбрана школа:', { major: m });
console.log('[DEBUG] Форма после заполнения:', JSON.stringify(form.value, null, 2));
}
onMounted(() => {
console.log('[DEBUG] Компонент смонтирован:', {
propsUsername: props.username,
sessionUsername: $user.data?.username,
user: user,
$user: $user.data,
});
if ($user.data) {
console.log('[DEBUG] Запуск profile.reload()');
profile.reload();
}
});
watch(
() => props.username,
(newUsername, oldUsername) => {
console.log('[DEBUG] Изменение props.username:', { old: oldUsername, new: newUsername });
profile.reload();
}
);
watch(
() => profile.data,
(newData, oldData) => {
console.log('[DEBUG] Изменение profile.data:', { old: oldData, new: newData });
if (newData) {
console.log('[DEBUG] Запуск schoolProfile.reload()');
schoolProfile.reload();
}
}
);
watch(
() => schoolProfile.data,
(newData, oldData) => {
console.log('[DEBUG] Изменение schoolProfile.data:', { old: oldData, new: newData });
if (newData && !editMode.value && !schoolProfileNotFound.value) {
console.log('[DEBUG] Заполнение формы из schoolProfile');
fillFormFromProfile();
}
}
);
watch(
() => effectiveUsername.value,
(newUsername) => {
console.log('[DEBUG] Изменение effectiveUsername для schoolProfile:', newUsername);
schoolProfile.update({
params: {
doctype: 'Schoolchildren Profile',
filters: { user: newUsername },
},
});
}
);
</script>