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:
parent
d81a12de6e
commit
807cb20456
1 changed files with 115 additions and 12 deletions
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Reference in a new issue