143 lines
4.9 KiB
TypeScript
143 lines
4.9 KiB
TypeScript
import type {
|
|
PayrollPeriodSummary,
|
|
PayrollStatementLine,
|
|
PayrollStatementPage,
|
|
PayrollStatementSummary,
|
|
} from "@/lib/api/finance";
|
|
|
|
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
|
|
export type PayrollLineGroupKey = "accruals" | "adjustments" | "deductions" | "payments";
|
|
|
|
export type PayrollLineGroup = {
|
|
key: PayrollLineGroupKey;
|
|
label: string;
|
|
lines: PayrollStatementLine[];
|
|
};
|
|
|
|
function parseDateOnly(value: string): Date | null {
|
|
const match = DATE_ONLY_PATTERN.exec(value);
|
|
if (!match) return null;
|
|
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
}
|
|
|
|
export function normalizePayrollStatements(
|
|
payload: PayrollStatementPage | PayrollStatementSummary[],
|
|
): PayrollStatementSummary[] {
|
|
return Array.isArray(payload) ? payload : payload.results || [];
|
|
}
|
|
|
|
export function formatPayrollMoney(value: string | number, currency = "RUB"): string {
|
|
const amount = typeof value === "number" ? value : Number(value);
|
|
if (!Number.isFinite(amount)) return "—";
|
|
|
|
try {
|
|
return new Intl.NumberFormat("ru-RU", {
|
|
style: "currency",
|
|
currency: currency || "RUB",
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 2,
|
|
}).format(amount);
|
|
} catch {
|
|
return `${new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 2 }).format(amount)} ${currency}`.trim();
|
|
}
|
|
}
|
|
|
|
export function formatPayrollPoints(
|
|
value: string | number | null | undefined,
|
|
emptyValue = "—",
|
|
): string {
|
|
if (value == null || value === "") return emptyValue;
|
|
const points = typeof value === "number" ? value : Number(value);
|
|
if (!Number.isFinite(points)) return String(value);
|
|
return new Intl.NumberFormat("ru-RU", {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 2,
|
|
}).format(points);
|
|
}
|
|
|
|
export function normalizePayrollPoints(
|
|
value: string | number | null | undefined,
|
|
emptyValue = "",
|
|
): string {
|
|
if (value == null || value === "") return emptyValue;
|
|
const normalized = typeof value === "string" ? value.trim().replace(",", ".") : value;
|
|
const points = Number(normalized);
|
|
if (!Number.isFinite(points)) return String(value);
|
|
return (Math.round((points + Number.EPSILON) * 100) / 100).toFixed(2);
|
|
}
|
|
|
|
export function formatPayrollDate(value: string | null | undefined): string {
|
|
if (!value) return "—";
|
|
const date = parseDateOnly(value) || new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "—";
|
|
return new Intl.DateTimeFormat("ru-RU", {
|
|
day: "2-digit",
|
|
month: "long",
|
|
year: "numeric",
|
|
}).format(date);
|
|
}
|
|
|
|
export function formatPayrollDateTime(value: string | null | undefined): string {
|
|
if (!value) return "—";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "—";
|
|
return new Intl.DateTimeFormat("ru-RU", {
|
|
day: "2-digit",
|
|
month: "long",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
}).format(date);
|
|
}
|
|
|
|
export function getPayrollPeriodLabel(period: PayrollPeriodSummary): string {
|
|
if (period.name.trim()) return period.name.trim();
|
|
const start = parseDateOnly(period.date_from);
|
|
if (!start) return period.code;
|
|
const label = new Intl.DateTimeFormat("ru-RU", {
|
|
month: "long",
|
|
year: "numeric",
|
|
}).format(start);
|
|
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
}
|
|
|
|
export function getPayrollPeriodRange(period: PayrollPeriodSummary): string {
|
|
return `${formatPayrollDate(period.date_from)} — ${formatPayrollDate(period.date_to)}`;
|
|
}
|
|
|
|
export function groupPayrollLines(lines: PayrollStatementLine[]): PayrollLineGroup[] {
|
|
const groups: Record<PayrollLineGroupKey, PayrollStatementLine[]> = {
|
|
accruals: [],
|
|
adjustments: [],
|
|
deductions: [],
|
|
payments: [],
|
|
};
|
|
|
|
for (const line of lines) {
|
|
if (line.kind === "earning") groups.accruals.push(line);
|
|
else if (line.kind === "adjustment_credit" || line.kind === "adjustment_debit") {
|
|
groups.adjustments.push(line);
|
|
} else if (line.kind === "deduction") groups.deductions.push(line);
|
|
else if (line.kind === "payment") groups.payments.push(line);
|
|
}
|
|
|
|
const labels: Record<PayrollLineGroupKey, string> = {
|
|
accruals: "Начисления",
|
|
adjustments: "Корректировки",
|
|
deductions: "Удержания",
|
|
payments: "Выплаты",
|
|
};
|
|
|
|
return (Object.keys(groups) as PayrollLineGroupKey[])
|
|
.filter((key) => groups[key].length > 0)
|
|
.map((key) => ({ key, label: labels[key], lines: groups[key] }));
|
|
}
|
|
|
|
export function getPayrollLineDirection(kind: PayrollStatementLine["kind"]): "positive" | "negative" {
|
|
return kind === "adjustment_debit" || kind === "deduction" || kind === "payment"
|
|
? "negative"
|
|
: "positive";
|
|
}
|