diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 402a52c3..f65addb5 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -271,6 +271,14 @@ html[data-bs-theme="dark"] .app-input::-webkit-calendar-picker-indicator { color: var(--muted-foreground); } +.app-card-caption { + color: var(--muted-foreground); + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0.025em; + text-transform: uppercase; +} + .app-divider { border-color: var(--border-subtle); } diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index 7213b42f..a820ec50 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -1,430 +1,801 @@ "use client"; import Image from "next/image"; -import Link from "next/link"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { AppShell } from "../../components/AppShell"; +import { useEffect, useMemo, useState } from "react"; +import { + Mail, + MessageCircle, + Phone, + Plus, + RefreshCw, + X, +} from "lucide-react"; + +import { AppShell } from "@/components/AppShell"; import { useUser } from "@/contexts/UserContext"; import { apiClient } from "@/lib/api"; import { resolveMediaUrl } from "@/lib/url"; -import { Camera, Pencil, Save, X } from "lucide-react"; +import { getWorkDuration } from "@/lib/users/userDetailUtils"; +import type { DirectoryLoginResult } from "@/types/api"; function formatDate(value?: string): string { - if (!value) return ""; - const d = new Date(value); - if (Number.isNaN(d.getTime())) return ""; - return d.toLocaleString("ru-RU", { - day: "2-digit", - month: "2-digit", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }); + if (!value) return "—"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleDateString("ru-RU", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +} + +function formatDateTime(value?: string): string { + if (!value) return "—"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString("ru-RU", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatBirthdayWithYear(value?: string): string | null { + if (!value) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleDateString("ru-RU", { + day: "numeric", + month: "long", + year: "numeric", + }); +} + +function getActionTimestamp(action: { + date?: string; + created_at?: string; +}): number { + return new Date(action.date || action.created_at || 0).getTime(); +} + +function initials(firstName?: string, lastName?: string) { + return `${lastName?.[0] || ""}${firstName?.[0] || ""}`.trim() || "П"; +} + +function normalizeTelegram(value: string) { + const trimmed = value.trim(); + if (!trimmed) return ""; + if (/^https?:\/\//i.test(trimmed)) return trimmed; + return `https://t.me/${trimmed.replace(/^@/, "")}`; +} + +function normalizeWhatsApp(value: string) { + const trimmed = value.trim(); + if (!trimmed) return ""; + if (/^https?:\/\//i.test(trimmed)) return trimmed; + const digits = trimmed.replace(/[^\d]/g, ""); + return digits ? `https://wa.me/${digits}` : ""; +} + +function truncateText(value?: string, maxLength = 120) { + if (!value) return ""; + if (value.length <= maxLength) return value; + return `${value.slice(0, maxLength).trimEnd()}…`; +} + +function getActionTone(action?: string) { + switch (action) { + case "on_leave": + case "on_maternity": + return { + badgeClass: "app-feedback-warning", + lineClass: "bg-amber-400/80", + }; + case "dismissed": + return { + badgeClass: "app-feedback-danger", + lineClass: "bg-red-400/80", + }; + case "transferred": + return { + badgeClass: "app-selected", + lineClass: "bg-sky-400/80", + }; + default: + return { + badgeClass: "app-feedback-success", + lineClass: "bg-emerald-400/80", + }; + } +} + +function SectionTitle({ + title, + icon, + action, +}: { + title: string; + icon?: React.ReactNode; + action?: React.ReactNode; +}) { + return ( +
+
+ {icon} +

{title}

+
+ {action} +
+ ); } export default function ProfilePage() { - const { user, loading, refreshUser } = useUser(); - const avatarInputRef = useRef(null); - const [editProfileOpen, setEditProfileOpen] = useState(false); - const [editContactsOpen, setEditContactsOpen] = useState(false); - const [savingProfile, setSavingProfile] = useState(false); - const [savingContacts, setSavingContacts] = useState(false); - const [profileError, setProfileError] = useState(null); - const [contactsError, setContactsError] = useState(null); - const [avatarFile, setAvatarFile] = useState(null); - const [profileForm, setProfileForm] = useState({ - last_name: "", - first_name: "", - patronymic: "", - birth_date: "", - }); - const [contactsForm, setContactsForm] = useState({ - email: "", - phone_number: "", - telegram: "", - whatsapp: "", - wechat: "", - }); + const { user, loading } = useUser(); + const [showAllActions, setShowAllActions] = useState(false); + const [directoryLogin, setDirectoryLogin] = useState(null); + const [directoryLoginLoading, setDirectoryLoginLoading] = useState(false); + const [directoryLoginRefreshing, setDirectoryLoginRefreshing] = + useState(false); + const [directoryLoginError, setDirectoryLoginError] = useState( + null, + ); + const [availableSkills, setAvailableSkills] = useState< + Array<{ id: number; name: string; description?: string }> + >([]); + const [profileSkills, setProfileSkills] = useState< + Array<{ id: number; name: string; description?: string }> + >([]); + const [skillsLoading, setSkillsLoading] = useState(false); + const [skillsSaving, setSkillsSaving] = useState(false); + const [skillName, setSkillName] = useState(""); + const [skillsError, setSkillsError] = useState(null); - const fullName = useMemo(() => { - if (!user) return ""; - return `${user.last_name || ""} ${user.first_name || ""} ${user.patronymic || ""}`.trim() || "Пользователь"; - }, [user]); + const fullName = useMemo(() => { + if (!user) return "Пользователь"; + return `${user.last_name || ""} ${user.first_name || ""} ${ + user.patronymic || "" + }`.trim() || "Пользователь"; + }, [user]); - const initials = useMemo(() => { - if (!user) return "П"; - const s = `${user.last_name?.[0] || ""}${user.first_name?.[0] || ""}`.trim(); - return s || "П"; - }, [user]); + const currentAction = useMemo(() => { + if (!user?.actions?.length) return null; + const now = Date.now(); + return [...user.actions] + .filter((action) => getActionTimestamp(action) <= now) + .sort((left, right) => getActionTimestamp(right) - getActionTimestamp(left))[0] || null; + }, [user?.actions]); - const departments = user?.departments || []; - const skills = user?.skills || []; - const actions = user?.actions || []; + const sortedActions = useMemo(() => { + if (!user?.actions?.length) return []; + return [...user.actions].sort( + (left, right) => getActionTimestamp(right) - getActionTimestamp(left), + ); + }, [user?.actions]); - const birthDateLabel = useMemo(() => { - if (!user?.birth_date) return "—"; - const d = new Date(user.birth_date); - if (Number.isNaN(d.getTime())) return user.birth_date; - return d.toLocaleDateString("ru-RU", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }); - }, [user?.birth_date]); + const currentActionId = useMemo( + () => currentAction?.id ?? null, + [currentAction?.id], + ); - useEffect(() => { - if (!user) return; - setProfileForm({ - last_name: user.last_name || "", - first_name: user.first_name || "", - patronymic: user.patronymic || "", - birth_date: user.birth_date || "", - }); - setContactsForm({ - email: user.email || "", - phone_number: user.phone_number || "", - telegram: user.telegram || "", - whatsapp: user.whatsapp || "", - wechat: user.wechat || "", - }); - }, [user]); + const visibleActions = useMemo( + () => (showAllActions ? sortedActions : sortedActions.slice(0, 3)), + [showAllActions, sortedActions], + ); - const handleSaveProfile = async () => { - try { - setSavingProfile(true); - setProfileError(null); + const normalizedSkillName = skillName.trim().toLowerCase(); - await apiClient.updateCurrentUserProfile({ - last_name: profileForm.last_name.trim(), - first_name: profileForm.first_name.trim(), - patronymic: profileForm.patronymic.trim(), - birth_date: profileForm.birth_date.trim() || null, - avatar: avatarFile || undefined, - }); + const selectedSkillIds = useMemo( + () => new Set(profileSkills.map((skill) => skill.id)), + [profileSkills], + ); - await refreshUser(); - setAvatarFile(null); - setEditProfileOpen(false); - } catch (e: any) { - setProfileError(String(e?.message || "Не удалось сохранить профиль")); - } finally { - setSavingProfile(false); - } - }; + const suggestedSkills = useMemo( + () => + availableSkills + .filter((skill) => !selectedSkillIds.has(skill.id)) + .filter((skill) => + normalizedSkillName + ? skill.name.toLowerCase().includes(normalizedSkillName) + : true, + ) + .slice(0, 10), + [availableSkills, normalizedSkillName, selectedSkillIds], + ); - const handleSaveContacts = async () => { - try { - setSavingContacts(true); - setContactsError(null); + useEffect(() => { + setDirectoryLogin(user?.username?.trim() || null); + setDirectoryLoginError(null); + }, [user?.username]); - await apiClient.updateCurrentUserProfile({ - email: contactsForm.email.trim(), - phone_number: contactsForm.phone_number.trim(), - telegram: contactsForm.telegram.trim(), - whatsapp: contactsForm.whatsapp.trim(), - wechat: contactsForm.wechat.trim(), - }); + useEffect(() => { + setProfileSkills(user?.skills || []); + }, [user?.skills]); - await refreshUser(); - setEditContactsOpen(false); - } catch (e: any) { - setContactsError(String(e?.message || "Не удалось сохранить контакты")); - } finally { - setSavingContacts(false); - } - }; + useEffect(() => { + let mounted = true; - if (loading) { - return ( - -
-
-
-

Загрузка профиля...

-
-
- - ); - } + async function loadDirectoryLogin() { + if (!user?.is_ldap_managed || user.username?.trim()) { + return; + } - if (!user) { - return null; - } + try { + setDirectoryLoginLoading(true); + setDirectoryLoginError(null); + const response = + (await apiClient.getDirectoryLogin()) as DirectoryLoginResult; + if (!mounted) return; + setDirectoryLogin(response.username?.trim() || null); + } catch (error: any) { + if (!mounted) return; + setDirectoryLoginError( + String(error?.message || "Не удалось получить логин из каталога"), + ); + } finally { + if (mounted) { + setDirectoryLoginLoading(false); + } + } + } - const normalizeTelegram = (value: string) => { - const v = value.trim(); - if (!v) return ""; - if (/^https?:\/\//i.test(v)) return v; - return `https://t.me/${v.replace(/^@/, "")}`; - }; + void loadDirectoryLogin(); - const normalizeWhatsApp = (value: string) => { - const v = value.trim(); - if (!v) return ""; - if (/^https?:\/\//i.test(v)) return v; - const digits = v.replace(/[^\d]/g, ""); - return digits ? `https://wa.me/${digits}` : ""; - }; + return () => { + mounted = false; + }; + }, [user?.id, user?.is_ldap_managed, user?.username]); - const contactItems = [ - { label: "Телефон", value: user.phone_number?.trim() || "", href: user.phone_number?.trim() ? `tel:${user.phone_number.trim().replace(/\s+/g, "")}` : "", external: false }, - { label: "Почта", value: user.email?.trim() || "", href: user.email?.trim() ? `mailto:${user.email.trim()}` : "", external: false }, - { label: "Telegram", value: user.telegram?.trim() || "", href: normalizeTelegram(user.telegram || ""), external: true }, - { label: "WhatsApp", value: user.whatsapp?.trim() || "", href: normalizeWhatsApp(user.whatsapp || ""), external: true }, - { label: "WeChat", value: user.wechat?.trim() || "", href: "", external: false }, - ].filter((item) => item.value); + useEffect(() => { + let mounted = true; - const editableContactItems = [ - { label: "Телефон", key: "phone_number" as const }, - { label: "Почта", key: "email" as const }, - { label: "Telegram", key: "telegram" as const }, - { label: "WhatsApp", key: "whatsapp" as const }, - { label: "WeChat", key: "wechat" as const }, - ]; + async function loadSkills() { + try { + setSkillsLoading(true); + const response = (await apiClient.getSkills()) as Array<{ + id: number; + name: string; + description?: string; + }>; + if (!mounted) return; + setAvailableSkills(response); + } catch (error: any) { + if (!mounted) return; + setSkillsError( + String(error?.message || "Не удалось загрузить список навыков"), + ); + } finally { + if (mounted) { + setSkillsLoading(false); + } + } + } - return ( - -
-
-
-
-
- {user.avatar ? ( - - {fullName} - - ) : ( -
{initials}
- )} -
- { - setAvatarFile(e.target.files?.[0] || null); - setEditProfileOpen(true); - }} - className="hidden" - /> - {editProfileOpen ? ( - - ) : null} -
+ void loadSkills(); -
-
- {editProfileOpen ? ( -
- setProfileForm((p) => ({ ...p, last_name: e.target.value }))} placeholder="Фамилия" className="rounded-lg border border-gray-300 px-3 py-2 text-sm" /> - setProfileForm((p) => ({ ...p, first_name: e.target.value }))} placeholder="Имя" className="rounded-lg border border-gray-300 px-3 py-2 text-sm" /> - setProfileForm((p) => ({ ...p, patronymic: e.target.value }))} placeholder="Отчество" className="rounded-lg border border-gray-300 px-3 py-2 text-sm" /> -
- ) : ( -

{fullName}

- )} - -
- {editProfileOpen ? ( -
- Дата рождения: - setProfileForm((p) => ({ ...p, birth_date: e.target.value }))} className="rounded-lg border border-gray-300 px-2 py-1 text-sm" /> -
- ) : ( -

Дата рождения: {birthDateLabel}

- )} -

Должность: {user.position?.name || "—"}

+ return () => { + mounted = false; + }; + }, []); -
- {departments.length === 0 ? ( -

Отделы не указаны

- ) : ( - departments.map((d, index) => { - const deptId = Number((d as any).id ?? (d as any).department_id); - const isValidDeptId = Number.isFinite(deptId) && deptId > 0; + const handleRefreshDirectoryLogin = async () => { + try { + setDirectoryLoginRefreshing(true); + setDirectoryLoginError(null); + const response = + (await apiClient.refreshDirectoryLogin()) as DirectoryLoginResult; + setDirectoryLogin(response.username?.trim() || null); + } catch (error: any) { + setDirectoryLoginError( + String(error?.message || "Не удалось обновить логин из каталога"), + ); + } finally { + setDirectoryLoginRefreshing(false); + } + }; - return isValidDeptId ? ( - -
-

{d.name}

- {d.role_name ?

{d.role_name}

: null} -
- {d.is_head ? Руководитель : null} - - ) : ( -
-
-

{d.name}

- {d.role_name ?

{d.role_name}

: null} -
- {d.is_head ? Руководитель : null} -
- ); - }) - )} -
-
-
+ const saveSkills = async (skillIds: number[]) => { + try { + setSkillsSaving(true); + setSkillsError(null); + const response = (await apiClient.updateCurrentUserProfile({ + skills_ids: skillIds, + })) as { + skills?: Array<{ id: number; name: string; description?: string }>; + }; + setProfileSkills(response.skills || []); + } catch (error: any) { + setSkillsError( + String(error?.message || "Не удалось обновить навыки"), + ); + } finally { + setSkillsSaving(false); + } + }; - {editProfileOpen ? ( -
- {profileError ?

{profileError}

: null} - {avatarFile ?

Выбран файл: {avatarFile.name}

: null} -
- - -
-
- ) : null} -
+ const handleRemoveSkill = async (skillId: number) => { + if (!user) return; + const nextIds = profileSkills + .map((skill) => skill.id) + .filter((id) => id !== skillId); + await saveSkills(nextIds); + }; -
-
-

Контакты

- -
+ const handleAddSkill = async (forcedSkill?: { id: number; name: string }) => { + if (!user) return; + const rawName = forcedSkill?.name || skillName; + const trimmedName = rawName.trim(); + if (!trimmedName || skillsSaving) return; - {editContactsOpen ? ( - <> - {contactsError ?

{contactsError}

: null} -
    - {editableContactItems.map((item) => ( -
  • - - setContactsForm((p) => ({ ...p, [item.key]: e.target.value }))} - placeholder={item.label} - className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm" - /> -
  • - ))} -
+ try { + setSkillsSaving(true); + setSkillsError(null); -
- - -
- - ) : contactItems.length === 0 ? ( -

Контакты не указаны

- ) : ( -
    - {contactItems.map((item) => ( -
  • - {item.label}:{" "} - {item.href ? ( - - {item.value} - - ) : ( - {item.value} - )} -
  • - ))} -
- )} -
+ const existingSkill = + forcedSkill || + availableSkills.find( + (skill) => skill.name.trim().toLowerCase() === trimmedName.toLowerCase(), + ); -
-

Навыки

- {skills.length === 0 ? ( -

Навыки не указаны

- ) : ( -
- {skills.map((s) => ( - - {s.name} - - ))} -
- )} -
+ let skillId = existingSkill?.id; -
-

Кадровые события

- {actions.length === 0 ? ( -

Событий нет

- ) : ( -
- {actions.map((a) => ( -
-

{a.action_display || a.action}

- {a.comment ?

{a.comment}

: null} -

{formatDate(a.date || a.created_at)}

-
- ))} -
- )} -
-
-
- ); + if (!skillId) { + const createdSkill = (await apiClient.createSkill({ + name: trimmedName, + })) as { id: number; name: string; description?: string }; + skillId = createdSkill.id; + setAvailableSkills((current) => + [...current, createdSkill].sort((left, right) => + left.name.localeCompare(right.name, "ru"), + ), + ); + } + + const nextIds = Array.from( + new Set([...profileSkills.map((skill) => skill.id), skillId]), + ); + + const response = (await apiClient.updateCurrentUserProfile({ + skills_ids: nextIds, + })) as { + skills?: Array<{ id: number; name: string; description?: string }>; + }; + setProfileSkills(response.skills || []); + setSkillName(""); + } catch (error: any) { + setSkillsError( + String(error?.message || "Не удалось добавить навык"), + ); + } finally { + setSkillsSaving(false); + } + }; + + if (loading) { + return ( + +
+
+
+

Загрузка профиля...

+
+
+ + ); + } + + if (!user) { + return null; + } + + return ( + +
+
+
+

Мой профиль

+ {currentAction ? ( + + {currentAction.action_display || currentAction.action} + + ) : null} +
+
+
+
+ {user.avatar ? ( + {fullName} + ) : ( +
+ {initials(user.first_name, user.last_name)} +
+ )} +
+ +
+

+ {fullName} +

+

+ {formatBirthdayWithYear(user.birth_date) || "Не указана"} +

+
+ + {user.position?.name || "Должность не указана"} + + {!!user.departments?.length && ( + + {user.departments.length === 1 + ? user.departments[0].name + : `${user.departments.length} отделов`} + + )} +
+
+
+ +
+
+
+
+
+ + + +
+

+ Почта +

+ {user.email ? ( + + {user.email} + + ) : ( +

Не указана

+ )} +
+
+
+
+ + + +
+

+ Телефон +

+ {user.phone_number ? ( + + {user.phone_number} + + ) : ( +

Не указан

+ )} +
+
+ {!!user.telegram && ( + <> +
+
+ + + +
+

+ Telegram +

+ + {user.telegram} + +
+
+ + )} + {!!user.whatsapp && ( + <> +
+
+ + + +
+

+ WhatsApp +

+ + {user.whatsapp} + +
+
+ + )} + {!!user.wechat && ( + <> +
+
+ + + +
+

+ WeChat +

+

+ {user.wechat} +

+
+
+ + )} + {(user.is_ldap_managed || + directoryLoginLoading || + directoryLoginError || + directoryLogin) && ( + <> +
+
+ + + +
+
+
+

+ Логин в каталоге +

+

+ {directoryLoginLoading + ? "Загружаем..." + : directoryLogin || "Не найден"} +

+ +
+ +
+
+
+ + )} +
+
+
+
+ +
+ +
+
+
+ setSkillName(event.target.value)} + placeholder="Добавить навык" + className="app-input w-full rounded-xl px-4 py-3 text-sm" + disabled={skillsSaving} + /> + + {availableSkills.map((skill) => ( + +
+ +
+ + {skillsError ? ( +

{skillsError}

+ ) : null} + +
+ {profileSkills.length ? ( +
+ {profileSkills.map((skill) => ( + + ))} +
+ ) : ( +

Навыки пока не указаны

+ )} +
+ + {suggestedSkills.length ? ( +
+

+ Доступные навыки +

+
+ {suggestedSkills.map((skill) => ( + + ))} +
+
+ ) : null} + + {skillsLoading ? ( +

Загружаем навыки...

+ ) : null} +
+
+ +
+ +
+
+
+

+ В компании +

+

+ {getWorkDuration(user.date_joined) || "—"} +

+
+
+

+ Дата найма +

+

+ {formatDate(user.date_joined)} +

+
+
+
+
+
+

+ Профиль создан +

+

+ {formatDate(user.created_at)} +

+
+
+

+ Последний вход +

+

+ {formatDateTime(user.last_login)} +

+
+
+
+
+ +
+ 3 ? ( + + ) : undefined + } + /> + {sortedActions.length ? ( +
+ {!showAllActions && sortedActions.length > 3 ? ( +

+ Последние {visibleActions.length} из {sortedActions.length} +

+ ) : null} + {visibleActions.map((action) => { + const tone = getActionTone(action.action); + const isCurrentAction = action.id === currentActionId; + return ( +
+ + {isCurrentAction ? ( + + ) : null} +
+
+
+ + {action.action_display || action.action} + + {isCurrentAction ? ( + + текущий + + ) : null} + {action.comment ? ( +

+ {truncateText( + action.comment, + showAllActions ? 240 : 120, + )} +

+ ) : null} +
+

+ {formatDateTime(action.date || action.created_at)} +

+
+
+
+ ); + })} +
+ ) : ( +

Событий нет

+ )} +
+
+
+ ); } diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index aaab253c..ecf4cf69 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -13,7 +13,6 @@ import { Mail, Monitor, Moon, - RefreshCw, Save, Smartphone, Sun, @@ -28,7 +27,7 @@ import { useWebPush } from "@/hooks/useWebPush"; import { apiClient } from "@/lib/api"; import type { ThemePreference } from "@/lib/theme"; import { resolveMediaUrl } from "@/lib/url"; -import type { AuthSession, DirectoryLoginResult } from "@/types/api"; +import type { AuthSession } from "@/types/api"; import { getVerbName } from "@/lib/verbTranslations"; type NotificationPreferences = { @@ -186,7 +185,7 @@ function SectionCard({
-

{title}

+

{title}

{description}

{action ?
{action}
: null} @@ -239,11 +238,6 @@ export default function SettingsPage() { next: false, confirm: false, }); - const [directoryLogin, setDirectoryLogin] = useState(null); - const [directoryLoginLoading, setDirectoryLoginLoading] = useState(false); - const [directoryLoginRefreshing, setDirectoryLoginRefreshing] = useState(false); - const [directoryLoginError, setDirectoryLoginError] = useState(null); - const { isSupported, isSubscribed, @@ -268,44 +262,8 @@ export default function SettingsPage() { whatsapp: user.whatsapp || "", wechat: user.wechat || "", }); - setDirectoryLogin(user.username?.trim() || null); - setDirectoryLoginError(null); }, [user]); - useEffect(() => { - let mounted = true; - - async function loadDirectoryLogin() { - if (!user?.is_ldap_managed || user.username?.trim()) { - return; - } - - try { - setDirectoryLoginLoading(true); - setDirectoryLoginError(null); - const response = (await apiClient.getDirectoryLogin()) as DirectoryLoginResult; - if (!mounted) return; - setDirectoryLogin(response.username?.trim() || null); - } catch (error) { - console.error(error); - if (!mounted) return; - setDirectoryLoginError( - error instanceof Error ? error.message : "Не удалось получить логин из каталога", - ); - } finally { - if (mounted) { - setDirectoryLoginLoading(false); - } - } - } - - void loadDirectoryLogin(); - - return () => { - mounted = false; - }; - }, [user?.id, user?.is_ldap_managed, user?.username]); - useEffect(() => { const section = new URLSearchParams(window.location.search).get("section"); if (!section) return; @@ -655,32 +613,6 @@ export default function SettingsPage() { } }; - const handleRefreshDirectoryLogin = async () => { - try { - setDirectoryLoginRefreshing(true); - setDirectoryLoginError(null); - const response = - (await apiClient.refreshDirectoryLogin()) as DirectoryLoginResult; - setDirectoryLogin(response.username?.trim() || null); - await refreshUser(); - toast.success( - response.username - ? "Логин в каталоге обновлен" - : "Логин в каталоге не найден", - ); - } catch (error) { - console.error(error); - setDirectoryLoginError( - error instanceof Error - ? error.message - : "Не удалось обновить логин из каталога", - ); - toast.error("Не удалось обновить логин из каталога"); - } finally { - setDirectoryLoginRefreshing(false); - } - }; - if (loading || !user) { return ( @@ -775,47 +707,6 @@ export default function SettingsPage() {

{fullName}

{user.email || "Почта не указана"}

-
-
-
-

- Логин в каталоге -

-

- {!user.is_ldap_managed - ? "Не используется" - : directoryLoginLoading - ? "Загружаем..." - : directoryLogin || "Не найден"} -

- {directoryLoginError ? ( -

- {directoryLoginError} -

- ) : user.is_ldap_managed ? ( -

- Берется из кэша БД и при необходимости обновляется из LDAP. -

- ) : ( -

- Для локального аккаунта логин каталога не используется. -

- )} -
- -
-