Merge: résout les conflits et aligne main avec origin/main
This commit is contained in:
commit
74aa268603
3 changed files with 507 additions and 37 deletions
250
app/api/staff/contrats/relance-salarie/route.ts
Normal file
250
app/api/staff/contrats/relance-salarie/route.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
||||
import { cookies } from 'next/headers';
|
||||
import { sendUniversalEmailV2, EmailDataV2 } from '@/lib/emailTemplateService';
|
||||
import { ENV } from '@/lib/cleanEnv';
|
||||
import { detectDemoModeFromHeaders } from '@/lib/demo-detector';
|
||||
|
||||
// POST /api/staff/contrats/relance-salarie
|
||||
// Envoie un email de relance pour la signature d'un contrat salarié
|
||||
export async function POST(req: NextRequest) {
|
||||
// Bloquer l'envoi de relances en mode démo
|
||||
if (detectDemoModeFromHeaders(req.headers)) {
|
||||
console.log('Mode démo détecté - envoi de relance bloqué');
|
||||
return NextResponse.json({ error: 'Action non disponible en mode démo' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { contractId } = await req.json();
|
||||
|
||||
if (!contractId) {
|
||||
return NextResponse.json({ error: 'Contract ID manquant' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Vérification de l'authentification
|
||||
const sb = createRouteHandlerClient({ cookies });
|
||||
const { data: { user } } = await sb.auth.getUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Vérifier staff pour lire la cible via cookie active_org_id
|
||||
let isStaff = false;
|
||||
try {
|
||||
const { data } = await sb.from('staff_users').select('is_staff').eq('user_id', user.id).maybeSingle();
|
||||
isStaff = !!data?.is_staff;
|
||||
} catch {}
|
||||
|
||||
let orgId: string | null = null;
|
||||
try {
|
||||
if (isStaff) {
|
||||
const c = cookies();
|
||||
orgId = c.get('active_org_id')?.value || null;
|
||||
} else {
|
||||
const { data, error } = await sb
|
||||
.from('organization_members')
|
||||
.select('org_id')
|
||||
.eq('user_id', user.id)
|
||||
.single();
|
||||
if (error || !data?.org_id) {
|
||||
return NextResponse.json({ error: 'Aucune organisation active' }, { status: 403 });
|
||||
}
|
||||
orgId = data.org_id;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Récupération des données du contrat depuis Supabase (cddu_contracts)
|
||||
let query = sb
|
||||
.from('cddu_contracts')
|
||||
.select(`
|
||||
id,
|
||||
reference,
|
||||
contract_number,
|
||||
employee_name,
|
||||
employee_matricule,
|
||||
production_name,
|
||||
role,
|
||||
start_date,
|
||||
end_date,
|
||||
docuseal_template_id,
|
||||
docuseal_submission_id,
|
||||
signature_link,
|
||||
org_id
|
||||
`)
|
||||
.eq('id', contractId);
|
||||
|
||||
if (orgId) {
|
||||
query = query.eq('org_id', orgId);
|
||||
}
|
||||
|
||||
const { data: contract, error: contractError } = await query.single();
|
||||
|
||||
if (contractError || !contract) {
|
||||
console.error('Erreur récupération contrat:', contractError);
|
||||
return NextResponse.json({ error: 'Contrat non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Récupération du slug du salarié via DocuSeal API
|
||||
let employeeSlug: string | null = null;
|
||||
let docusealEmail: string | null = null;
|
||||
if (contract.docuseal_submission_id) {
|
||||
try {
|
||||
const docusealResponse = await fetch(`https://api.docuseal.eu/submissions/${contract.docuseal_submission_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Token': ENV.DOCUSEAL_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (docusealResponse.ok) {
|
||||
const docusealData = await docusealResponse.json();
|
||||
const submitters = docusealData?.submitters || [];
|
||||
const employeeSubmitter = submitters.find((s: any) => s.role === 'Salarié');
|
||||
employeeSlug = employeeSubmitter?.slug || null;
|
||||
if (employeeSubmitter?.email) {
|
||||
docusealEmail = String(employeeSubmitter.email);
|
||||
}
|
||||
} else {
|
||||
console.error('DocuSeal API error:', docusealResponse.status, await docusealResponse.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération DocuSeal:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Construction du lien de signature
|
||||
let signatureLink: string | null = null;
|
||||
if (employeeSlug) {
|
||||
const siteBase = process.env.NEXT_PUBLIC_SITE_URL || 'https://paie.odentas.fr';
|
||||
signatureLink = `${siteBase}/signature-salarie?docuseal_id=${employeeSlug}`;
|
||||
}
|
||||
|
||||
if (!signatureLink) {
|
||||
return NextResponse.json({ error: 'Lien de signature indisponible' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Formatage des données pour l'email
|
||||
const formattedDate = formatDate((contract as any).start_date);
|
||||
|
||||
// Récupérer le nom d'employeur depuis organizations.name
|
||||
let employerName = 'Employeur';
|
||||
try {
|
||||
const targetOrgId = (contract as any).org_id || orgId;
|
||||
if (targetOrgId) {
|
||||
const { data: org } = await sb
|
||||
.from('organizations')
|
||||
.select('name')
|
||||
.eq('id', targetOrgId)
|
||||
.maybeSingle();
|
||||
employerName = org?.name || (contract as any).structure || 'Employeur';
|
||||
} else {
|
||||
employerName = (contract as any).structure || 'Employeur';
|
||||
}
|
||||
} catch (e) {
|
||||
employerName = (contract as any).structure || 'Employeur';
|
||||
}
|
||||
|
||||
// Récupérer le prénom du salarié depuis la base de données
|
||||
let prenom_salarie = '';
|
||||
if (contract.employee_matricule) {
|
||||
try {
|
||||
const { data: salData } = await sb
|
||||
.from('salaries')
|
||||
.select('prenom')
|
||||
.or(`code_salarie.eq.${contract.employee_matricule},num_salarie.eq.${contract.employee_matricule}`)
|
||||
.limit(1);
|
||||
|
||||
if (salData && salData[0]?.prenom) {
|
||||
prenom_salarie = salData[0].prenom;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de la récupération du prénom depuis salaries:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!prenom_salarie) {
|
||||
console.error('Prénom du salarié manquant pour le contrat:', contract.id);
|
||||
return NextResponse.json({
|
||||
error: 'Le prénom du salarié est manquant dans la base de données. Veuillez le renseigner avant de relancer la signature.'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Récupérer l'email du salarié depuis salaries.adresse_mail
|
||||
let toEmail: string | null = null;
|
||||
if (contract.employee_matricule) {
|
||||
try {
|
||||
let salQ = sb
|
||||
.from('salaries')
|
||||
.select('adresse_mail')
|
||||
.or(`code_salarie.eq.${contract.employee_matricule},num_salarie.eq.${contract.employee_matricule}`)
|
||||
.limit(1);
|
||||
if (orgId) salQ = salQ.eq('employer_id', orgId);
|
||||
const { data: salData, error: salErr } = await salQ;
|
||||
if (!salErr && salData && salData[0]?.adresse_mail) {
|
||||
toEmail = salData[0].adresse_mail as string;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Impossible de récupérer adresse_mail depuis salaries:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback via DocuSeal si disponible
|
||||
if (!toEmail && docusealEmail) {
|
||||
toEmail = docusealEmail;
|
||||
}
|
||||
|
||||
if (!toEmail) {
|
||||
return NextResponse.json({ error: 'Email du salarié manquant' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Envoi de l'email via le template universel (variant salarié)
|
||||
const emailData: EmailDataV2 = {
|
||||
firstName: prenom_salarie,
|
||||
organizationName: employerName,
|
||||
matricule: contract.employee_matricule || (contract as any).matricule || '',
|
||||
profession: contract.role || 'Contrat',
|
||||
startDate: formattedDate,
|
||||
productionName: (contract as any).production_name || '',
|
||||
documentType: contract.role || 'Contrat',
|
||||
contractReference: contract.reference || String(contract.id),
|
||||
ctaUrl: signatureLink,
|
||||
};
|
||||
|
||||
const messageId = await sendUniversalEmailV2({
|
||||
type: 'signature-request-employee',
|
||||
toEmail,
|
||||
subject: `[Rappel] Signez votre contrat ${employerName}`,
|
||||
data: emailData,
|
||||
});
|
||||
|
||||
console.log('Email de relance salarié envoyé:', {
|
||||
contractId,
|
||||
email: toEmail,
|
||||
messageId
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Email de relance envoyé avec succès',
|
||||
messageId
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Erreur envoi relance:', error);
|
||||
return NextResponse.json({
|
||||
error: 'Erreur lors de l\'envoi de la relance',
|
||||
message: error.message
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction pour formater une date ISO au format DD/MM/AAAA
|
||||
function formatDate(isoDate: string): string {
|
||||
if (!isoDate) return '';
|
||||
const date = new Date(isoDate);
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
|
@ -3,13 +3,14 @@
|
|||
import { useEffect, useMemo, useState, useRef, useImperativeHandle, forwardRef } from "react";
|
||||
import { supabase } from "@/lib/supabaseClient";
|
||||
import Link from "next/link";
|
||||
import { RefreshCw, FileDown, Eye, FileSignature, Check, X } from "lucide-react";
|
||||
import { RefreshCw, Check, X, Settings, FileText, CheckCircle, BarChart3, Eye, ChevronDown, Trash2, FileDown, FileSignature, DollarSign, XCircle, BellRing } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import BulkPdfProgressModal from "./BulkPdfProgressModal";
|
||||
import PdfVerificationModal from "./PdfVerificationModal";
|
||||
import ContractDetailsModal from "./ContractDetailsModal";
|
||||
import BulkESignProgressModal from "./BulkESignProgressModal";
|
||||
import BulkESignConfirmModal from "./BulkESignConfirmModal";
|
||||
import { EmployeeReminderModal } from "./contracts/EmployeeReminderModal";
|
||||
|
||||
// Utility function to format dates as DD/MM/YYYY
|
||||
function formatDate(dateString: string | null | undefined): string {
|
||||
|
|
@ -191,6 +192,7 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
const [showEtatPaieModal, setShowEtatPaieModal] = useState(false);
|
||||
const [showSalaryModal, setShowSalaryModal] = useState(false);
|
||||
const [showActionMenu, setShowActionMenu] = useState(false);
|
||||
const [showESignMenu, setShowESignMenu] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
// Quick filter counts
|
||||
|
|
@ -222,6 +224,11 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
||||
const [contractDetailsIds, setContractDetailsIds] = useState<string[]>([]);
|
||||
|
||||
// Employee Reminder state
|
||||
const [showEmployeeReminderModal, setShowEmployeeReminderModal] = useState(false);
|
||||
const [selectedContractForReminder, setSelectedContractForReminder] = useState<any>(null);
|
||||
const [isLoadingReminder, setIsLoadingReminder] = useState(false);
|
||||
|
||||
// Quick filters helpers
|
||||
const toYMD = (d: Date) => {
|
||||
const y = d.getFullYear();
|
||||
|
|
@ -1122,6 +1129,44 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
calculateCounts();
|
||||
}, []);
|
||||
|
||||
// Fonction pour relancer un salarié spécifique
|
||||
const handleReminderClick = (contract: any) => {
|
||||
setSelectedContractForReminder(contract);
|
||||
setShowEmployeeReminderModal(true);
|
||||
};
|
||||
|
||||
// Fonction pour envoyer la relance au salarié
|
||||
const sendEmployeeReminder = async () => {
|
||||
if (!selectedContractForReminder) return;
|
||||
|
||||
setIsLoadingReminder(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/staff/contrats/relance-salarie', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ contractId: selectedContractForReminder.id }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Email de relance envoyé avec succès à ${selectedContractForReminder.employee_name}`);
|
||||
setShowEmployeeReminderModal(false);
|
||||
setSelectedContractForReminder(null);
|
||||
} else {
|
||||
toast.error(result.error || result.message || 'Erreur lors de l\'envoi de la relance');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Erreur relance salarié:', error);
|
||||
toast.error(error.message || 'Erreur lors de l\'envoi de la relance');
|
||||
} finally {
|
||||
setIsLoadingReminder(false);
|
||||
}
|
||||
};
|
||||
|
||||
// derive options from initialData for simple selects
|
||||
const structures = useMemo(() => {
|
||||
const uniqueStructures = Array.from(new Set((initialData || []).map((r) => r.structure).filter(Boolean) as string[]));
|
||||
|
|
@ -1342,16 +1387,15 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
{selectedContractIds.size} contrat{selectedContractIds.size > 1 ? 's' : ''} sélectionné{selectedContractIds.size > 1 ? 's' : ''}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
{/* Menu dropdown pour les actions groupées */}
|
||||
{/* Menu dropdown Actions contrat */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowActionMenu(!showActionMenu)}
|
||||
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex items-center gap-1"
|
||||
>
|
||||
Action groupée
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<Settings className="w-4 h-4" />
|
||||
Actions contrat
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{showActionMenu && (
|
||||
|
|
@ -1367,8 +1411,9 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
setShowDpaeModal(true);
|
||||
setShowActionMenu(false);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
Modifier DPAE
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -1376,8 +1421,9 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
setShowEtatContratModal(true);
|
||||
setShowActionMenu(false);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Modifier État Contrat
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -1385,16 +1431,18 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
setShowEtatPaieModal(true);
|
||||
setShowActionMenu(false);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
Modifier État Paie
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
viewSelectedDetails();
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
Voir les détails
|
||||
</button>
|
||||
<div className="border-t border-gray-200 my-1"></div>
|
||||
|
|
@ -1403,8 +1451,9 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
setShowDeleteModal(true);
|
||||
setShowActionMenu(false);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Supprimer les contrats
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1412,41 +1461,77 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Menu dropdown E-signature */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowESignMenu(!showESignMenu)}
|
||||
className="px-3 py-1 text-sm bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<FileSignature className="w-4 h-4" />
|
||||
E-signature
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{showESignMenu && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setShowESignMenu(false)}
|
||||
/>
|
||||
<div className="absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg z-20 border border-gray-200">
|
||||
<div className="py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
generateBatchPdfs();
|
||||
setShowESignMenu(false);
|
||||
}}
|
||||
disabled={isGeneratingPdfs}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors disabled:text-gray-400 disabled:hover:bg-white flex items-center gap-2"
|
||||
>
|
||||
<FileDown className="w-4 h-4" />
|
||||
{isGeneratingPdfs ? "Génération..." : "Créer les PDF"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
verifySelectedPdfs();
|
||||
setShowESignMenu(false);
|
||||
}}
|
||||
disabled={isLoadingPdfs}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors disabled:text-gray-400 disabled:hover:bg-white flex items-center gap-2"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{isLoadingPdfs ? "Chargement..." : "Vérifier PDFs"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleBulkESignClick();
|
||||
setShowESignMenu(false);
|
||||
}}
|
||||
disabled={isGeneratingESign}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors disabled:text-gray-400 disabled:hover:bg-white flex items-center gap-2"
|
||||
>
|
||||
<FileSignature className="w-4 h-4" />
|
||||
{isGeneratingESign ? "Envoi..." : "Envoyer e-sign"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowSalaryModal(true)}
|
||||
className="px-3 py-1 text-sm bg-green-600 text-white rounded hover:bg-green-700 transition-colors"
|
||||
className="px-3 py-1 text-sm bg-green-600 text-white rounded hover:bg-green-700 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<DollarSign className="w-4 h-4" />
|
||||
Saisir brut
|
||||
</button>
|
||||
<button
|
||||
onClick={generateBatchPdfs}
|
||||
disabled={isGeneratingPdfs}
|
||||
className="px-3 py-1 text-sm bg-orange-600 text-white rounded hover:bg-orange-700 transition-colors disabled:bg-orange-400 flex items-center gap-1"
|
||||
>
|
||||
<FileDown className="size-3" />
|
||||
{isGeneratingPdfs ? "Génération..." : "Créer les PDF"}
|
||||
</button>
|
||||
<button
|
||||
onClick={verifySelectedPdfs}
|
||||
disabled={isLoadingPdfs}
|
||||
className="px-3 py-1 text-sm bg-purple-600 text-white rounded hover:bg-purple-700 transition-colors disabled:bg-purple-400 flex items-center gap-1"
|
||||
>
|
||||
<Eye className="size-3" />
|
||||
{isLoadingPdfs ? "Chargement..." : "Vérifier PDFs"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBulkESignClick}
|
||||
disabled={isGeneratingESign}
|
||||
className="px-3 py-1 text-sm bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors disabled:bg-indigo-400 flex items-center gap-1"
|
||||
>
|
||||
<FileSignature className="size-3" />
|
||||
{isGeneratingESign ? "Envoi..." : "Envoyer e-sign"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedContractIds(new Set())}
|
||||
className="px-3 py-1 text-sm bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors"
|
||||
className="px-3 py-1 text-sm bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
Désélectionner tout
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1485,6 +1570,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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -1587,6 +1673,16 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
}</td>
|
||||
<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">
|
||||
<button
|
||||
onClick={() => handleReminderClick(r)}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-white bg-indigo-600 rounded hover:bg-indigo-700 transition-colors"
|
||||
title="Relancer le salarié"
|
||||
>
|
||||
<BellRing className="w-3.5 h-3.5" />
|
||||
Relancer
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -1789,6 +1885,22 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal de relance salarié */}
|
||||
<EmployeeReminderModal
|
||||
isOpen={showEmployeeReminderModal}
|
||||
onClose={() => {
|
||||
setShowEmployeeReminderModal(false);
|
||||
setSelectedContractForReminder(null);
|
||||
}}
|
||||
onConfirm={sendEmployeeReminder}
|
||||
isLoading={isLoadingReminder}
|
||||
contractData={{
|
||||
reference: selectedContractForReminder?.reference || selectedContractForReminder?.contract_number,
|
||||
employee_name: selectedContractForReminder?.employee_name,
|
||||
employee_email: selectedContractForReminder?.employee_email
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
108
components/staff/contracts/EmployeeReminderModal.tsx
Normal file
108
components/staff/contracts/EmployeeReminderModal.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import React from 'react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Send, AlertTriangle } from "lucide-react";
|
||||
|
||||
interface EmployeeReminderModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
isLoading: boolean;
|
||||
contractData: {
|
||||
reference?: string;
|
||||
employee_name?: string;
|
||||
employee_email?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const EmployeeReminderModal: React.FC<EmployeeReminderModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
isLoading,
|
||||
contractData
|
||||
}) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Send className="size-5 text-indigo-600" />
|
||||
Relancer le salarié
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Confirmer l'envoi d'un email de rappel pour la signature du contrat.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
<div className="bg-indigo-50 border border-indigo-200 rounded-lg p-4 mb-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="size-5 text-indigo-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-indigo-800">
|
||||
<p className="font-medium mb-1">Action de relance</p>
|
||||
<p>
|
||||
Un email de rappel va être envoyé au salarié pour l'inviter
|
||||
à signer le contrat de travail.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<span className="font-medium text-gray-600">Contrat :</span>
|
||||
<span className="col-span-2">{contractData.reference || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<span className="font-medium text-gray-600">Salarié :</span>
|
||||
<span className="col-span-2">{contractData.employee_name || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<span className="font-medium text-gray-600">Email salarié :</span>
|
||||
<span className="col-span-2 font-mono text-xs bg-gray-100 px-2 py-1 rounded">
|
||||
{contractData.employee_email || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
disabled={isLoading}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Send className="size-4 mr-2 animate-pulse" />
|
||||
Envoi en cours...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="size-4 mr-2" />
|
||||
Envoyer la relance
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in a new issue