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 ? (
-
-
-
- ) : (
-
{initials}
- )}
-
-
{
- setAvatarFile(e.target.files?.[0] || null);
- setEditProfileOpen(true);
- }}
- className="hidden"
- />
- {editProfileOpen ? (
-
- ) : null}
-
+ void loadSkills();
-
-
- {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}
-
+ 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 ? (
+
+ ) : (
+
+ {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.telegram && (
+ <>
+
+
+ >
+ )}
+ {!!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}
+ />
+
+
+
+
+
+ {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.
-
- ) : (
-
- Для локального аккаунта логин каталога не используется.
-
- )}
-
-
-
-