diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index f65addb5..dc23520e 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -317,6 +317,21 @@ html[data-bs-theme="dark"] .app-input::-webkit-calendar-picker-indicator { color: var(--accent-primary-strong); } +.app-status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + width: fit-content; + min-height: 1.75rem; + flex-shrink: 0; + border-radius: 9999px; + padding: 0.25rem 0.75rem; + font-size: 0.75rem; + font-weight: 500; + line-height: 1; + white-space: nowrap; +} + .app-avatar-frame { border: 1px solid var(--border-subtle); background: var(--surface-secondary); diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index 54b70f56..736600fa 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -15,7 +15,10 @@ import { AppShell } from "@/components/AppShell"; import { useUser } from "@/contexts/UserContext"; import { apiClient } from "@/lib/api"; import { resolveMediaUrl } from "@/lib/url"; -import { getWorkDuration } from "@/lib/users/userDetailUtils"; +import { + getEmployeeActionTone, + getWorkDuration, +} from "@/lib/users/userDetailUtils"; import type { DirectoryLoginResult } from "@/types/api"; function formatDate(value?: string): string { @@ -85,32 +88,6 @@ function truncateText(value?: string, maxLength = 120) { 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, @@ -184,26 +161,6 @@ export default function ProfilePage() { [showAllActions, sortedActions], ); - const normalizedSkillName = skillName.trim().toLowerCase(); - - const selectedSkillIds = useMemo( - () => new Set(profileSkills.map((skill) => skill.id)), - [profileSkills], - ); - - 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], - ); - useEffect(() => { setDirectoryLogin(user?.username?.trim() || null); setDirectoryLoginError(null); @@ -391,46 +348,46 @@ export default function ProfilePage() { 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 && ( @@ -445,8 +402,6 @@ export default function ProfilePage() {
-
-
@@ -577,7 +532,6 @@ export default function ProfilePage() { ? "Загружаем..." : directoryLogin || "Не найден"}

-
- ))} -
-
- ) : null} - {skillsLoading ? ( -

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

+

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

) : null}
@@ -683,8 +616,8 @@ export default function ProfilePage() {
-
-
+
+

В компании

@@ -692,7 +625,7 @@ export default function ProfilePage() { {getWorkDuration(user.date_joined) || "—"}

-
+

Дата найма

@@ -703,7 +636,7 @@ export default function ProfilePage() {
-
+

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

@@ -711,7 +644,7 @@ export default function ProfilePage() { {formatDate(user.created_at)}

-
+

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

@@ -741,30 +674,32 @@ export default function ProfilePage() { } /> {sortedActions.length ? ( -
+
{!showAllActions && sortedActions.length > 3 ? (

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

) : null} {visibleActions.map((action) => { - const tone = getActionTone(action.action); + const tone = getEmployeeActionTone(action.action); const isCurrentAction = action.id === currentActionId; return (
{isCurrentAction ? ( ) : null} -
+
{action.action_display || action.action} diff --git a/frontend/src/app/users/[id]/page.tsx b/frontend/src/app/users/[id]/page.tsx index e859b471..ef6b87aa 100644 --- a/frontend/src/app/users/[id]/page.tsx +++ b/frontend/src/app/users/[id]/page.tsx @@ -27,6 +27,7 @@ import { resolveMediaUrl } from "@/lib/url"; import { formatBirthday, formatPhoneForLink, + getEmployeeActionTone, getWorkDuration, } from "@/lib/users/userDetailUtils"; @@ -69,20 +70,6 @@ function normalizeWhatsApp(value: string) { return digits ? `https://wa.me/${digits}` : ""; } -function getActionTone(action?: string) { - switch (action) { - case "on_leave": - case "on_maternity": - return "app-feedback-warning"; - case "dismissed": - return "app-feedback-danger"; - case "transferred": - return "app-selected"; - default: - return "app-feedback-success"; - } -} - function SectionTitle({ title, action, @@ -198,24 +185,6 @@ export default function UserDetailPage() { }; }, []); - const normalizedSkillName = skillName.trim().toLowerCase(); - const selectedSkillIds = useMemo( - () => new Set(personSkills.map((skill) => skill.id)), - [personSkills], - ); - 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 contactRows = useMemo( () => person @@ -336,7 +305,7 @@ export default function UserDetailPage() { return ( -
+
) : person ? ( <> -
-
+
+

Профиль сотрудника

{latestAction ? ( {latestAction.action_display || latestAction.action} ) : null}
-
+
-
+
{avatarUrl && !avatarFailed ? ( {fullName} setAvatarFailed(true)} unoptimized /> ) : ( -
+
{initials}
)} @@ -392,17 +361,17 @@ export default function UserDetailPage() {
-

+

{fullName}

-
- +
+ {person.position?.name || "Должность не указана"} {primaryDepartment ? ( {primaryDepartment.name} @@ -411,16 +380,6 @@ export default function UserDetailPage() {
- {canManageActions ? ( - - ) : null} {canEdit ? ( - ))} -
-
- ) : null} - {skillsLoading ? (

Загружаем навыки... diff --git a/frontend/src/components/users/EmployeeActionsTimeline.tsx b/frontend/src/components/users/EmployeeActionsTimeline.tsx index 2ae2558b..2f7aaa1b 100644 --- a/frontend/src/components/users/EmployeeActionsTimeline.tsx +++ b/frontend/src/components/users/EmployeeActionsTimeline.tsx @@ -2,8 +2,7 @@ import { History, Pencil, Trash2 } from "lucide-react"; import { - getEmployeeActionBadgeClass, - getEmployeeActionBorderColor, + getEmployeeActionTone, } from "@/lib/users/userDetailUtils"; import type { EmployeeAction } from "@/types/api"; @@ -73,19 +72,18 @@ export default function EmployeeActionsTimeline({ {sortedActions.map((action) => { const isCurrent = latestActionId === action.id; const deleteKey = `delete-${action.id}`; - const tone = getEmployeeActionBadgeClass(action.action); - const lineColor = getEmployeeActionBorderColor(action.action); + const tone = getEmployeeActionTone(action.action); return (

{isCurrent ? ( ) : null}
@@ -93,7 +91,7 @@ export default function EmployeeActionsTimeline({
{action.action_display || action.action} diff --git a/frontend/src/lib/users/userDetailUtils.ts b/frontend/src/lib/users/userDetailUtils.ts index 1f612d40..dce145c7 100644 --- a/frontend/src/lib/users/userDetailUtils.ts +++ b/frontend/src/lib/users/userDetailUtils.ts @@ -122,32 +122,43 @@ export function formatBirthday(birthDate?: string): string | null { return new Date(birthDate).toLocaleDateString("ru-RU", { day: "numeric", month: "long" }); } -export function getEmployeeActionBadgeClass(actionType?: string): string { +export function getEmployeeActionTone(actionType?: string): { + badgeClass: string; + lineColor: string; +} { switch (actionType) { case "on_leave": - return "bg-yellow-100 text-yellow-700"; case "on_maternity": - return "bg-purple-100 text-purple-700"; + return { + badgeClass: "app-feedback-warning", + lineColor: "#f59e0b", + }; case "transferred": - return "bg-blue-100 text-blue-700"; + return { + badgeClass: "app-selected", + lineColor: "#38bdf8", + }; case "dismissed": - return "bg-red-100 text-red-700"; + return { + badgeClass: "app-feedback-danger", + lineColor: "#ef4444", + }; + case "returned_from_leave": + case "returned_from_maternity": + case "rehired": + case "hired": default: - return "bg-green-100 text-green-700"; + return { + badgeClass: "app-feedback-success", + lineColor: "#22c55e", + }; } } +export function getEmployeeActionBadgeClass(actionType?: string): string { + return getEmployeeActionTone(actionType).badgeClass; +} + export function getEmployeeActionBorderColor(actionType?: string): string { - switch (actionType) { - case "on_leave": - return "#eab308"; - case "on_maternity": - return "#a855f7"; - case "transferred": - return "#3b82f6"; - case "dismissed": - return "#ef4444"; - default: - return "#22c55e"; - } -} \ No newline at end of file + return getEmployeeActionTone(actionType).lineColor; +}