Corp/frontend/src/lib/users/userDetailUtils.ts

279 lines
7.8 KiB
TypeScript
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.

import type { EmployeeAction, EmployeeDepartment, User } from "@/types/api";
export type ProfileDepartmentSummary = {
label: string;
href?: string;
};
export type UserProfileTextField =
| "firstName"
| "lastName"
| "patronymic"
| "email"
| "phone"
| "telegram"
| "whatsapp"
| "wechat";
export type UserProfileEditForm = {
firstName: string;
lastName: string;
patronymic: string;
email: string;
phone: string;
telegram: string;
whatsapp: string;
wechat: string;
avatarFile: File | null;
avatarPreview: string | null;
};
export type EmployeeActionField = "type" | "date" | "dateTo" | "comment";
export type EmployeeActionForm = {
type: string;
date: string;
dateTo: string;
comment: string;
editingId: number | null;
};
export const employeeActionTypes = [
{ value: "working", label: "Работает" },
{ value: "remote", label: "На удалёнке" },
{ value: "hired", label: "Принят" },
{ value: "dismissed", label: "Уволен" },
{ value: "on_leave", label: "В отпуске" },
{ value: "returned_from_leave", label: "Вернулся из отпуска" },
{ value: "on_sick_leave", label: "На больничном" },
{ value: "returned_from_sick_leave", label: "Вернулся с больничного" },
{ value: "on_day_off", label: "В отгуле" },
{ value: "returned_from_day_off", label: "Вернулся из отгула" },
{ value: "on_maternity", label: "В декрете" },
{ value: "returned_from_maternity", label: "Вернулся из декрета" },
{ value: "transferred", label: "Переведен" },
{ value: "rehired", label: "Восстановлен" },
] as const;
export const temporaryEmployeeActionTypes = new Set([
"on_leave",
"on_sick_leave",
"on_day_off",
"on_maternity",
]);
export const createEmptyActionForm = (): EmployeeActionForm => ({
type: "",
date: new Date().toISOString().split("T")[0],
dateTo: "",
comment: "",
editingId: null,
});
export const createEmptyEditForm = (): UserProfileEditForm => ({
firstName: "",
lastName: "",
patronymic: "",
email: "",
phone: "",
telegram: "",
whatsapp: "",
wechat: "",
avatarFile: null,
avatarPreview: null,
});
export function getUserFullName(person: User | null): string {
if (!person) return "";
return `${person.last_name || ""} ${person.first_name || ""} ${person.patronymic || ""}`.trim() || "Пользователь";
}
export function getUserInitials(person: User | null): string {
return `${person?.last_name?.[0] || ""}${person?.first_name?.[0] || ""}` || "П";
}
function pluralizeDepartments(count: number): string {
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) return "отдел";
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) {
return "отдела";
}
return "отделов";
}
export function getProfileDepartmentSummary(
departments?: EmployeeDepartment[] | null,
): ProfileDepartmentSummary | null {
if (!departments?.length) return null;
if (departments.length === 1) {
return {
label: departments[0].name,
href: `/departments/${departments[0].id}`,
};
}
return {
label: `${departments.length} ${pluralizeDepartments(departments.length)}`,
};
}
export function getLatestEmployeeAction(actions?: EmployeeAction[] | null): EmployeeAction | null {
if (!actions || actions.length === 0) return null;
const now = new Date();
const pastActions = actions.filter((action) => new Date(action.date) <= now);
if (pastActions.length === 0) return null;
return pastActions.reduce((latest, action) => (
new Date(action.date) > new Date(latest.date) ? action : latest
));
}
export function sortEmployeeActions(actions?: EmployeeAction[] | null): EmployeeAction[] {
if (!actions || actions.length === 0) return [];
return [...actions].sort((left, right) => (
new Date(right.date).getTime() - new Date(left.date).getTime()
));
}
export function isTemporaryEmployeeAction(actionType?: string): boolean {
return Boolean(actionType && temporaryEmployeeActionTypes.has(actionType));
}
export function formatPhoneForLink(phone: string): string {
return phone.replace(/[^0-9+]/g, "");
}
export function getWorkDuration(dateJoined?: string): string | null {
if (!dateJoined) return null;
const start = new Date(dateJoined);
const now = new Date();
const diffMs = now.getTime() - start.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const years = Math.floor(diffDays / 365);
const months = Math.floor((diffDays % 365) / 30);
const parts: string[] = [];
if (years > 0) {
parts.push(`${years} ${years === 1 ? "год" : years < 5 ? "года" : "лет"}`);
}
if (months > 0) {
parts.push(`${months} ${months === 1 ? "месяц" : months < 5 ? "месяца" : "месяцев"}`);
}
return parts.join(" ") || "меньше месяца";
}
export function formatBirthday(birthDate?: string): string | null {
if (!birthDate) return null;
return new Date(birthDate).toLocaleDateString("ru-RU", { day: "numeric", month: "long" });
}
export function formatBirthdayWithYear(birthDate?: string): string | null {
if (!birthDate) return null;
const date = new Date(birthDate);
if (Number.isNaN(date.getTime())) return birthDate;
return date.toLocaleDateString("ru-RU", {
day: "numeric",
month: "long",
year: "numeric",
});
}
export function formatProfileDate(value?: string): string {
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",
});
}
export function formatProfileDateTime(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",
});
}
export function normalizeTelegramLink(value: string) {
const trimmed = value.trim();
if (!trimmed) return "";
if (/^https?:\/\//i.test(trimmed)) return trimmed;
return `https://t.me/${trimmed.replace(/^@/, "")}`;
}
export function normalizeWhatsAppLink(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}` : "";
}
export function truncateText(value?: string, maxLength = 120) {
if (!value) return "";
if (value.length <= maxLength) return value;
return `${value.slice(0, maxLength).trimEnd()}…`;
}
export function getEmployeeActionTone(actionType?: string): {
badgeClass: string;
lineColor: string;
} {
switch (actionType) {
case "on_leave":
case "on_sick_leave":
case "on_day_off":
case "on_maternity":
return {
badgeClass: "app-feedback-warning",
lineColor: "#f59e0b",
};
case "transferred":
case "remote":
return {
badgeClass: "app-selected",
lineColor: "#38bdf8",
};
case "dismissed":
return {
badgeClass: "app-feedback-danger",
lineColor: "#ef4444",
};
case "returned_from_leave":
case "returned_from_sick_leave":
case "returned_from_day_off":
case "returned_from_maternity":
case "rehired":
case "hired":
case "working":
default:
return {
badgeClass: "app-feedback-success",
lineColor: "#22c55e",
};
}
}
export function getEmployeeActionBadgeClass(actionType?: string): string {
return getEmployeeActionTone(actionType).badgeClass;
}
export function getEmployeeActionBorderColor(actionType?: string): string {
return getEmployeeActionTone(actionType).lineColor;
}