panels-origin/api/mx.ts
2026-08-21 16:55:01 -05:00

195 lines
6.7 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.

const CURP_RE =
/^[A-Z][AEIOUX][A-Z]{2}\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])[HMX](?:AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[A-Z\d]\d$/;
const RFC_PF = /^[A-ZÑ&]{4}\d{6}[A-Z0-9]{3}$/;
const RFC_PM = /^[A-ZÑ&]{3}\d{6}[A-Z0-9]{3}$/;
export function normUpper(value: string | null | undefined): string {
return (value ?? "").toString().trim().toUpperCase().replace(/\s+/g, "");
}
export function normName(value: string | null | undefined): string {
return (value ?? "").toString().trim().replace(/\s+/g, " ");
}
const NAME_PARTICLES = new Set([
"de", "del", "la", "las", "los", "el", "y", "e", "da", "das", "do", "dos", "di", "du", "le", "van", "von",
]);
function capPiece(piece: string): string {
if (!piece) return piece;
return piece.replace(/(^|['’])(\p{L})/gu, (_m, sep: string, ch: string) => sep + ch.toLocaleUpperCase("es-MX"));
}
/** Nombre propio: Héctor de la Cruz. No guarda TODO EN MAYÚSCULAS. */
export function titleCase(value: string | null | undefined): string {
const raw = normName(value);
if (!raw) return "";
return raw
.toLocaleLowerCase("es-MX")
.split(" ")
.map((word, i) => {
if (!word) return word;
if (i > 0 && NAME_PARTICLES.has(word)) return word;
return word.split("-").map(capPiece).join("-");
})
.join(" ");
}
/** Oración: primera letra y después de . ? ! */
export function sentenceCase(value: string | null | undefined): string {
const raw = normName(value);
if (!raw) return "";
return raw
.toLocaleLowerCase("es-MX")
.replace(/(^|[.!?]\s+)(\p{L})/gu, (_m, sep: string, ch: string) => sep + ch.toLocaleUpperCase("es-MX"));
}
export function looksAllCaps(value: string | null | undefined): boolean {
const letters = [...(value ?? "")].filter((ch) => /\p{L}/u.test(ch)).join("");
if (letters.length < 2) return false;
const up = letters.toLocaleUpperCase("es-MX");
const low = letters.toLocaleLowerCase("es-MX");
return letters === up && letters !== low;
}
export function validateCurp(raw: string): string | null {
const curp = normUpper(raw);
if (!CURP_RE.test(curp)) return "CURP inválida (18 caracteres, formato oficial)";
return null;
}
export function validateRfc(raw: string): string | null {
const rfc = normUpper(raw).replace(/Ñ/g, "Ñ");
if (!RFC_PF.test(rfc) && !RFC_PM.test(rfc)) {
return "RFC inválido (12 o 13 caracteres, formato SAT)";
}
return null;
}
/** Dígito verificador IMSS (11 dígitos). */
export function validateNss(raw: string): string | null {
const nss = (raw ?? "").toString().replace(/\D/g, "");
if (nss.length !== 11) return "NSS inválido (11 dígitos)";
const digits = nss.split("").map(Number);
let sum = 0;
for (let i = 0; i < 10; i++) {
let n = digits[i] * (i % 2 === 1 ? 2 : 1);
if (n > 9) n -= 9;
sum += n;
}
const check = (10 - (sum % 10)) % 10;
if (check !== digits[10]) return "NSS inválido (dígito verificador)";
return null;
}
export function formatNss(raw: string): string {
return (raw ?? "").toString().replace(/\D/g, "");
}
export type WorkerInput = {
first_name?: string;
middle_name?: string | null;
last_name_p?: string;
last_name_m?: string;
curp?: string;
rfc?: string;
nss?: string;
phone?: string;
email?: string;
address?: string;
blood_type?: string | null;
hire_type?: string;
company_id?: number | null;
position?: string;
risk_code?: string;
work_type?: string;
daily_wage?: number;
needs_badge?: boolean | number;
status?: string;
};
export function validateWorkerFields(body: WorkerInput): Record<string, string> {
const errors: Record<string, string> = {};
if (!normName(body.first_name)) errors.first_name = "Nombre obligatorio";
if (!normName(body.last_name_p)) errors.last_name_p = "Apellido paterno obligatorio";
if (!normName(body.last_name_m)) errors.last_name_m = "Apellido materno obligatorio";
const curpErr = validateCurp(body.curp ?? "");
if (curpErr) errors.curp = curpErr;
const rfcErr = validateRfc(body.rfc ?? "");
if (rfcErr) errors.rfc = rfcErr;
const nssErr = validateNss(body.nss ?? "");
if (nssErr) errors.nss = nssErr;
if (!normName(body.phone)) errors.phone = "Teléfono obligatorio";
if (!normName(body.email)) errors.email = "Correo obligatorio";
if (!normName(body.address)) errors.address = "Dirección obligatoria";
// Empresa / patrón se asigna solo con el alta IMSS; no es obligatoria en el padrón.
if (!normName(body.position)) errors.position = "Cargo obligatorio";
if (!normName(body.risk_code)) errors.risk_code = "Riesgo obligatorio";
const wt = (body.work_type ?? "").toUpperCase();
if (!["N", "D"].includes(wt)) errors.work_type = "Tipo de trabajo N o D";
if (body.daily_wage === undefined || body.daily_wage === null || Number.isNaN(Number(body.daily_wage))) {
errors.daily_wage = "Jornal obligatorio";
}
return errors;
}
export function canonicalRiskCode(raw?: string) {
const c = (raw ?? "").toLowerCase().trim();
if (c === "alto") return "rojo";
if (c === "medio") return "amarillo";
if (c === "bajo") return "verde";
return c;
}
export function pipelineLabelFor(status: string): string {
const labels: Record<string, string> = {
incompleto: "Falta expediente",
listo_gafete: "Listo gafete",
impreso: "Gafete impreso",
activo: "Activo en proyecto",
baja: "Baja",
};
return labels[status] ?? status;
}
export function normalizeWorker(body: WorkerInput) {
return {
first_name: titleCase(body.first_name),
middle_name: titleCase(body.middle_name) || null,
last_name_p: titleCase(body.last_name_p),
last_name_m: titleCase(body.last_name_m),
curp: normUpper(body.curp),
rfc: normUpper(body.rfc),
nss: formatNss(body.nss ?? ""),
phone: normName(body.phone),
email: (body.email ?? "").toString().trim().toLowerCase(),
address: sentenceCase(body.address),
blood_type: normUpper(body.blood_type) || null,
hire_type: (body.hire_type ?? "").toUpperCase(),
position: titleCase(body.position),
risk_code: canonicalRiskCode(body.risk_code),
work_type: (body.work_type ?? "").toUpperCase(),
daily_wage: Number(body.daily_wage ?? 0),
needs_badge: body.needs_badge === false || body.needs_badge === 0 ? 0 : 1,
status: body.status === "baja" ? "baja" : "activo",
};
}
export function fullName(w: {
first_name: string;
middle_name?: string | null;
last_name_p: string;
last_name_m: string;
}): string {
return [w.first_name, w.middle_name, w.last_name_p, w.last_name_m]
.filter(Boolean)
.join(" ");
}
export function frontName(w: { first_name: string; last_name_p: string }): string {
return `${w.first_name} ${w.last_name_p}`.trim();
}