feat: Ajouter colonne Notif. pour tracer les emails de signature électronique

- Remplace la colonne Actions par une colonne Notif. affichant les dernières notifications
- Affiche E: pour employeur et S: pour salarié avec date/heure
- Code couleur basé sur la date de début du contrat:
  * Vert si début > 48h
  * Orange si début < 48h
  * Rouge si début aujourd'hui ou passé
- Utilise la table email_logs existante (aucune migration nécessaire)
- Récupération automatique via supabase client des emails de type signature-request
This commit is contained in:
odentas 2025-10-22 20:12:49 +02:00
parent d81a12de6e
commit 807cb20456

View file

@ -28,6 +28,41 @@ function formatDate(dateString: string | null | undefined): string {
}
}
// Utility function to format notification date as DD/MM HH:MM
function formatNotificationDate(dateString: string | null): string {
if (!dateString) return "—";
try {
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month} ${hours}:${minutes}`;
} catch {
return "—";
}
}
// Utility function to get notification color based on contract start date
function getNotificationColor(startDate: string | null | undefined): string {
if (!startDate) return 'text-slate-400';
const start = new Date(startDate);
const today = new Date();
today.setHours(0, 0, 0, 0);
start.setHours(0, 0, 0, 0);
const diffTime = start.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// Rouge si contrat démarre aujourd'hui ou dans le passé
if (diffDays <= 0) return 'text-red-600';
// Orange si moins de 48 heures (moins de 2 jours)
if (diffDays < 2) return 'text-orange-600';
// Vert si plus de 48 heures
return 'text-green-600';
}
// Utility function to format employee name as "NOM Prénom"
// Priorité : utiliser salaries.salarie si disponible, sinon formater employee_name
function formatEmployeeName(contract: { employee_name?: string | null; salaries?: { salarie?: string | null; nom?: string | null; prenom?: string | null; } | null }): string {
@ -98,6 +133,11 @@ type Contract = {
} | null;
};
type NotificationInfo = {
employerLastSent: string | null;
employeeLastSent: string | null;
};
type ContractProgress = {
id: string;
contractNumber?: string;
@ -137,6 +177,9 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
// Selection state
const [selectedContractIds, setSelectedContractIds] = useState<Set<string>>(new Set());
// Notification tracking state
const [notificationMap, setNotificationMap] = useState<Map<string, NotificationInfo>>(new Map());
// Key for localStorage
const FILTERS_STORAGE_KEY = 'staff-contracts-filters';
@ -1040,6 +1083,44 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
setIsESignCancelled(false);
};
// Fonction pour récupérer les dernières notifications de signature pour un ensemble de contrats
const fetchLastNotifications = async (contractIds: string[]) => {
if (contractIds.length === 0) return;
try {
const { data, error } = await supabase
.from('email_logs')
.select('contract_id, created_at, email_type')
.in('contract_id', contractIds)
.in('email_type', ['signature-request-employer', 'signature-request-employee', 'signature-request-salarie'])
.order('created_at', { ascending: false });
if (error) {
console.error('Erreur lors de la récupération des notifications:', error);
return;
}
// Grouper par contract_id et trouver les dernières notifications
const map = new Map<string, NotificationInfo>();
data?.forEach(log => {
if (!map.has(log.contract_id)) {
map.set(log.contract_id, { employerLastSent: null, employeeLastSent: null });
}
const info = map.get(log.contract_id)!;
if (log.email_type === 'signature-request-employer' && !info.employerLastSent) {
info.employerLastSent = log.created_at;
}
if (['signature-request-employee', 'signature-request-salarie'].includes(log.email_type) && !info.employeeLastSent) {
info.employeeLastSent = log.created_at;
}
});
setNotificationMap(map);
} catch (error) {
console.error('Exception lors de la récupération des notifications:', error);
}
};
// Debounce searches when filters change
useEffect(() => {
// if no filters applied, prefer initial data
@ -1054,6 +1135,14 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [q, structureFilter, typeFilter, etatContratFilters, etatPaieFilter, dpaeFilter, signatureFilter, startFrom, startTo, sortField, sortOrder, limit]);
// Récupérer les notifications quand les données changent
useEffect(() => {
const contractIds = rows.map(r => r.id);
if (contractIds.length > 0) {
fetchLastNotifications(contractIds);
}
}, [rows]);
// Calculate counts for quick filters
useEffect(() => {
const calculateCounts = async () => {
@ -1671,7 +1760,7 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
<th className="text-left px-3 py-2 cursor-pointer" onClick={() => { setSortField('end_date'); setSortOrder((o) => o === 'asc' ? 'desc' : 'asc'); }}>
Date fin {sortField === 'end_date' ? (sortOrder === 'asc' ? '▲' : '▼') : ''}
</th>
<th className="text-left px-3 py-2">Actions</th>
<th className="text-left px-3 py-2" title="Dernières notifications de signature électronique">Notif.</th>
</tr>
</thead>
<tbody>
@ -1775,17 +1864,31 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
<td className="px-3 py-2">{formatDate(r.start_date)}</td>
<td className="px-3 py-2">{formatDate(r.end_date)}</td>
<td className="px-3 py-2">
<div className="flex items-center gap-2">
<button
className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded border hover:bg-indigo-50 text-indigo-700 border-indigo-200 disabled:opacity-50"
title="Relancer le salarié"
onClick={() => handleReminderClick(r)}
disabled={r.contrat_signe === 'Oui'}
>
<BellRing className="w-3 h-3" />
Relancer
</button>
</div>
{(() => {
const notifInfo = notificationMap.get(r.id);
const color = getNotificationColor(r.start_date);
if (!notifInfo || (!notifInfo.employerLastSent && !notifInfo.employeeLastSent)) {
return <span className="text-xs text-slate-400"></span>;
}
return (
<div className={`flex flex-col gap-1 text-xs ${color}`} title={`E: Employeur • S: Salarié\nCouleur basée sur la date de début du contrat`}>
{notifInfo.employerLastSent && (
<div className="flex items-center gap-1">
<span className="font-semibold">E:</span>
<span>{formatNotificationDate(notifInfo.employerLastSent)}</span>
</div>
)}
{notifInfo.employeeLastSent && (
<div className="flex items-center gap-1">
<span className="font-semibold">S:</span>
<span>{formatNotificationDate(notifInfo.employeeLastSent)}</span>
</div>
)}
</div>
);
})()}
</td>
</tr>
))}