852 lines
35 KiB
Vue
852 lines
35 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-surface-gray-1">
|
||
<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">Ошибка загрузки профиля: {{ profile.error.message }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="profile.data">
|
||
<header class="sticky top-0 z-10 flex items-center justify-between border-b border-gray-200 bg-white px-6 py-4 shadow-sm">
|
||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||
</header>
|
||
|
||
<div class="mx-auto max-w-6xl px-4 py-6">
|
||
<!-- Profile Header -->
|
||
<div class="bg-white 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>
|
||
<p class="mt-2 text-lg text-gray-600">{{ profile.data.headline || 'Нет информации о себе' }}</p>
|
||
</div>
|
||
|
||
<div v-if="$user.data && isSessionUser()" class="md:ml-auto">
|
||
<Button @click="toggleEdit()" class="bg-primary-600 hover:bg-primary-700 text-white px-5 py-2.5 rounded-lg transition-colors duration-200">
|
||
<template #prefix>
|
||
<Edit class="w-4 h-4 stroke-1.5" />
|
||
</template>
|
||
{{ editMode ? 'Отменить редактирование' : 'Редактировать профиль' }}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- VIEW MODE -->
|
||
<div v-if="!editMode" class="mt-6">
|
||
<div v-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>
|
||
|
||
<div v-else-if="schoolProfile.error" class="bg-white rounded-2xl shadow-sm border border-red-200 p-6">
|
||
<p class="text-red-500 text-lg font-medium">Ошибка загрузки данных студента: {{ schoolProfile.error.message }}</p>
|
||
</div>
|
||
|
||
<div v-else-if="schoolProfile.data" 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-gray-50">
|
||
<h3 class="text-xl font-semibold text-gray-900">Основная информация</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">Фамилия:</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">Имя:</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">Отчество:</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">Дата рождения:</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">Телефон:</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="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||
</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">Университет:</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">Уровень образования:</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">Направление подготовки:</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">Образовательная программа:</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">Курс:</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">Староста в группе:</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-gray-50">
|
||
<h3 class="text-xl font-semibold text-gray-900">Коротко о своих интересах</h3>
|
||
</div>
|
||
<div class="p-6">
|
||
<p class="text-gray-700 leading-relaxed whitespace-pre-line">{{ schoolProfile.data.interests || 'Информация не указана' }}</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-gray-50">
|
||
<h3 class="text-xl font-semibold text-gray-900">О себе</h3>
|
||
</div>
|
||
<div class="p-6">
|
||
<p class="text-gray-700 leading-relaxed whitespace-pre-line">{{ schoolProfile.data.about_me || 'Информация не указана' }}</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-gray-50">
|
||
<h3 class="text-xl font-semibold text-gray-900">О мечтах</h3>
|
||
</div>
|
||
<div class="p-6">
|
||
<p class="text-gray-700 leading-relaxed whitespace-pre-line">{{ schoolProfile.data.dreams || 'Информация не указана' }}</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">Данные профиля не найдены</h3>
|
||
<p class="text-gray-600">Информация о студенте отсутствует</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-gray-50">
|
||
<h3 class="text-xl font-semibold text-gray-900">Редактирование профиля</h3>
|
||
<p class="text-sm text-gray-600 mt-1">Заполните информацию о себе</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">Личная информация</h4>
|
||
|
||
<Input
|
||
v-model="form.last_name"
|
||
label="Фамилия"
|
||
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
|
||
/>
|
||
|
||
<Input
|
||
v-model="form.first_name"
|
||
label="Имя"
|
||
class="bg-gray-50 border-gray-300 focus:border-primary-500 focus:ring-primary-500"
|
||
/>
|
||
|
||
<Input
|
||
v-model="form.middle_name"
|
||
label="Отчество"
|
||
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">Дата рождения</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="Телефон (не публиковать)"
|
||
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">Университет</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">Уровень образования</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">Направление подготовки</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">Курс</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">Староста в группе</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-primary-600 hover:bg-primary-700 text-white px-6 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';
|
||
|
||
const { user } = sessionStore();
|
||
const $user = inject('$user');
|
||
|
||
// Логирование инициализации
|
||
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) {
|
||
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;
|
||
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) {
|
||
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>
|