15 Commits

Author SHA1 Message Date
267994044c feature: Affichage de tous les detail de la resevation
Some checks failed
Organisation/sirh/pipeline/head There was a failure building this commit
2026-06-08 11:06:22 +00:00
a4b4a68dd2 feature: Affichage du message sur l'tat du contrat 2026-05-11 13:13:08 +02:00
07dc097d27 feature: Affichage des details de la reservation 2026-05-11 13:12:42 +02:00
4146563f41 Fonctionnalite : Ajout de la liste des contrats 2026-05-11 13:05:06 +02:00
c0cdca48fa Fonctionnalite: Ajout des contrats 2026-05-11 12:43:16 +02:00
9d9e6c6549 Bug: Affichage de la lite des conges en fonction des profil
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-05-07 15:32:22 +00:00
784822478e feature: Affichage de la liste des bailleur 2026-05-07 15:28:52 +00:00
ab87baaa3a Bug: Affichage des details de la reservation 2026-05-07 15:28:42 +00:00
6026af5498 Bug : Affichages des conges 2026-05-07 15:23:47 +00:00
91bdd791f1 Bug : Affichage des demandes de conges 2026-05-07 15:11:25 +00:00
671072864d Bug: Details des reservations
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-05-07 14:44:16 +00:00
bb93a853db Gestion de l'envoie des mails
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-05-07 11:25:11 +00:00
99434a21e0 Fonctionnalite: verifier qu'aucun contrat n'est actif avant la creation d'un nouveau pour un employe
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-05-06 16:01:37 +00:00
f6b90e7dd0 Verification du statut avant la creation d'un contrat 2026-05-06 15:53:02 +00:00
fcfac71026 clean: remove pycache from tracking 2026-05-06 15:44:16 +00:00
49 changed files with 691 additions and 155 deletions

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ venv/
media/ media/
staticfiles/ staticfiles/
.env .env
migrations/ migrations/
*.pyc

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -150,6 +150,7 @@ MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Configuration de l'email
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'ssl0.ovh.net' EMAIL_HOST = 'ssl0.ovh.net'
EMAIL_PORT = 465 EMAIL_PORT = 465

View File

@@ -1,5 +1,5 @@
from django.utils import timezone from django.utils import timezone
from gestion_employe.models import Contrat from gestion_employe.models import Contrat, Employe
from gestion_conge.models import Conge from gestion_conge.models import Conge
@@ -7,6 +7,7 @@ QUOTA_CONGE_ANNUEL = 30
NOMBRE_PAGINATION = 8 NOMBRE_PAGINATION = 8
DEBUT_RAPPEL = 60 DEBUT_RAPPEL = 60
DUREE_FIN_CONTRAT = 90 DUREE_FIN_CONTRAT = 90
EMAIL_ASSISTANTE_DE_DIRECTION = list(Employe.objects.filter(fonction="assistant_direction").values_list('user__email', flat=True))
def solde_conge(employe): def solde_conge(employe):
"""Fonction de calcul du solde de congé restant l'employé""" """Fonction de calcul du solde de congé restant l'employé"""
@@ -31,4 +32,26 @@ def solde_conge(employe):
"success": True, "success": True,
"quota_annuel": QUOTA_CONGE_ANNUEL - jours_conges_valider, "quota_annuel": QUOTA_CONGE_ANNUEL - jours_conges_valider,
"nombre_jours_valide": jours_conges_valider "nombre_jours_valide": jours_conges_valider
} }
def envoyer_mail(sujet, message, destinataires):
"""Fonction d'envoi de mail"""
from django.core.mail import send_mail
from django.conf import settings
send_mail(
sujet,
message,
settings.EMAIL_HOST_USER,
destinataires,
fail_silently=False,
)
def destinataire_mail_demande_conges(employe):
"""Fonction de récupération des destinataires pour les mails de demande de congés"""
if employe.chef:
return EMAIL_ASSISTANTE_DE_DIRECTION
else:
if employe.departement:
chefs_departement = Employe.objects.filter(departement=employe.departement, chef=True)
return list(chefs_departement.values_list('user__email', flat=True))

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -83,7 +83,6 @@ def index(request):
Q(validation_hierarchique = True) | Q(validation_hierarchique = False) Q(validation_hierarchique = True) | Q(validation_hierarchique = False)
).order_by('-date_demande') ).order_by('-date_demande')
return render(request, 'gestion_conge/index.html', { return render(request, 'gestion_conge/index.html', {
"nombre_conges_valide": nombre_conges_valide, "nombre_conges_valide": nombre_conges_valide,
"nombre_conges_refuse": nombre_conges_refuse, "nombre_conges_refuse": nombre_conges_refuse,
@@ -123,72 +122,82 @@ def demander_conge(request):
conge_obj.save() conge_obj.save()
messages.success(request, "Votre demande de congé a été enregistrée.") messages.success(request, "Votre demande de congé a été enregistrée.")
fonctions_utilitaire.envoyer_mail(
sujet = "Demande de congé",
message = f"""Bonjour {employe.user.first_name} {employe.user.last_name}, votre demande de congé a été enregistrée. Veuillez consulter votre profil pour plus de détails.""",
destinataires = fonctions_utilitaire.destinataire_mail_demande_conges() + [employe.user.email]
)
return redirect("gestion_conges:conge") return redirect("gestion_conges:conge")
return redirect("gestion_conges:conge") return redirect("gestion_conges:conge")
@login_required @login_required
def liste_demande_conges(request): def liste_demande_conges(request):
"""Vue de liste des demandes de congés en attente de validation selon le statut de l'utilisateur actuel"""
try: try:
employe = Employe.objects.get(user__username = request.user) employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist: except Employe.DoesNotExist:
return JsonResponse({ return JsonResponse({
"success": False, "success": False,
"message": "Votre profil Utilisateur n'est lié à aucun profil Employé. Veuillez contacter l'administrateur." "message": "Profil employé introuvable"
}) })
try: affectation = Affectation.objects.filter(
affectation = Affectation.objects.get( employe=employe,
employe=employe, date_fin_daffectation__gte=timezone.now().date()
date_fin_daffectation__gte=timezone.now().date() ).first()
)
except Affectation.DoesNotExist: is_direction = employe.user.groups.filter(name='direction').exists()
affectation = None
if employe.chef: if employe.chef:
print("chef")
conges_en_attente = Conge.objects.filter( conges = Conge.objects.filter(
employe__departement = employe.departement, Q(employe__departement=employe.departement) |
validation_hierarchique = None Q(employe=employe)
).order_by('-date_demande') ).order_by('-date_demande')
elif affectation and affectation.role == "chef_projet": elif affectation and affectation.role == "chef_projet":
employes_du_projet = Affectation.objects.filter( employes_du_projet = Affectation.objects.filter(
projet = affectation.projet, projet=affectation.projet,
date_fin_daffectation__gte = timezone.now().date() date_fin_daffectation__gte=timezone.now().date()
).values('employe') ).values_list('employe', flat=True)
conges_en_attente = Conge.objects.filter( conges = Conge.objects.filter(
employe__in = employes_du_projet, Q(employe__in=employes_du_projet) |
validation_hierarchique = None Q(employe=employe)
).order_by('-date_demande') ).order_by('-date_demande')
elif 'direction' in employe.user.groups.values_list('name', flat=True):
conges_en_attente = Conge.objects.filter( elif is_direction:
validation_hierarchique = True,
validation_direction = None conges = Conge.objects.filter(
).order_by('-date_demande') Q(validation_hierarchique=True) |
Q(employe__user__groups__name='direction')
).distinct().order_by('-date_demande')
else: else:
conges_en_attente = Conge.objects.filter(
employe__user__username = request.user conges = Conge.objects.filter(
employe=employe
).order_by('-date_demande') ).order_by('-date_demande')
return JsonResponse({ return JsonResponse({
"success": True, "success": True,
"data":[ "data": [
{ {
**model_to_dict(conge), **model_to_dict(conge),
"prenom_nom": f"{conge.employe.user.first_name} {conge.employe.user.last_name}", "prenom_nom": f"{conge.employe.user.first_name} {conge.employe.user.last_name}",
"date_demande": conge.date_demande, "date_demande": conge.date_demande,
"nombre_jours": conge.nombre_jours, "nombre_jours": conge.nombre_jours,
"type": dict(conge.TYPE_CHOICES).get(conge.type), "type": dict(conge.TYPE_CHOICES).get(conge.type),
"solde_conge": fonctions_utilitaire.solde_conge(conge.employe)["quota_annuel"] "solde_conge": fonctions_utilitaire.solde_conge(conge.employe)["quota_annuel"]
} }
for conge in conges_en_attente]}, for conge in conges
safe=False ]
) })
@login_required @login_required
def validation_de_conge(request): def validation_de_conge(request):

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -19,12 +19,13 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if not expiration_contrat %} {% if not has_contrat %}
<div class="alert alert-danger fade show alert-dismissible mt-2"> <div class="alert alert-danger mt-2">
<strong>Important :</strong> Les informations sur votre contrat n'ont pas été renseignées, veuillez contacter les ressources humaines. <strong>Important :</strong> Les informations sur votre contrat n'ont pas été renseignées, veuillez contacter les ressources humaines.
</div> </div>
{% elif contrat_nb_jours_restant %}
<div class="alert alert-danger fade show alert-dismissible mt-2"> {% elif expiration_contrat %}
<div class="alert alert-warning mt-2">
<strong>Important :</strong> Votre contrat de travail expire dans {{ contrat_nb_jours_restant }} jours, veuillez contacter les ressources humaines. <strong>Important :</strong> Votre contrat de travail expire dans {{ contrat_nb_jours_restant }} jours, veuillez contacter les ressources humaines.
</div> </div>
{% endif %} {% endif %}

View File

@@ -0,0 +1,24 @@
<div class="modal fade" id="modalContratsEmploye" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">📄 Contrats de l'employé</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="contrats-employe-body">
<!-- contenu dynamique JS -->
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">
Fermer
</button>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import json import json
from datetime import timedelta, datetime from datetime import date, timedelta, datetime
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.utils import timezone from django.utils import timezone
@@ -109,6 +109,13 @@ def affecter_employe_projet(request):
} }
) )
messages.success(request, f"L'employé {employe.user.first_name} {employe.user.last_name} a été affecté au projet {projet.nom_projet}.") messages.success(request, f"L'employé {employe.user.first_name} {employe.user.last_name} a été affecté au projet {projet.nom_projet}.")
fonctions_utilitaire.envoyer_mail(
sujet = "Affectation à un projet",
message = f"""Bonjour {employe.user.first_name} {employe.user.last_name}, vous avez été affecté au projet {projet.nom_projet.upper()} pour la période du {form.cleaned_data['date_affectation'].strftime('%d/%m/%Y')} au {date_fin_affectation.strftime('%d/%m/%Y')} en tant que {dict(Affectation.ROLE_CHOICES).get(form.cleaned_data['role'])}.
Veuillez consulter votre profil pour plus de détails.""",
destinataires = [employe.user.email]
)
return redirect('gestion_employe:index') return redirect('gestion_employe:index')
else: else:
messages.error(request, "Erreur : Formulaire non valide.") messages.error(request, "Erreur : Formulaire non valide.")
@@ -122,26 +129,41 @@ def mon_profil(request):
try: try:
employe = Employe.objects.get(user__username=request.user) employe = Employe.objects.get(user__username=request.user)
except Employe.DoesNotExist: except Employe.DoesNotExist:
messages.error(request, "Impossible d'acceder au menu 'Mon profil' car votre profil Utilisateur n'est lié à aucun profil Employé. Veuillez contacter l'Administrateur.") messages.error(
request,
"Impossible d'acceder au menu 'Mon profil' car votre profil Utilisateur n'est lié à aucun profil Employé. Veuillez contacter l'Administrateur."
)
return redirect("gestion_conges:conge") return redirect("gestion_conges:conge")
contrats = Contrat.objects.filter(employe=employe, statut='actif').first() contrats = Contrat.objects.filter(employe=employe, statut='actif').first()
projets = Affectation.objects.filter( projets = Affectation.objects.filter(
employe = employe, employe=employe,
date_fin_daffectation__gte = timezone.now().date() date_fin_daffectation__gte=timezone.now().date()
).select_related('projet') ).select_related('projet')
has_contrat = contrats is not None
expiration_contrat = False
contrat_nb_jours_restant = None
if contrats:
nb_jours = contrats.nombre_jours_restant
contrat_nb_jours_restant = nb_jours
expiration_contrat = nb_jours <= fonctions_utilitaire.DUREE_FIN_CONTRAT
return render( return render(
request, request,
'gestion_employe/monprofil.html', 'gestion_employe/monprofil.html',
{ {
'employe': employe, 'employe': employe,
'contrats': [{ 'contrats': [{
**model_to_dict(contrats), **model_to_dict(contrats),
"type_contrat": dict(Contrat.TYPE_CONTRAT).get(contrats.type_contrat), "type_contrat": dict(Contrat.TYPE_CONTRAT).get(contrats.type_contrat),
"statut": dict(Contrat.STATUT_CONTRAT).get(contrats.statut), "statut": dict(Contrat.STATUT_CONTRAT).get(contrats.statut),
"fichier_contrat": contrats.fichier_contrat.url if contrats.fichier_contrat else "", "fichier_contrat": contrats.fichier_contrat.url if contrats.fichier_contrat else "",
} if contrats else []], }] if contrats else [],
'projets': [ 'projets': [
{ {
**model_to_dict(a.projet), **model_to_dict(a.projet),
@@ -151,12 +173,13 @@ def mon_profil(request):
"pourcentage_temps_affectation": a.pourcentage_temps_affectation "pourcentage_temps_affectation": a.pourcentage_temps_affectation
} for a in projets } for a in projets
], ],
"formation_form": FormationForm(), "formation_form": FormationForm(),
"expiration_contrat": contrats.nombre_jours_restant <= fonctions_utilitaire.DUREE_FIN_CONTRAT if contrats else False, "has_contrat": has_contrat,
"contrat_nb_jours_restant": contrats.nombre_jours_restant if contrats else None "expiration_contrat": expiration_contrat,
"contrat_nb_jours_restant": contrat_nb_jours_restant
} }
) )
@login_required @login_required
def modifier_mot_passe(request): def modifier_mot_passe(request):
"""Vue pour permettre à un utilisateur de modifier son mot de passe et ses informations de profil""" """Vue pour permettre à un utilisateur de modifier son mot de passe et ses informations de profil"""
@@ -247,14 +270,26 @@ def suppression_affectation(request):
return JsonResponse({"message": "Affectation supprimée avec succès."}) return JsonResponse({"message": "Affectation supprimée avec succès."})
def creation_contrat(request): def creation_contrat(request):
"""Vue pour permettre à un utilisateur de créer un contrat pour un employé""" """Créer un contrat pour un employé (avec contrôle d'existence de contrat actif)"""
try: try:
employe = Employe.objects.get(id=request.POST.get('employe_id')) employe = Employe.objects.get(id=request.POST.get('employe_id'))
except Employe.DoesNotExist: except Employe.DoesNotExist:
messages.error(request, "Employé non trouvé.") messages.error(request, "Employé non trouvé.")
return redirect('employe-index') return redirect('employe-index')
contrat_actif = Contrat.objects.filter(
employe=employe,
date_fin__gte=date.today()
).exists()
if request.method == "POST": if request.method == "POST":
if contrat_actif:
messages.error(
request,
"Impossible de créer un contrat : cet employé a déjà un contrat actif."
)
return redirect('gestion_employe:index')
form = ContratForm(request.POST, request.FILES) form = ContratForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
contrat = form.save(commit=False) contrat = form.save(commit=False)
@@ -263,9 +298,13 @@ def creation_contrat(request):
messages.success(request, "Contrat créé avec succès.") messages.success(request, "Contrat créé avec succès.")
return redirect('gestion_employe:index') return redirect('gestion_employe:index')
messages.error(request, "Formulaire non valide") messages.error(request, "Formulaire non valide")
else: else:
form = ContratForm(initial={'employe': employe}) form = ContratForm(initial={'employe': employe})
return render(request, 'gestion_employe/index.html', {'contrat_form': form})
return render(request, 'gestion_employe/index.html', {
'contrat_form': form
})
@login_required @login_required
def enregistrer_detail_employe(request): def enregistrer_detail_employe(request):
@@ -413,4 +452,4 @@ def supprimer_formation(request, id_formation):
if request.method == "POST": if request.method == "POST":
formation.delete() formation.delete()
messages.success(request, "Formation supprimée ") messages.success(request, "Formation supprimée ")
return redirect("mes_formations") return redirect("mes_formations")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,5 @@
const btnEnregistrerBailleur = document.getElementById('btnEnregistrerBailleur'); const btnEnregistrerBailleur = document.getElementById('btnEnregistrerBailleur');
let table;
btnEnregistrerBailleur.addEventListener('click', function() { btnEnregistrerBailleur.addEventListener('click', function() {
const form = document.getElementById('formBailleur'); const form = document.getElementById('formBailleur');
@@ -20,4 +21,27 @@ btnEnregistrerBailleur.addEventListener('click', function() {
alert('Ce bailleur existe déjà dans la base de données.'); alert('Ce bailleur existe déjà dans la base de données.');
} }
}); });
});
table = new Tabulator("#table-bailleurs", {
ajaxURL: "/gestion-projet/bailleurs/",
layout: "fitColumns",
pagination: "local",
paginationSize: 5,
columns: [
{title: "#", formatter: "rownum", width: 60},
{title: "Organisme", field: "nom_organisme"},
{title: "Contact", field: "contact"},
{title: "Email", field: "email"},
{title: "Pays", field: "pays"},
],
rowDblClick: function(e, row) {
const data = row.getData();
if (confirm(`Voulez-vous vraiment supprimer ${data.nom_organisme} ?`)) {
supprimerBailleur(data.id);
}
}
}); });

View File

@@ -1,19 +1,49 @@
<div class="modal fade" id="modalBailleur" tabindex="-1" aria-labelledby="modalBailleurLabel" aria-hidden="true"> <div class="modal fade" id="modalBailleur" tabindex="-1" aria-labelledby="modalBailleurLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog ">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="modalBailleurLabel">Ajouter un Bailleur</h5> <h5 class="modal-title" id="modalBailleurLabel">Gestion des Bailleurs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body p-4"> <ul class="nav nav-tabs px-3 pt-2" id="bailleurTab" role="tablist">
<form id="formBailleur" method="POST" action="{% url 'gestion_projet:creation-bailleur' %}"> <li class="nav-item" role="presentation">
{% csrf_token %} <button class="nav-link active" id="ajouter-tab" data-bs-toggle="tab"
{{ form_ajout_bailleur.as_p }} data-bs-target="#ajouter" type="button" role="tab">
</form> Ajouter
</div> </button>
<div class="modal-footer"> </li>
<button type="submit" id="btnEnregistrerBailleur" class="btn btn-success"><i class="bi bi-save me-1"></i> Enregistrer</button> <li class="nav-item" role="presentation">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button> <button class="nav-link" id="liste-tab" data-bs-toggle="tab"
data-bs-target="#liste" type="button" role="tab">
Liste
</button>
</li>
</ul>
<div class="modal-body">
<div class="tab-content pt-3">
<div class="tab-pane fade show active" id="ajouter" role="tabpanel">
<form id="formBailleur" method="POST" action="{% url 'gestion_projet:creation-bailleur' %}">
{% csrf_token %}
{{ form_ajout_bailleur.as_p }}
<button type="submit" class="btn btn-success mt-3" id="btnEnregistrerBailleur">
<i class="bi bi-save me-1"></i> Enregistrer
</button>
</form>
</div>
<div class="tab-pane fade" id="liste" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Liste des bailleurs</h6>
</div>
<div class="row">
<div class="col">
<div id="table-bailleurs"
data-url="{% url 'gestion_projet:liste-bailleurs' %}">
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -19,6 +19,12 @@ urlpatterns = [
views.creation_projet, views.creation_projet,
name='creation-projet' name='creation-projet'
), ),
path(
'bailleurs/',
views.liste_bailleur,
name='liste-bailleurs'
),
path( path(
'projet/modifier/<int:projet_id>/', 'projet/modifier/<int:projet_id>/',
views.modification_projet, views.modification_projet,
@@ -84,6 +90,7 @@ urlpatterns = [
views.liste_activites_projet, views.liste_activites_projet,
name='liste-activites-projet' name='liste-activites-projet'
), ),
# path( # path(
# 'projet/ajout-de-document/', # 'projet/ajout-de-document/',
# views.ajouter_document_projet, # views.ajouter_document_projet,
@@ -119,4 +126,6 @@ urlpatterns = [
views.mises_a_jour_projet, views.mises_a_jour_projet,
name='mises-a-jour-projet' name='mises-a-jour-projet'
) )
]
]

View File

@@ -2,6 +2,7 @@ from datetime import date
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@@ -143,6 +144,22 @@ def creation_bailleur(request):
return JsonResponse({'success': True}) return JsonResponse({'success': True})
return JsonResponse({'success': False}) return JsonResponse({'success': False})
@login_required
def liste_bailleur(request):
""" Vue pour retourner la liste de tous les bailleurs """
bailleurs = Bailleur.objects.all().order_by('-id')
data = []
for b in bailleurs:
data.append({
"id": b.id,
"nom_organisme": b.nom_organisme,
"contact": b.contact,
"email": b.email,
"pays": b.pays,
})
return JsonResponse(data, safe=False)
@login_required @login_required
def ajouter_financement_projet(request): def ajouter_financement_projet(request):
"""Ajoute un financement à un projet en vérifiant que le total ne dépasse pas 100%""" """Ajoute un financement à un projet en vérifiant que le total ne dépasse pas 100%"""

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -30,4 +30,4 @@ class Reservation(models.Model):
statut = models.CharField(choices=STATUT, default='en_attente', max_length=25) statut = models.CharField(choices=STATUT, default='en_attente', max_length=25)
def __str__(self): def __str__(self):
return f"{self.salle} - {self.employe.user.first_name} {self.employe.user.last_name} le {self.date_reservation}" return f"{self.salle} - {self.employe.user.first_name} {self.employe.user.last_name}"

View File

@@ -15,7 +15,6 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
currentReservationId = data.id_reservation; currentReservationId = data.id_reservation;
console.log(data);
$("id_reservation_detail").value = data.id_reservation; $("id_reservation_detail").value = data.id_reservation;
$("id_reservation_refus").value = data.id_reservation; $("id_reservation_refus").value = data.id_reservation;
$("id_reservation_zoom").value = data.id_reservation; $("id_reservation_zoom").value = data.id_reservation;
@@ -28,7 +27,17 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
$("motif_reservation").value=data.motif_reservation; $("motif_reservation").value=data.motif_reservation;
$("besoin_zoom").checked=data.besoin_zoom; $("besoin_zoom").checked=data.besoin_zoom;
$("besoin_ordinateur").checked=data.besoin_ordinateur; $("besoin_ordinateur").checked=data.besoin_ordinateur;
$("lien_zoom").value=data.lien_zoom; $("lien_zoom").value=data.lien_zoom;
const zoomContainer = $("lien_zoom_container");
if (!data.besoin_zoom) {
zoomContainer.classList.add("d-none");
} else {
zoomContainer.classList.remove("d-none");
}
if (data.besoin_zoom === false){
$("lien_zoom_container").className = "d-none";
}
if(data.statut !== "annulee"){ if(data.statut !== "annulee"){
$("motif_refus_container").className = "d-none"; $("motif_refus_container").className = "d-none";
@@ -167,10 +176,6 @@ tableau_reservation_attente.on("rowClick", (row, rowData) => {
$("lien_zoom_container").className = 'd-none'; $("lien_zoom_container").className = 'd-none';
} }
if(data.statut !== "refusee"){
$("motif_refus_container").className = 'd-none';
}
$("id_reservation_detail").value = data.id; $("id_reservation_detail").value = data.id;
$("id_reservation_refus").value = data.id; $("id_reservation_refus").value = data.id;
$("id_reservation_zoom").value = data.id; $("id_reservation_zoom").value = data.id;
@@ -178,16 +183,17 @@ tableau_reservation_attente.on("rowClick", (row, rowData) => {
$("employe").value=data.employe; $("employe").value=data.employe;
$("salle").value=data.salle; $("salle").value=data.salle;
$("statut-reservation").innerHTML=data.statut; $("statut-reservation").innerHTML=data.statut;
$("date_evenement").value=data.date_debut; $("date_debut").value = data.date_debut;
$("date_fin").value = data.date_fin;
$("heure_debut").value=data.heure_debut; $("heure_debut").value=data.heure_debut;
$("heure_fin").value=data.heure_fin; $("heure_fin").value=data.heure_fin;
$("motif_reservation").value=data.motif_reservation; $("motif_reservation").value=data.motif_reservation;
$("besoin_zoom").checked=data.besoin_zoom; $("besoin_zoom").checked=data.besoin_zoom;
$("besoin_ordinateur").checked=data.besoin_ordi; $("besoin_ordinateur").checked=data.besoin_ordi;
$("lien_zoom").value=data.lien_zoom; $("lien_zoom").value=data.lien_zoom;
$("motif_refus").value=data.motif_refus;
const modal = new bootstrap.Modal($("modalDetailReservation")); const modal = new bootstrap.Modal($("modalDetailReservation"));
bootstrap.Modal.getOrCreateInstance($("modalReservationAttente")).hide(); bootstrap.Modal.getOrCreateInstance($("modalReservationAttente")).hide();
modal.show(); modal.show();
}) })

View File

@@ -21,7 +21,7 @@
<input class="form-control" id="salle" readonly> <input class="form-control" id="salle" readonly>
</div> </div>
<div class="form-group mb-2"> <div class="form-group mb-2">
<label>Date de l'évènement :</label> <label>Date de debut :</label>
<input type='date' class="form-control" id="date_evenement" readonly > <input type='date' class="form-control" id="date_evenement" readonly >
</div> </div>
<div class="form-group mb-2"> <div class="form-group mb-2">
@@ -48,24 +48,19 @@
<label label="form-check-label">Besoin d'un ordinateur</label> <label label="form-check-label">Besoin d'un ordinateur</label>
<input type="checkbox" class="form-check-input" id="besoin_ordinateur" readonly > <input type="checkbox" class="form-check-input" id="besoin_ordinateur" readonly >
</div> </div>
<div class="form-group mb-2" id='motif_refus_container'>
<label>Motif de refus de la reservation :</label>
<textarea class="form-control" id="motif_refus" readonly></textarea>
</div>
<div class='d-flex justify-content-around mt-2'> <div class='d-flex justify-content-around mt-2'>
{% if appartient_au_departement_informatique %} {% if appartient_au_departement_informatique %}
<button class="btn btn-primary" id="ajoutZoom">Ajout du lien zoom</button> <button class="btn btn-primary" id="ajoutZoom">Ajout du lien zoom</button>
{% endif %} {% endif %}
{% if appartient_direction and reservation.statut == "en_attente" %} {% if appartient_direction and reservation.statut == "en_attente" %}
<button class="btn btn-danger" id="refuserReservation" data-lienrefus="{% url 'gestion_salle:refuser-reservation' %}">Refuser</button> <button class="btn btn-danger" id="refuserReservation" data-lienrefus="{% url 'gestion_salle:refuser-reservation' %}">Refuser</button>
{% endif %}
<button class="btn btn-danger" id="bouton-annuler">Annuler</button>
{% if appartient_direction %}
<button class="btn btn-success" id="bouton-valider">Valider</button> <button class="btn btn-success" id="bouton-valider">Valider</button>
{% endif %} {% endif %}
<span id="current-user-id" data-user-id="{{ request.user.id }}"></span>
<button class="btn btn-danger" id="bouton-annuler">Annuler</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,70 +1,108 @@
import json
from datetime import timedelta from datetime import timedelta
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.http import JsonResponse, HttpRequest from django.http import JsonResponse, HttpRequest
from django.forms import model_to_dict from django.forms import model_to_dict
from fonction_utilitaire import fonctions_utilitaire
from gestion_employe.models import Employe from gestion_employe.models import Employe
from gestion_salle.forms import ReservationForm from gestion_salle.forms import ReservationForm
from .models import Reservation from .models import Reservation
from datetime import timedelta
from django.contrib.auth.models import User
@login_required @login_required
def index(request:HttpRequest): def index(request: HttpRequest):
"""Vue de gestion de la reservation de la salle"""
try: try:
employe = Employe.objects.get(user=request.user) employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist: except Employe.DoesNotExist:
messages.error(request, "Impossible d'accéder au menu 'Reservation de salle' car votre profil Utilisateur n'est lié à aucun profil Employe. Veuillez contacter l'administrateur.") messages.error(request, "Profil employé introuvable.")
return redirect('gestion_conges:conge') return redirect('gestion_conges:conge')
if request.method == "POST": if request.method == "POST":
form = ReservationForm(request.POST) form = ReservationForm(request.POST)
if form.is_valid():
date_debut = form.cleaned_data.get('date_debut')
date_fin = form.cleaned_data.get('date_fin')
salle = form.cleaned_data.get('salle')
heure_debut = form.cleaned_data.get('heure_debut')
heure_fin = form.cleaned_data.get('heure_fin')
motif_reservation = form.cleaned_data.get('motif_reservation')
besoin_zoom = form.cleaned_data.get('besoin_zoom')
besoin_ordi = form.cleaned_data.get('besoin_ordi')
while date_debut <= date_fin :
reservation = Reservation(
employe = employe,
date_debut = date_debut,
date_fin = date_debut,
salle = salle,
heure_debut = heure_debut,
heure_fin = heure_fin,
besoin_zoom = besoin_zoom,
besoin_ordi = besoin_ordi,
motif_reservation=motif_reservation,
)
reservation.save()
date_debut = date_debut + timedelta(days=1)
messages.success(request, "Réservation(s) créées avec succès.") if form.is_valid():
date_debut = form.cleaned_data['date_debut']
date_fin = form.cleaned_data['date_fin']
salle = form.cleaned_data['salle']
heure_debut = form.cleaned_data['heure_debut']
heure_fin = form.cleaned_data['heure_fin']
motif = form.cleaned_data['motif_reservation']
besoin_zoom = form.cleaned_data['besoin_zoom']
besoin_ordi = form.cleaned_data['besoin_ordi']
if date_fin < date_debut:
messages.error(request, "Date fin invalide.")
return redirect('gestion_salle:reservation-salle')
if heure_fin <= heure_debut:
messages.error(request, "Heure invalide.")
return redirect('gestion_salle:reservation-salle')
if not request.user.first_name.strip() or not request.user.last_name.strip():
messages.error(
request,
"Veuillez renseigner votre nom et prénom pour pouvoir faire une réservation."
)
return redirect('gestion_salle:reservation-salle')
created = []
current_date = date_debut
while current_date <= date_fin:
reservation = Reservation.objects.create(
employe=employe,
date_debut=current_date,
date_fin=current_date,
salle=salle,
heure_debut=heure_debut,
heure_fin=heure_fin,
besoin_zoom=besoin_zoom,
besoin_ordi=besoin_ordi,
motif_reservation=motif,
statut="en_attente"
)
created.append(reservation)
current_date += timedelta(days=1)
messages.success(request, "Réservation(s) créée(s) avec succès.")
if fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION:
fonctions_utilitaire.envoyer_mail(
sujet = "Reservation de salle",
message = f"""
Une nouvelle demande de réservation de la {dict(Reservation.TYPE_CHOICES).get(salle)} a été effectuée par {employe.user.first_name} {employe.user.last_name} du {form.cleaned_data.get('date_debut').strftime('%d/%m/%Y')} au {form.cleaned_data.get('date_fin').strftime('%d/%m/%Y')} pour motif "{motif}".
Veuillez vous connecter à la plateforme pour plus de détails.""",
destinataires = list(fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION)
)
return redirect('gestion_salle:reservation-salle') return redirect('gestion_salle:reservation-salle')
formulaire_reservation = ReservationForm() formulaire_reservation = ReservationForm()
departement = Employe.objects.get(user__username=request.user).departement departement = employe.departement
appartient_direction = 'direction' in request.user.groups.values_list('name', flat=True)
liste_demande_reservation = [ appartient_direction = request.user.groups.filter(name='direction').exists()
reservation.id for reservation in
Reservation.objects.filter(employe=employe, statut='en_attente') liste_demande_reservation = Reservation.objects.filter(
] employe=employe,
statut='en_attente'
).values_list('id', flat=True)
context = { context = {
'formulaire_reservation': formulaire_reservation, 'formulaire_reservation': formulaire_reservation,
'nb_reservation_attente': Reservation.objects.filter(statut='en_attente').count(), 'nb_reservation_attente': Reservation.objects.filter(statut='en_attente').count(),
'appartient_au_departement_informatique': 'Informatique' == departement.nom if departement else False, 'appartient_au_departement_informatique': departement and departement.nom == "Systeme informatique",
'appartient_direction': appartient_direction, 'appartient_direction': appartient_direction,
'liste_demande_reservation': liste_demande_reservation 'liste_demande_reservation': list(liste_demande_reservation),
} }
return render(request, "gestion_salle/index.html", context)
return render(request, "gestion_salle/index.html", context)
def liste_reservation(request:HttpRequest): def liste_reservation(request:HttpRequest):
"""Vue d'affichage des creneaux disponibles""" """Vue d'affichage des creneaux disponibles"""
reservations = Reservation.objects.filter(statut = "validee") reservations = Reservation.objects.filter(statut = "validee")
@@ -86,7 +124,6 @@ def liste_reservation(request:HttpRequest):
"end": reservation.heure_fin, "end": reservation.heure_fin,
"color": color, "color": color,
}) })
return JsonResponse(liste_reservation, safe=False) return JsonResponse(liste_reservation, safe=False)
@login_required @login_required
@@ -103,13 +140,16 @@ def liste_reservation_attente(request):
return JsonResponse(liste_reservation, safe=False) return JsonResponse(liste_reservation, safe=False)
def detail_reservation(request:HttpRequest, reservation_id:int): def detail_reservation(request:HttpRequest, reservation_id:int):
reservation = Reservation.objects.get(id=reservation_id) reservation = Reservation.objects.get(id=reservation_id)
employe = reservation.employe.user employe = reservation.employe.user
reservation_json = { reservation_json = {
'id_reservation': reservation_id, 'id_reservation': reservation_id,
'employe': f"{employe.first_name} {employe.last_name}", 'employe': f"{employe.first_name} {employe.last_name}",
'salle': reservation.salle, 'salle': reservation.salle,
'statut': reservation.statut, 'statut': dict(Reservation.STATUT).get(reservation.statut),
'date_evenement': reservation.date_debut.strftime('%Y-%m-%d'), 'date_evenement': reservation.date_debut.strftime('%Y-%m-%d'),
'heure_debut': reservation.heure_debut.strftime('%H:%M'), 'heure_debut': reservation.heure_debut.strftime('%H:%M'),
'heure_fin': reservation.heure_fin.strftime('%H:%M'), 'heure_fin': reservation.heure_fin.strftime('%H:%M'),
@@ -117,9 +157,7 @@ def detail_reservation(request:HttpRequest, reservation_id:int):
'besoin_zoom': reservation.besoin_zoom, 'besoin_zoom': reservation.besoin_zoom,
'besoin_ordinateur': reservation.besoin_ordi, 'besoin_ordinateur': reservation.besoin_ordi,
'lien_zoom': reservation.lien_zoom or '', 'lien_zoom': reservation.lien_zoom or '',
'motif_refus': reservation.motif_refus or '',
} }
return JsonResponse(reservation_json, safe=True) return JsonResponse(reservation_json, safe=True)
@login_required @login_required
@@ -165,31 +203,49 @@ def annuler_reservation(request:HttpRequest):
return redirect('gestion_salle:reservation-salle') return redirect('gestion_salle:reservation-salle')
@login_required @login_required
def valider_reservation(request:HttpRequest): def valider_reservation(request: HttpRequest):
"""Vue de gestion de l'annulation de la reservation""" """Validation d'une réservation"""
if request.method == 'POST': if request.method == 'POST':
reservation_id= request.POST['id_reservation'] reservation_id = request.POST.get('id_reservation')
try: try:
reservation = Reservation.objects.get(id=reservation_id) reservation = Reservation.objects.get(id=reservation_id)
except reservation.DoesNotExist: except Reservation.DoesNotExist:
messages.error(request, "La resevertion selectionné n'existe pas.") messages.error(request, "La réservation sélectionnée n'existe pas.")
return redirect("salle") return redirect("salle")
reservation.statut = 'validee' reservation.statut = 'validee'
reservation.save() reservation.save()
if fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION:
fonctions_utilitaire.envoyer_mail(
sujet = "Reservation de salle",
message = f"""Bonjour {request.user.first_name} {request.user.last_name}, votre reservation de la salle {dict(Reservation.TYPE_CHOICES).get(reservation.salle)} du {reservation.date_debut.strftime('%d/%m/%Y')} au {reservation.date_fin.strftime('%d/%m/%Y')} pour motif "{reservation.motif_reservation}" a été validée. Veuillez vous connecter à la plateforme pour plus de détails.""",
destinataires = [reservation.employe.user.email]
)
messages.success(request, f"Réservation de {reservation.employe.get_full_name()} validée avec succès.") messages.success(request, f"Réservation de {reservation.employe.get_full_name()} validée avec succès.")
return redirect('gestion_salle:reservation-salle') return redirect('gestion_salle:reservation-salle')
@login_required @login_required
def refuser_reservation(request:HttpRequest): def refuser_reservation(request: HttpRequest):
"""Vue de gestion de refus de la reservation""" """Refuser une réservation"""
data = json.loads(request.body) if request.method == 'POST':
reservation_id = data.get("id_reservation") reservation_id = request.POST.get('id_reservation')
try:
reservation = Reservation.objects.get(id=reservation_id) try:
except Reservation.DoesNotExist and ValueError: reservation = Reservation.objects.get(id=reservation_id)
return JsonResponse({"message": "La resevertion selectionné n'existe pas."}) except Reservation.DoesNotExist:
else: messages.error(request, "La réservation n'existe pas.")
reservation.statut = "refusee" return redirect("salle")
reservation.statut = 'refusee'
reservation.save() reservation.save()
return JsonResponse({"message": "Réservation refusée avec succès."})
if fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION:
fonctions_utilitaire.envoyer_mail(
sujet = "Reservation de salle",
message = f"""Bonjour {request.user.first_name} {request.user.last_name}, votre reservation de la salle {dict(Reservation.TYPE_CHOICES).get(reservation.salle)} du {reservation.date_debut.strftime('%d/%m/%Y')} au {reservation.date_fin.strftime('%d/%m/%Y')} pour motif "{reservation.motif_reservation}" a été refusée. Veuillez vous connecter à l'Assistante de Direction pour plus de détails.""",
destinataires = [reservation.employe.user.email]
)
return JsonResponse({"message": "Réservation refusée avec succès."})

301
notification/services.py Normal file
View File

@@ -0,0 +1,301 @@
from django.core.mail import send_mail
from django.conf import settings
def send_notification_email(user, sujet, message):
"""
Fonction générique pour envoyer un email simple
"""
if not user.email:
return
send_mail(
sujet,
message,
settings.DEFAULT_FROM_EMAIL,
[user.email],
fail_silently=False,
)
def email_reservation_creee(reservation):
user = reservation.employe.user
message = f"""
Bonjour {user.get_full_name()},
Votre réservation a été enregistrée avec succès.
Salle : {reservation.salle}
Date : {reservation.date_debut}
Heure : {reservation.heure_debut} - {reservation.heure_fin}
Motif : {reservation.motif_reservation}
Merci.
"""
send_notification_email(user, "Confirmation de votre réservation", message)
def email_reservation_directeur(reservation, directeur):
message = f"""
Bonjour {directeur.get_full_name()},
Une nouvelle réservation attend votre validation.
Employé : {reservation.employe.user.get_full_name()}
Salle : {reservation.salle}
Date : Du {reservation.date_debut} au {reservation.date_fin}
Heure : {reservation.heure_debut} - {reservation.heure_fin}
Motif : {reservation.motif_reservation}
Connectez-vous à la plateforme pour valider.
https://support.cerfig.org/login/
Cordialement,
SIRH
"""
send_notification_email(directeur, "Nouvelle réservation à valider", message)
def email_reservation_zoom(reservation, admin):
message = f"""
Bonjour {admin.get_full_name()},
Une demande de lien Zoom a été faite.
Employé : {reservation.employe.user.get_full_name()}
Date : {reservation.date_debut} au {reservation.date_fin}
Heure : {reservation.heure_debut} - {reservation.heure_fin}
Motif : {reservation.motif_reservation}
Veuillez créer le lien Zoom.
https://support.cerfig.org/login/
Cordialement,
SIRH
"""
send_notification_email(admin, "Création lien Zoom requise", message)
def email_statut_reservation(reservation):
user = reservation.employe.user
message = f"""
Bonjour {user.get_full_name()},
Le statut de votre réservation a été mis à jour.
Salle : {reservation.salle}
Date : {reservation.date_debut}
Statut : {reservation.statut}
Merci de vous connecter
https://support.cerfig.org
Cordialement,
SIRH
"""
send_notification_email(user, "Mise à jour de votre réservation", message)
def email_reservation_validee(reservation):
user = reservation.employe.user
message = f"""
Bonjour {user.get_full_name()},
Votre réservation a été VALIDÉE.
Salle : {reservation.salle}
Date : {reservation.date_debut}
https://support.cerfig.org/login/
Cordialement,
SIRH
"""
send_notification_email(user, "Réservation validée", message)
def email_reservation_refusee(reservation):
user = reservation.employe.user
message = f"""
Bonjour {user.get_full_name()},
Votre réservation a été REFUSÉE.
Salle : {reservation.salle}
Date : {reservation.date_debut}
https://support.cerfig.org/login/
Cordialement,
SIRH
"""
send_notification_email(user, "Réservation refusée", message)
def email_contrat_cree(contrat):
user = contrat.employe.user
message = f"""
Bonjour {user.get_full_name()},
Un nouveau contrat a été créé pour vous.
Date début : {contrat.date_debut}
Date fin : {contrat.date_fin}
Bienvenue !
Merci de vous connecter pour plus informations sur votre contrat
https://support.cerfig.org
Cordialement,
SIRH
"""
send_notification_email(user, "Nouveau contrat", message)
def email_statut_contrat(contrat):
user = contrat.employe.user
message = f"""
Bonjour {user.get_full_name()},
Votre contrat a été modifier par les ressource humaine .
veuillez vous connecter pour voir les nouvelles informations sur votre contrat
https://support.cerfig.org/
Cordialement,
SIRH
"""
send_notification_email(user, "Mise à jour du contrat", message)
def email_affectation_projet(affectation):
user = affectation.employe.user
projet = affectation.projet
message = f"""
Bonjour {user.get_full_name()},
Votre affectation au projet a été mise à jour.
Projet : {projet.nom}
Description : {projet.description}
Date début : {projet.date_debut}
Date fin : {projet.date_fin}
Rôle : {affectation.role}
Cordialement,
SIRH
"""
send_notification_email(user, "Affectation projet", message)
def email_expiration_fichier(projet, fichier, chef_projet):
message = f"""
Bonjour {chef_projet.get_full_name()},
Un fichier est proche de son expiration.
Projet : {projet.nom}
Fichier : {fichier.nom}
Date début : {fichier.date_debut}
Date fin : {fichier.date_fin}
https://support.cerfig.org/
Cordialement,
SIRH
"""
send_notification_email(chef_projet, "Fichier proche expiration", message)
def email_conge_cree(conge):
user = conge.employe.user
message = f"""
Bonjour {user.get_full_name()},
Votre demande de congé a été enregistrée avec succès.
Type de congé : {conge.type_conge}
Date début : {conge.date_debut}
Date fin : {conge.date_fin}
Motif : {conge.motif}
Votre demande est en attente de validation.
https://support.cerfig.org/
Cordialement,
SIRH
"""
send_notification_email(user, "Demande de congé enregistrée", message)
def email_conge_responsable(conge, responsable):
message = f"""
Bonjour {responsable.get_full_name()},
Une demande de congé nécessite votre validation.
Employé : {conge.employe.user.get_full_name()}
Type : {conge.type_conge}
Date : {conge.date_debut} au {conge.date_fin}
Motif : {conge.motif}
Merci de valider ou refuser.
https://support.cerfig.org/
Cordialement,
SIRH
"""
send_notification_email(responsable, "Validation congé (Niveau 1)", message)
def email_conge_directeur(conge, directeur):
message = f"""
Bonjour {directeur.get_full_name()},
Une demande de congé a été validée par le responsable et attend votre validation finale.
Employé : {conge.employe.user.get_full_name()}
Type : {conge.type_conge}
Date : {conge.date_debut} au {conge.date_fin}
Merci de valider définitivement.
https://support.cerfig.org/login/
Cordialement,
SIRH
"""
send_notification_email(directeur, "Validation congé (Niveau 2)", message)
def email_conge_valide(conge):
user = conge.employe.user
message = f"""
Bonjour {user.get_full_name()},
Votre demande de congé a été VALIDÉE.
Type : {conge.type_conge}
Date : {conge.date_debut} au {conge.date_fin}
Bon repos a vous
https://support.cerfig.org
Cordialement,
SIRH
"""
send_notification_email(user, "Congé validé", message)
def email_conge_refuse(conge):
user = conge.employe.user
message = f"""
Bonjour {user.get_full_name()},
Votre demande de congé a été REFUSÉE.
Type : {conge.type_conge}
Date : {conge.date_debut} au {conge.date_fin}
Motif : {conge.motif_refus or "Non précisé"}
https://support.cerfig.org
Cordialement,
SIRH
"""
send_notification_email(user, "Congé refusé", message)