mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 16:53:17 +00:00
195 lines
6.7 KiB
TypeScript
195 lines
6.7 KiB
TypeScript
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();
|
||
}
|