337 lines
13 KiB
TypeScript
337 lines
13 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { AlertTriangle, Send, X } from 'lucide-react';
|
|
import { CountryConfig, PeriodRange, ReconciliationRow } from '../types';
|
|
import { supabase } from '../lib/supabase';
|
|
import { ensureFreshSupabaseSession } from '../lib/sessionHealth';
|
|
|
|
const REPORT_BAMBOO_URL =
|
|
(import.meta.env.VITE_REPORTAR_BAMBOO_URL as string | undefined) || '';
|
|
|
|
interface BambooCorrectionModalProps {
|
|
row: ReconciliationRow;
|
|
country: CountryConfig;
|
|
periodRange: PeriodRange;
|
|
operatorName: string;
|
|
operatorEmail: string;
|
|
executionId?: string;
|
|
reportUrl?: string;
|
|
onClose: () => void;
|
|
onSuccess: (rowId: string) => void;
|
|
onShowToast: (message: string, type: 'success' | 'error' | 'info') => void;
|
|
}
|
|
|
|
const REASONS = [
|
|
'Nombre mal escrito en BambooHR',
|
|
'Apodo o nombre de uso',
|
|
'Nombre incompleto / nombres adicionales',
|
|
'Otro',
|
|
];
|
|
|
|
function parseJsonSafely(text: string) {
|
|
try {
|
|
return text ? JSON.parse(text) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default function BambooCorrectionModal({
|
|
row,
|
|
country,
|
|
periodRange,
|
|
operatorName,
|
|
operatorEmail,
|
|
executionId,
|
|
reportUrl,
|
|
onClose,
|
|
onSuccess,
|
|
onShowToast,
|
|
}: BambooCorrectionModalProps) {
|
|
const suggested = row.bestBambooCandidate;
|
|
const [bambooName, setBambooName] = useState(suggested?.employeeName || '');
|
|
const [employeeNumber, setEmployeeNumber] = useState(suggested?.employeeNumber || '');
|
|
const [reason, setReason] = useState(REASONS[0]);
|
|
const [comment, setComment] = useState('');
|
|
const [confirmed, setConfirmed] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setBambooName(suggested?.employeeName || '');
|
|
setEmployeeNumber(suggested?.employeeNumber || '');
|
|
setReason(REASONS[0]);
|
|
setComment('');
|
|
setConfirmed(false);
|
|
}, [row.id, suggested?.employeeName, suggested?.employeeNumber]);
|
|
|
|
const recipientLabel = useMemo(() => {
|
|
if (country.code === 'TT') return 'Laurele Nicome-Jack';
|
|
if (country.code === 'PA') return 'Delarys Peñalba y Ariadne Cortéz';
|
|
return 'Pablo Gamboa Cú, Sara Meyer Lima y María Morales Adler';
|
|
}, [country.code]);
|
|
|
|
const sourceNames = useMemo(
|
|
() =>
|
|
Array.from(
|
|
new Set(
|
|
[row.employee, row.bankNameFile, row.bankAccountHolder]
|
|
.map((value) => String(value || '').trim())
|
|
.filter(Boolean),
|
|
),
|
|
),
|
|
[row.employee, row.bankNameFile, row.bankAccountHolder],
|
|
);
|
|
|
|
const handleSubmit = async () => {
|
|
if (!REPORT_BAMBOO_URL) {
|
|
onShowToast('Falta configurar VITE_REPORTAR_BAMBOO_URL.', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!bambooName.trim()) {
|
|
onShowToast('Escribe el nombre correcto del empleado en BambooHR.', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!confirmed) {
|
|
onShowToast('Confirma que se trata de la misma persona antes de enviar el caso.', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!supabase) {
|
|
onShowToast('Supabase no está configurado en esta aplicación.', 'error');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
const session = await ensureFreshSupabaseSession(300);
|
|
const accessToken = session?.access_token;
|
|
|
|
if (!accessToken) {
|
|
throw new Error('Tu sesión expiró. Inicia sesión nuevamente antes de reportar el caso.');
|
|
}
|
|
|
|
const correctionPayload = {
|
|
country: country.code,
|
|
country_name: country.name,
|
|
source_name: row.employee,
|
|
source_names: sourceNames,
|
|
bank_name_file: row.bankNameFile || '',
|
|
bank_account_holder: row.bankAccountHolder || '',
|
|
bank_account: row.account || '',
|
|
bank_amount: row.amountBank,
|
|
currency: country.currency,
|
|
bamboo_name: bambooName.trim(),
|
|
bamboo_employee_number: employeeNumber.trim(),
|
|
reason,
|
|
comment: comment.trim(),
|
|
confirmed_same_person: true,
|
|
execution_id: executionId || '',
|
|
report_url: reportUrl || '',
|
|
period_label: periodRange.label,
|
|
period_start: periodRange.start,
|
|
period_end: periodRange.end,
|
|
requested_by_name: operatorName,
|
|
requested_by_email: operatorEmail,
|
|
};
|
|
|
|
// FormData evita una preflight CORS innecesaria al llamar el webhook de n8n.
|
|
// El JWT sigue viajando únicamente por HTTPS y n8n lo valida contra Supabase.
|
|
const formData = new FormData();
|
|
formData.append('payload', JSON.stringify(correctionPayload));
|
|
formData.append('access_token', accessToken);
|
|
|
|
const response = await fetch(REPORT_BAMBOO_URL, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
const responseText = await response.text();
|
|
const payload = parseJsonSafely(responseText);
|
|
|
|
if (!response.ok || payload?.ok === false) {
|
|
const message =
|
|
payload?.message ||
|
|
payload?.error ||
|
|
payload?.errors?.[0] ||
|
|
responseText ||
|
|
'No se pudo enviar el caso a RRHH.';
|
|
throw new Error(String(message));
|
|
}
|
|
|
|
onSuccess(row.id);
|
|
onShowToast(
|
|
`Caso enviado a RRHH de ${country.name}. La equivalencia quedó registrada para futuros cruces.`,
|
|
'success',
|
|
);
|
|
onClose();
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'No se pudo enviar el caso a RRHH.';
|
|
onShowToast(message, 'error');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[90] flex items-center justify-center px-4 py-6">
|
|
<button
|
|
type="button"
|
|
className="absolute inset-0 bg-black/45"
|
|
onClick={isSubmitting ? undefined : onClose}
|
|
aria-label="Cerrar"
|
|
/>
|
|
|
|
<div className="relative w-full max-w-2xl bg-white border border-[#D0D0D0] shadow-2xl max-h-[92vh] overflow-y-auto">
|
|
<div className="h-1.5 bg-[#6CC24A]" />
|
|
|
|
<div className="px-6 py-5 border-b border-neutral-200 flex items-start justify-between gap-4">
|
|
<div>
|
|
<p className="text-[10px] uppercase tracking-[0.18em] font-black text-[#6B8FA3]">Banco sin BambooHR</p>
|
|
<h2 className="text-xl font-black text-[#4F758B] mt-1">Reportar corrección de identidad a RRHH</h2>
|
|
<p className="text-xs text-neutral-500 mt-2 leading-relaxed">
|
|
Esta validación crea una equivalencia permanente para que el mismo falso positivo no vuelva a aparecer mientras RRHH corrige BambooHR.
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isSubmitting}
|
|
className="p-2 text-neutral-400 hover:text-neutral-700 hover:bg-neutral-100 disabled:opacity-40"
|
|
aria-label="Cerrar modal"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="px-6 py-5 space-y-5">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 bg-[#F5F5F5] border border-neutral-200 p-4">
|
|
<div>
|
|
<p className="text-[9px] uppercase tracking-wider font-black text-[#6B8FA3]">Nombre detectado</p>
|
|
<p className="text-sm font-black text-neutral-800 mt-1">{row.employee}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-[9px] uppercase tracking-wider font-black text-[#6B8FA3]">Cuenta bancaria</p>
|
|
<p className="text-sm font-mono font-bold text-neutral-700 mt-1">{row.account || '—'}</p>
|
|
</div>
|
|
<div className="sm:col-span-2">
|
|
<p className="text-[9px] uppercase tracking-wider font-black text-[#6B8FA3]">Se notificará a</p>
|
|
<p className="text-xs font-bold text-neutral-700 mt-1">{recipientLabel}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{suggested?.employeeName && (
|
|
<div className="border border-[#D6E8F4] bg-[#F7FBFD] p-4 flex items-start gap-3">
|
|
<AlertTriangle className="w-4 h-4 text-[#4F758B] mt-0.5 flex-shrink-0" />
|
|
<div className="text-xs text-neutral-600 leading-relaxed">
|
|
<strong className="text-[#4F758B]">Candidato sugerido por el cruce:</strong>{' '}
|
|
{suggested.employeeName}
|
|
{suggested.employeeNumber ? ` · Employee Number ${suggested.employeeNumber}` : ''}.
|
|
Verifícalo manualmente antes de confirmar.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<label className="sm:col-span-2">
|
|
<span className="block text-[10px] uppercase tracking-wider font-black text-[#4F758B] mb-1.5">
|
|
Nombre correcto en BambooHR *
|
|
</span>
|
|
<input
|
|
type="text"
|
|
value={bambooName}
|
|
onChange={(event) => setBambooName(event.target.value)}
|
|
placeholder="Ej.: Jonathan Alexander Pérez"
|
|
className="w-full border border-[#D0D0D0] px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-[#4F758B]"
|
|
/>
|
|
</label>
|
|
|
|
<label>
|
|
<span className="block text-[10px] uppercase tracking-wider font-black text-[#4F758B] mb-1.5">
|
|
Employee Number
|
|
</span>
|
|
<input
|
|
type="text"
|
|
value={employeeNumber}
|
|
onChange={(event) => setEmployeeNumber(event.target.value)}
|
|
placeholder="Recomendado para mayor seguridad"
|
|
className="w-full border border-[#D0D0D0] px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-[#4F758B]"
|
|
/>
|
|
</label>
|
|
|
|
<label>
|
|
<span className="block text-[10px] uppercase tracking-wider font-black text-[#4F758B] mb-1.5">
|
|
Motivo *
|
|
</span>
|
|
<select
|
|
value={reason}
|
|
onChange={(event) => setReason(event.target.value)}
|
|
className="w-full border border-[#D0D0D0] px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-[#4F758B] bg-white"
|
|
>
|
|
{REASONS.map((option) => (
|
|
<option key={option} value={option}>{option}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label className="sm:col-span-2">
|
|
<span className="block text-[10px] uppercase tracking-wider font-black text-[#4F758B] mb-1.5">
|
|
Comentario para RRHH
|
|
</span>
|
|
<textarea
|
|
value={comment}
|
|
onChange={(event) => setComment(event.target.value)}
|
|
rows={3}
|
|
placeholder="Detalle adicional que ayude a RRHH a corregir el registro."
|
|
className="w-full border border-[#D0D0D0] px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-[#4F758B] resize-y"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<label className="flex items-start gap-3 border border-[#D0D0D0] p-4 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={confirmed}
|
|
onChange={(event) => setConfirmed(event.target.checked)}
|
|
className="mt-0.5 accent-[#6CC24A]"
|
|
/>
|
|
<span className="text-xs text-neutral-600 leading-relaxed">
|
|
Confirmo que revisé el caso manualmente y que <strong>{row.employee}</strong> corresponde a la misma persona indicada arriba en BambooHR.
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div className="px-6 py-4 border-t border-neutral-200 bg-neutral-50 flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isSubmitting}
|
|
className="px-4 py-2.5 border border-neutral-300 bg-white text-neutral-600 text-xs font-black uppercase tracking-wider disabled:opacity-40"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSubmit}
|
|
disabled={isSubmitting || !confirmed || !bambooName.trim()}
|
|
className="px-5 py-2.5 bg-[#4F758B] text-white text-xs font-black uppercase tracking-wider inline-flex items-center justify-center gap-2 border-l-4 border-[#6CC24A] disabled:bg-neutral-300 disabled:border-neutral-300 disabled:cursor-not-allowed"
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" />
|
|
Enviando...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Send className="w-4 h-4" />
|
|
Enviar a RRHH
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|