Ajout modif groupée état paie état contrat staff

This commit is contained in:
odentas 2025-10-13 00:36:13 +02:00
parent b6a789477c
commit 6caf84c294
5 changed files with 410 additions and 8 deletions

View file

@ -78,6 +78,39 @@ export default function SignaturesElectroniques() {
load();
}, []);
// Ajouter des écouteurs pour recharger les données quand un modal se ferme
useEffect(() => {
const dlgSignature = document.getElementById('dlg-signature') as HTMLDialogElement | null;
const dlgEmbed = document.getElementById('dlg-embed') as HTMLDialogElement | null;
const handleSignatureClose = () => {
console.log('🔄 Modal signature fermé, rechargement des données...');
load();
};
const handleEmbedClose = () => {
console.log('🔄 Modal embed fermé, rechargement des données...');
load();
};
if (dlgSignature) {
dlgSignature.addEventListener('close', handleSignatureClose);
}
if (dlgEmbed) {
dlgEmbed.addEventListener('close', handleEmbedClose);
}
return () => {
if (dlgSignature) {
dlgSignature.removeEventListener('close', handleSignatureClose);
}
if (dlgEmbed) {
dlgEmbed.removeEventListener('close', handleEmbedClose);
}
};
}, []);
// Polling supprimé pour éviter l'interférence avec les signatures
// Les données seront rechargées manuellement ou au refresh de la page

View file

@ -19,9 +19,11 @@ export async function POST(request: NextRequest) {
console.log(`📝 Traitement de ${contractIds.length} contrats pour signature électronique`);
// Déterminer l'URL de base pour les appels API internes
// En production, utiliser toujours l'URL de production principale
const baseUrl = process.env.NEXT_PUBLIC_APP_URL ||
process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` :
'http://localhost:3000';
(process.env.VERCEL_ENV === 'production' ? 'https://paie.odentas.fr' :
process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` :
'http://localhost:3000');
console.log(`🌐 Base URL utilisée: ${baseUrl}`);

View file

@ -0,0 +1,45 @@
import { NextResponse } from "next/server";
import { createSbServer } from "@/lib/supabaseServer";
export async function POST(req: Request) {
try {
const sb = createSbServer();
const { data: { user } } = await sb.auth.getUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { data: me } = await sb.from("staff_users").select("is_staff").eq("user_id", user.id).maybeSingle();
if (!me?.is_staff) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
const { contractIds, etatContrat } = await req.json();
if (!contractIds || !Array.isArray(contractIds) || contractIds.length === 0) {
return NextResponse.json({ error: "Contract IDs are required" }, { status: 400 });
}
if (!etatContrat || !['Reçue', 'En cours', 'Traitée', 'Refusée'].includes(etatContrat)) {
return NextResponse.json({ error: "Valid contract status is required" }, { status: 400 });
}
// Mettre à jour tous les contrats sélectionnés
const { data: updatedContracts, error } = await sb
.from("cddu_contracts")
.update({ etat_de_la_demande: etatContrat })
.in("id", contractIds)
.select("id, etat_de_la_demande");
if (error) {
console.error("Error updating contract status:", error);
return NextResponse.json({ error: "Failed to update contracts" }, { status: 500 });
}
return NextResponse.json({
success: true,
contracts: updatedContracts,
message: `${updatedContracts?.length || 0} contrat(s) mis à jour`
});
} catch (err: any) {
console.error("Bulk contract status update error:", err);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}

View file

@ -0,0 +1,45 @@
import { NextResponse } from "next/server";
import { createSbServer } from "@/lib/supabaseServer";
export async function POST(req: Request) {
try {
const sb = createSbServer();
const { data: { user } } = await sb.auth.getUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { data: me } = await sb.from("staff_users").select("is_staff").eq("user_id", user.id).maybeSingle();
if (!me?.is_staff) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
const { contractIds, etatPaie } = await req.json();
if (!contractIds || !Array.isArray(contractIds) || contractIds.length === 0) {
return NextResponse.json({ error: "Contract IDs are required" }, { status: 400 });
}
if (!etatPaie || !['À traiter', 'En cours', 'Traitée'].includes(etatPaie)) {
return NextResponse.json({ error: "Valid payroll status is required" }, { status: 400 });
}
// Mettre à jour tous les contrats sélectionnés
const { data: updatedContracts, error } = await sb
.from("cddu_contracts")
.update({ etat_de_la_paie: etatPaie })
.in("id", contractIds)
.select("id, etat_de_la_paie");
if (error) {
console.error("Error updating payroll status:", error);
return NextResponse.json({ error: "Failed to update contracts" }, { status: 500 });
}
return NextResponse.json({
success: true,
contracts: updatedContracts,
message: `${updatedContracts?.length || 0} contrat(s) mis à jour`
});
} catch (err: any) {
console.error("Bulk payroll status update error:", err);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}

View file

@ -140,7 +140,10 @@ export default function ContractsGrid({ initialData, activeOrgId }: { initialDat
// Modal states
const [showDpaeModal, setShowDpaeModal] = useState(false);
const [showEtatContratModal, setShowEtatContratModal] = useState(false);
const [showEtatPaieModal, setShowEtatPaieModal] = useState(false);
const [showSalaryModal, setShowSalaryModal] = useState(false);
const [showActionMenu, setShowActionMenu] = useState(false);
// PDF generation state
const [isGeneratingPdfs, setIsGeneratingPdfs] = useState(false);
@ -1004,12 +1007,59 @@ export default function ContractsGrid({ initialData, activeOrgId }: { initialDat
{selectedContractIds.size} contrat{selectedContractIds.size > 1 ? 's' : ''} sélectionné{selectedContractIds.size > 1 ? 's' : ''}
</span>
<div className="flex gap-2">
<button
onClick={() => setShowDpaeModal(true)}
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
Action groupée
</button>
{/* Menu dropdown pour les actions groupées */}
<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>
</button>
{showActionMenu && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setShowActionMenu(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={() => {
setShowDpaeModal(true);
setShowActionMenu(false);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
>
Modifier DPAE
</button>
<button
onClick={() => {
setShowEtatContratModal(true);
setShowActionMenu(false);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
>
Modifier État Contrat
</button>
<button
onClick={() => {
setShowEtatPaieModal(true);
setShowActionMenu(false);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
>
Modifier État Paie
</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"
@ -1220,6 +1270,56 @@ export default function ContractsGrid({ initialData, activeOrgId }: { initialDat
</div>
)}
{/* Modal Action groupée État Contrat */}
{showEtatContratModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-semibold mb-4">Action groupée - État Contrat</h3>
<p className="text-sm text-gray-600 mb-4">
Modifier l'état du contrat pour {selectedContractIds.size} contrat{selectedContractIds.size > 1 ? 's' : ''}
</p>
<EtatContratActionModal
selectedContracts={selectedContracts}
onClose={() => setShowEtatContratModal(false)}
onSuccess={(updatedContracts) => {
// Mettre à jour les contrats dans la liste
setRows(prev => prev.map(row => {
const updated = updatedContracts.find(u => u.id === row.id);
return updated ? { ...row, etat_de_la_demande: updated.etat_de_la_demande } : row;
}));
setShowEtatContratModal(false);
setSelectedContractIds(new Set());
}}
/>
</div>
</div>
)}
{/* Modal Action groupée État Paie */}
{showEtatPaieModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-semibold mb-4">Action groupée - État Paie</h3>
<p className="text-sm text-gray-600 mb-4">
Modifier l'état de la paie pour {selectedContractIds.size} contrat{selectedContractIds.size > 1 ? 's' : ''}
</p>
<EtatPaieActionModal
selectedContracts={selectedContracts}
onClose={() => setShowEtatPaieModal(false)}
onSuccess={(updatedContracts) => {
// Mettre à jour les contrats dans la liste
setRows(prev => prev.map(row => {
const updated = updatedContracts.find(u => u.id === row.id);
return updated ? { ...row, etat_de_la_paie: updated.etat_de_la_paie } : row;
}));
setShowEtatPaieModal(false);
setSelectedContractIds(new Set());
}}
/>
</div>
</div>
)}
{/* Modal Saisir brut */}
{showSalaryModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@ -1377,6 +1477,183 @@ function DpaeActionModal({
);
}
// Modal pour l'état du contrat
function EtatContratActionModal({
selectedContracts,
onClose,
onSuccess
}: {
selectedContracts: Contract[];
onClose: () => void;
onSuccess: (contracts: { id: string; etat_de_la_demande: string }[]) => void;
}) {
const [newEtatContrat, setNewEtatContrat] = useState<string>('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
if (!newEtatContrat) return;
setLoading(true);
try {
const response = await fetch('/api/staff/contracts/bulk-update-etat-contrat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contractIds: selectedContracts.map(c => c.id),
etatContrat: newEtatContrat
})
});
if (!response.ok) throw new Error('Erreur lors de la mise à jour');
const result = await response.json();
onSuccess(result.contracts);
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors de la mise à jour des contrats');
} finally {
setLoading(false);
}
};
return (
<>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nouvel état du contrat
</label>
<select
value={newEtatContrat}
onChange={(e) => setNewEtatContrat(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Sélectionner un état</option>
<option value="Reçue">Reçue</option>
<option value="En cours">En cours</option>
<option value="Traitée">Traitée</option>
<option value="Refusée">Refusée</option>
</select>
</div>
<div className="border rounded-lg p-3 max-h-40 overflow-y-auto">
<p className="text-sm font-medium text-gray-700 mb-2">Contrats sélectionnés :</p>
{selectedContracts.map(contract => (
<div key={contract.id} className="text-sm text-gray-600 py-1">
{contract.contract_number || contract.id} - {formatEmployeeName(contract.employee_name)}
</div>
))}
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
disabled={loading}
>
Annuler
</button>
<button
onClick={handleSubmit}
disabled={!newEtatContrat || loading}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Mise à jour...' : 'Appliquer'}
</button>
</div>
</>
);
}
// Modal pour l'état de la paie
function EtatPaieActionModal({
selectedContracts,
onClose,
onSuccess
}: {
selectedContracts: Contract[];
onClose: () => void;
onSuccess: (contracts: { id: string; etat_de_la_paie: string }[]) => void;
}) {
const [newEtatPaie, setNewEtatPaie] = useState<string>('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
if (!newEtatPaie) return;
setLoading(true);
try {
const response = await fetch('/api/staff/contracts/bulk-update-etat-paie', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contractIds: selectedContracts.map(c => c.id),
etatPaie: newEtatPaie
})
});
if (!response.ok) throw new Error('Erreur lors de la mise à jour');
const result = await response.json();
onSuccess(result.contracts);
} catch (error) {
console.error('Erreur:', error);
alert('Erreur lors de la mise à jour des contrats');
} finally {
setLoading(false);
}
};
return (
<>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nouvel état de la paie
</label>
<select
value={newEtatPaie}
onChange={(e) => setNewEtatPaie(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Sélectionner un état</option>
<option value="À traiter">À traiter</option>
<option value="En cours">En cours</option>
<option value="Traitée">Traitée</option>
</select>
</div>
<div className="border rounded-lg p-3 max-h-40 overflow-y-auto">
<p className="text-sm font-medium text-gray-700 mb-2">Contrats sélectionnés :</p>
{selectedContracts.map(contract => (
<div key={contract.id} className="text-sm text-gray-600 py-1">
{contract.contract_number || contract.id} - {formatEmployeeName(contract.employee_name)}
</div>
))}
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
disabled={loading}
>
Annuler
</button>
<button
onClick={handleSubmit}
disabled={!newEtatPaie || loading}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Mise à jour...' : 'Appliquer'}
</button>
</div>
</>
);
}
// Modal pour saisir le brut
function SalaryInputModal({
selectedContracts,