Ajout modif groupée état paie état contrat staff
This commit is contained in:
parent
b6a789477c
commit
6caf84c294
5 changed files with 410 additions and 8 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
45
app/api/staff/contracts/bulk-update-etat-contrat/route.ts
Normal file
45
app/api/staff/contracts/bulk-update-etat-contrat/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
45
app/api/staff/contracts/bulk-update-etat-paie/route.ts
Normal file
45
app/api/staff/contracts/bulk-update-etat-paie/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue