Agregar Bono 14 Guatemala y actualizar dist
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
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(
|
||||
() =>
|
||||
country.code === 'TT'
|
||||
? 'Laurele Nicome-Jack'
|
||||
: '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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user