5 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
69 changed files with 591 additions and 292 deletions

2
Jenkinsfile vendored
View File

@@ -39,8 +39,6 @@ pipeline
python manage.py makemigrations python manage.py makemigrations
python manage.py migrate python manage.py migrate
echo $SUDO_PASSWORD | sudo -S rm -r staticfiles
python manage.py collectstatic --noinput
echo $SUDO_PASSWORD | sudo -S chown -R www-data:www-data /var/www/sirh echo $SUDO_PASSWORD | sudo -S chown -R www-data:www-data /var/www/sirh
echo "Deploiement reussi" echo "Deploiement reussi"

View File

@@ -24,10 +24,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = config('SECRET_KEY') SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=True, cast=bool) DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[]).split(',') ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[]).split(',')
CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", default=[]).split(",")
# Application definition # Application definition
@@ -41,9 +40,12 @@ INSTALLED_APPS = [
'gestion_employe', 'gestion_employe',
'gestion_conge', 'gestion_conge',
'gestion_projet', 'gestion_projet',
'gestion_salle' 'gestion_salle',
'simple_sso.sso_server'
] ]
LOGIN_URL = 'login'
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@@ -78,14 +80,22 @@ WSGI_APPLICATION = 'SIRH.wsgi.application'
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases # https://docs.djangoproject.com/en/5.2/ref/settings/#databases
if config('ENVIRONMENT') == 'local':
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
else:
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
'NAME': config("DB_NAME"), 'NAME': config('DATABASE_NAME'),
'USER': config("DB_USER"), 'USER': config('DATABASE_USER'),
'PASSWORD': config("DB_PASSWORD"), 'PASSWORD': config('DATABASE_PASSWORD'),
'HOST': config("DB_HOST"), 'HOST': config('DATABASE_HOST'),
'PORT': config("DB_PORT", cast=int), 'PORT': config('DATABASE_PORT'),
} }
} }
@@ -140,21 +150,12 @@ MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Connexion sso
# LOGIN_URL = '/client/'
LOGIN_URL = 'simple-sso-login'
SSO_PRIVATE_KEY = 'D2brFY3LnNDLQBBnvYPXAn185Wi9EpPvnHsUosjEZ9vZMyEtXqkcVfFWkVbN0xpG'
SSO_PUBLIC_KEY = 'm9JDP3uGPmGG9ULqPw2BY3ryBNMTGRsuukgIOhk7lIN9lT0wlr2zhoRZiNZ28UCM'
SSO_SERVER = 'http://test-authentification.cerfig.org:89/server/'
#SSO_LOGOUT_URL = 'http://test-authentification.cerfig.org:89/deconnexion'
# Configuration de l'email # 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
EMAIL_USE_SSL = True EMAIL_USE_SSL = True
EMAIL_USE_TLS = False EMAIL_USE_TLS = False
EMAIL_HOST_USER = config("EMAIL_HOST_USER") EMAIL_HOST_USER = 'support.it@cerfig.org'
EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD") EMAIL_HOST_PASSWORD = 'Cerfig2025'
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

View File

@@ -1,33 +0,0 @@
# SGL/sso.py
from gestion_employe.models import Employe, Departement
from simple_sso.sso_client.client import Client
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
User = get_user_model()
class SIRHClient(Client):
def build_user(self, user_data):
"""Crée le User et son Profile avec les infos reçues du serveur."""
user_data_copy = user_data.copy()
user_data.pop("departement")
user_data.pop("chef_departement")
user_data.pop("groups")
user = super().build_user(user_data)
departement = Departement.objects.get_or_create(
nom=user_data_copy.get("departement", ""))[0]
Employe.objects.update_or_create(
user = user,
defaults = {
"departement": departement,
"chef": user_data_copy.get("chef_departement", ""),
}
)
for group_name in user_data_copy.get("groups", []):
group, _ = Group.objects.get_or_create(name=group_name)
user.groups.add(group)
return user

View File

@@ -15,9 +15,8 @@
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<div class="row flex-nowrap"> <div class="row flex-nowrap">
{% include 'parts/menu_application.html' %}
{% include 'parts/menu_principal.html' %} {% include 'parts/menu_principal.html' %}
<div class="col p-4"> <div class="col-9 p-4">
{% block 'contenu' %} {% endblock %} {% block 'contenu' %} {% endblock %}
</div> </div>
</div> </div>

View File

@@ -24,7 +24,6 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
<div class="mb-3"> <div class="mb-3">
<label for="mail">Votre adresse email :</label> <label for="mail">Votre adresse email :</label>
<input type="text" name="mail" class="form-control" placeholder="Entrez votre e-mail" required> <input type="text" name="mail" class="form-control" placeholder="Entrez votre e-mail" required>

View File

@@ -1,8 +0,0 @@
<div class="col-1 bg-danger border d-flex flex-column vh-100 pt-5 sticky-top">
<a href="http://test-sirh.cerfig.org:89/" class="text-white text-center fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-people-fill fs-1 d-block"></i>SIRH
</a>
<a href="http://test-sgl.cerfig.org:89/" class="text-white text-center fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-building-gear fs-1 d-block"></i> SGL
</a>
</div>

View File

@@ -13,36 +13,35 @@
<i class="bi bi-person-circle text-white" style="font-size:60px;"></i> <i class="bi bi-person-circle text-white" style="font-size:60px;"></i>
{% endif %} {% endif %}
<div class="text-white mt-2 fw-bold fs-5"> <div class="text-white mt-2 fw-bold fs-5">
{{ user.first_name }} {{ user.last_name }} {{ user.username }}
</div> </div>
</div> </div>
<a href="{% url 'gestion_employe:mon-profil' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;"> <a href="{% url 'gestion_employe:mon-profil' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-person-circle"></i> Mon profil <i class="bi bi-person-circle"></i> Mon profil
</a> </a>
{% if user|has_group:"ressource_humaine" or user|has_group:"direction" %} {% if user|has_group:"ressource_humaine" or user|has_group:"direction" %}
<a href="{% url 'gestion_employe:index' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;"> <a href="{% url 'gestion_employe:index' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-speedometer2"></i> Tableau de bord <i class="bi bi-speedometer2"></i> Tableau de bord
</a> </a>
{% endif %} {% endif %}
{% if user|has_group:"ressource_humaine" or user|has_group:"direction" %} {% if user|has_group:"ressource_humaine" or user|has_group:"direction" %}
<a href="{% url 'gestion_projet:index' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;"> <a href="{% url 'gestion_projet:index' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-folder"></i> Gestion des Projets <i class="bi bi-folder"></i> Gestion des Projets
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'gestion_conges:conge' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;"> <a href="{% url 'gestion_conges:conge' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-airplane"></i> Gestion des congés <i class="bi bi-airplane"></i> Gestion des congés
</a> </a>
{% if user|is_chef_projet %} {% if user|is_chef_projet %}
<a href="{% url 'gestion_projet:activites-projet' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;"> <a href="{% url 'gestion_projet:activites-projet' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-list-task"></i> Suivi des Activités <i class="bi bi-list-task"></i> Suivi des Activités
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'gestion_salle:reservation-salle' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;"> <a href="{% url 'gestion_salle:reservation-salle' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-calendar-check"></i> Réservation <i class="bi bi-calendar-check"></i> Réservation
</a> </a>
{% comment %} {% url 'deconnexion' %} {% endcomment %} <a href="{% url 'deconnexion' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<a href="http://test-authentification.cerfig.org:89/deconnexion" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;">
<i class="bi bi-box-arrow-right"></i> Déconnexion <i class="bi bi-box-arrow-right"></i> Déconnexion
</a> </a>
</div> </div>

View File

@@ -16,29 +16,32 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
# from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf import settings from django.conf import settings
# from simple_sso.sso_server.server import Server
from .sso import SIRHClient from . import views
client_sirh = SIRHClient( # server_sso = Server()
settings.SSO_SERVER,
settings.SSO_PUBLIC_KEY,
settings.SSO_PRIVATE_KEY
)
urlpatterns = [ urlpatterns = [
# path('login/',
# views.login_view,
# name='login'
# ),
# path(
# 'deconnexion/',
# views.deconnexion_view,
# name='deconnexion'
# ),
path( path(
'', '',
views.login_view,
name='index'
),
path('login/',
views.login_view,
name='login'
),
path(
'deconnexion/',
views.deconnexion_view,
name='deconnexion'
),
path(
'employé/',
include("gestion_employe.urls") include("gestion_employe.urls")
), ),
path( path(
@@ -57,6 +60,8 @@ urlpatterns = [
'admin/', 'admin/',
admin.site.urls admin.site.urls
), ),
path("client/", include(client_sirh.get_urls())), # path(
] # 'sso',
# + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # include(server_sso.get_urls())
# )
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -0,0 +1,37 @@
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import render, redirect
from django.contrib import messages
def login_view(request):
"""
Gère la connexion des utilisateurs avec redirection selon le rôle et
vérification de l'acceptation de la politique d'utilisation.
"""
if request.method == 'POST':
email = request.POST.get('mail')
password = request.POST.get('mot_de_passe')
if not (email and password):
messages.error(request, "Veuillez remplir tous les champs.")
return render(request, 'login.html')
user = authenticate(request, username=email, password=password)
if user is None:
messages.error(request, "Nom dutilisateur ou mot de passe incorrect.")
return render(request, 'login.html')
if not user.is_active:
messages.error(request, "Compte inactif. Contactez l'administrateur.")
return render(request, 'login.html')
login(request, user)
return redirect("gestion_conges:conge")
return render(request, 'login.html')
def deconnexion_view(request):
"""Gère la déconnexion de l'utilisateur."""
logout(request)
return redirect('login')

View File

@@ -7,7 +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)) 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é"""

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.13 on 2026-06-16 11:06 # Generated by Django 5.2.13 on 2026-04-17 12:03
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View File

@@ -3,7 +3,7 @@ const $ = (element) => document.getElementById(element);
const url_liste_conge_attente = $("liste-demande-conges").dataset.url const url_liste_conge_attente = $("liste-demande-conges").dataset.url
const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", { const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", {
// layout : "fitColumns", layout : "fitColumns",
columns: [ columns: [
{"title": "Nom et Prénom", "field": "prenom_nom"}, {"title": "Nom et Prénom", "field": "prenom_nom"},
{"title": "Date de début", "field": "date_debut", formatter:"datetime", formatterParams:{ {"title": "Date de début", "field": "date_debut", formatter:"datetime", formatterParams:{
@@ -15,7 +15,7 @@ const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", {
outputFormat:"dd/MM/yy", outputFormat:"dd/MM/yy",
}}, }},
{"title": "Type de congé", "field": "type"}, {"title": "Type de congé", "field": "type"},
{"title": "Date de la demande", "field": "date_demande", responsive: 1}, {"title": "Date de la demande", "field": "date_demande"},
{"title": "Validation par supérieur hiérarchique", "field": "validation_hierarchique", formatter:"tickCross", formatterParams :{ {"title": "Validation par supérieur hiérarchique", "field": "validation_hierarchique", formatter:"tickCross", formatterParams :{
allowEmpty : true , allowEmpty : true ,
}}, }},
@@ -24,7 +24,7 @@ const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", {
}}, }},
], ],
pagination: true, pagination: true,
paginationSize: 5, paginationSize: 5
}) })
const bouton_demande_conges = $("bouton-demande-conge"); const bouton_demande_conges = $("bouton-demande-conge");

View File

@@ -7,7 +7,6 @@
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %}">{{message}}</div> <div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %}">{{message}}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<div class="row d-flex justify-content-center mb-4"> <div class="row d-flex justify-content-center mb-4">
<div class="col text-white bg-danger d-flex flex-column justify-content-center align-items-center border rounded p-4"> <div class="col text-white bg-danger d-flex flex-column justify-content-center align-items-center border rounded p-4">
<div class="card-header fw-bold"> <div class="card-header fw-bold">
@@ -33,17 +32,11 @@
<h5 class="fw-bold">{{ nombre_conges_valide }}</h5> <h5 class="fw-bold">{{ nombre_conges_valide }}</h5>
</div> </div>
</div> </div>
</div> <div class="d-flex justify-content-between my-4">
<div class="row mb-3">
<div class="col d-flex justify-content-between align-items-center">
<h3><i class="bi bi-list-ul"></i> Liste des demandes de congé</h3> <h3><i class="bi bi-list-ul"></i> Liste des demandes de congé</h3>
<button class='btn btn-primary' id="bouton-demande-conge">Demande de congé</button> <button class='btn btn-primary' id="bouton-demande-conge">Demande de congé</button>
</div> </div>
</div> <div class="table-responsive">
<div class="row">
<div class="col">
<div id="liste-demande-conges" data-url="{% url 'gestion_conges:liste-des-conges' %}"></div> <div id="liste-demande-conges" data-url="{% url 'gestion_conges:liste-des-conges' %}"></div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.13 on 2026-06-16 11:06 # Generated by Django 5.2.13 on 2026-04-17 12:03
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('matricule', models.CharField(blank=True, max_length=10, null=True, unique=True)), ('matricule', models.CharField(blank=True, max_length=10, null=True, unique=True)),
('fonction', models.CharField(blank=True, choices=[('directeur', 'Directeur'), ('assistant_direction', 'Assistante de direction'), ('assistant_technique_recherche', 'Assistant technique de recherche'), ('comptable', 'Comptable'), ('raf', 'RAF'), ('data_manager', 'Data Manager'), ('logisticien', 'Logisticien'), ('post_doctorant', 'Post-Doctorant'), ('qualiticien', 'Qualiticien'), ('technicien_surface', 'Technicien de surface'), ('chauffeur', 'Chauffeur')], max_length=50, null=True)), ('fonction', models.CharField(blank=True, choices=[('directeur', 'Directeur'), ('assistant_direction', 'Assistante de direction'), ('comptable', 'Comptable'), ('raf', 'RAF'), ('data_manager', 'Data Manager'), ('logisticien', 'Logisticien'), ('post_doctorant', 'Post-Doctorant'), ('qualiticien', 'Qualiticien'), ('technicien_surface', 'Technicien de surface'), ('chauffeur', 'Chauffeur')], max_length=50, null=True)),
('date_embauche', models.DateField(blank=True, null=True)), ('date_embauche', models.DateField(blank=True, null=True)),
('adresse', models.CharField(blank=True, max_length=100, null=True)), ('adresse', models.CharField(blank=True, max_length=100, null=True)),
('telephone', models.CharField(blank=True, max_length=15, null=True)), ('telephone', models.CharField(blank=True, max_length=15, null=True)),
@@ -48,7 +48,7 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('numero_contrat', models.CharField(max_length=100, unique=True)), ('numero_contrat', models.CharField(max_length=100, unique=True)),
('type_contrat', models.CharField(choices=[('contrat_duree_determinee', 'Contrat à Durée Déterminée'), ('contrat_duree_indeterminee', 'Contrat à Durée Indéterminée'), ('contrat_prestation', 'Contrat de Prestation de Service'), ('contrat_stage', 'Contrat de Stage'), ('convention_bourse_entretien', "Convention de bourse d'entretien")], max_length=50)), ('type_contrat', models.CharField(choices=[('contrat_duree_determinee', 'Contrat à Durée Déterminée'), ('contrat_duree_indeterminee', 'Contrat à Durée Indéterminée'), ('contrat_prestation', 'Contrat de Prestation de Service'), ('contrat_stage', 'Contrat de Stage')], max_length=50)),
('date_debut', models.DateField()), ('date_debut', models.DateField()),
('date_fin', models.DateField(blank=True, null=True)), ('date_fin', models.DateField(blank=True, null=True)),
('salaire_mensuel', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), ('salaire_mensuel', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.4 on 2026-04-29 10:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestion_employe', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='contrat',
name='type_contrat',
field=models.CharField(choices=[('contrat_duree_determinee', 'Contrat à Durée Déterminée'), ('contrat_duree_indeterminee', 'Contrat à Durée Indéterminée'), ('contrat_prestation', 'Contrat de Prestation de Service'), ('contrat_stage', 'Contrat de Stage'), ('convention_bourse_entretien', "Convention de bourse d'entretien")], max_length=50),
),
]

View File

@@ -12,7 +12,6 @@ const tableau_liste_employe = new Tabulator("#tableau_liste_employe", {
// ajaxURL: url_liste_employe, // ajaxURL: url_liste_employe,
pagination: true, pagination: true,
paginationSize: 10, paginationSize: 10,
responsiveLayout : "hide"
}) })
fetch(url_liste_employe) fetch(url_liste_employe)

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

@@ -129,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),
@@ -158,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"""
@@ -261,6 +277,7 @@ def creation_contrat(request):
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( contrat_actif = Contrat.objects.filter(
employe=employe, employe=employe,
date_fin__gte=date.today() date_fin__gte=date.today()

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.13 on 2026-06-16 11:06 # Generated by Django 5.2.13 on 2026-04-17 12:03
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View File

@@ -23,7 +23,6 @@ btnEnregistrerBailleur.addEventListener('click', function() {
}); });
}); });
document.addEventListener("DOMContentLoaded", function () {
table = new Tabulator("#table-bailleurs", { table = new Tabulator("#table-bailleurs", {
ajaxURL: "/gestion-projet/bailleurs/", ajaxURL: "/gestion-projet/bailleurs/",
@@ -46,6 +45,3 @@ document.addEventListener("DOMContentLoaded", function () {
} }
} }
}); });
});

View File

@@ -48,4 +48,3 @@
</div> </div>
</div> </div>
</div> </div>

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
@@ -334,7 +335,6 @@ def activites_projet(request):
} }
return render(request, 'gestion_projet/suivi_activite.html', context) return render(request, 'gestion_projet/suivi_activite.html', context)
@login_required @login_required
def ajouter_activite_projet(request): def ajouter_activite_projet(request):
"""Vue pour ajouter une activité à un projet spécifique via un formulaire""" """Vue pour ajouter une activité à un projet spécifique via un formulaire"""

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.13 on 2026-06-16 11:06 # Generated by Django 5.2.13 on 2026-04-17 12:03
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View File

@@ -29,10 +29,21 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
$("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){ if (data.besoin_zoom === false){
$("lien_zoom_container").className = "d-none"; $("lien_zoom_container").className = "d-none";
} }
if(data.statut !== "annulee"){
$("motif_refus_container").className = "d-none";
}else{
$("motif_refus").value=data.motif_refus;
}
}) })
} }
}); });
@@ -78,11 +89,6 @@ $("bouton-annuler").addEventListener("click", (e) => {
const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value; const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
const url_annuler = $("formulaire-details").dataset.urlannuler; const url_annuler = $("formulaire-details").dataset.urlannuler;
console.log("URL d'annulation :");
console.log($("formulaire-details"))
e.preventDefault();
fetch( fetch(
url_annuler, url_annuler,
{ {
@@ -133,9 +139,6 @@ if($("refuserReservation")){
const url = e.currentTarget.dataset.lienrefus; const url = e.currentTarget.dataset.lienrefus;
const idRes = $("id_reservation_detail").value; const idRes = $("id_reservation_detail").value;
console.log(idRes);
e.preventDefault();
fetch(url, { fetch(url, {
method: "POST", method: "POST",
headers: { headers: {
@@ -167,11 +170,12 @@ const tableau_reservation_attente = new Tabulator("#tableau-reservation-attente"
tableau_reservation_attente.on("rowClick", (row, rowData) => { tableau_reservation_attente.on("rowClick", (row, rowData) => {
const data = rowData.getData(); const data = rowData.getData();
console.log(data);
if(data.besoin_zoom === false){ if(data.besoin_zoom === false){
$("lien_zoom_container").className = 'd-none'; $("lien_zoom_container").className = 'd-none';
} }
const id_user = $("current-user-id").dataset.userid;
$("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;
@@ -181,6 +185,7 @@ tableau_reservation_attente.on("rowClick", (row, rowData) => {
$("statut-reservation").innerHTML=data.statut; $("statut-reservation").innerHTML=data.statut;
$("date_debut").value = data.date_debut; $("date_debut").value = data.date_debut;
$("date_fin").value = data.date_fin; $("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;
@@ -188,10 +193,6 @@ tableau_reservation_attente.on("rowClick", (row, rowData) => {
$("besoin_ordinateur").checked=data.besoin_ordi; $("besoin_ordinateur").checked=data.besoin_ordi;
$("lien_zoom").value=data.lien_zoom; $("lien_zoom").value=data.lien_zoom;
if (id_user != data.employe_id){
$("bouton-annuler").className = "d-none";
}
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

@@ -26,9 +26,9 @@
<div class="form-group col-5 me-2"> <div class="form-group col-5 me-2">
<label>Selectionner une salle :</label> <label>Selectionner une salle :</label>
<select class = "form-select" id="liste-salle"> <select class = "form-select" id="liste-salle">
<option value='Salle de formation'>Salle de formation</option> <option value='formation'>Salle de formation</option>
<option value='Salle de réunion'>Salle de réunion</option> <option value='reunion'>Salle de réunion</option>
<option value='Lien Zoom'>Lien Zoom</option> <option value='lien_zoom'>Lien Zoom</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,4 @@
<!-- Modal d'affichage des détails d'une reservation --> <!-- Modal d'affichage des détails d'une reservation -->
{% load tags_personnaliser %}
<div class="modal fade" id="modalDetailReservation" tabindex="-1" aria-labelledby="modalDetailReservationLabel" aria-hidden="true"> <div class="modal fade" id="modalDetailReservation" tabindex="-1" aria-labelledby="modalDetailReservationLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@@ -23,11 +22,7 @@
</div> </div>
<div class="form-group mb-2"> <div class="form-group mb-2">
<label>Date de debut :</label> <label>Date de debut :</label>
<input type='date' class="form-control" id="date_debut" readonly > <input type='date' class="form-control" id="date_evenement" readonly >
</div>
<div class="form-group mb-2">
<label>Date de fin :</label>
<input type='date' class="form-control" id="date_fin" readonly >
</div> </div>
<div class="form-group mb-2"> <div class="form-group mb-2">
<label>Heure de début :</label> <label>Heure de début :</label>
@@ -57,11 +52,11 @@
{% 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 user|has_group:'direction' %} {% 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>
<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-userid="{{ request.user.id }}"></span> <span id="current-user-id" data-user-id="{{ request.user.id }}"></span>
<button class="btn btn-danger" id="bouton-annuler">Annuler</button> <button class="btn btn-danger" id="bouton-annuler">Annuler</button>
</div> </div>
</form> </form>

View File

@@ -1,5 +1,4 @@
from datetime import timedelta from datetime import timedelta
import json
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
@@ -9,6 +8,9 @@ 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
@@ -20,6 +22,8 @@ def index(request: HttpRequest):
messages.error(request, "Profil employé introuvable.") 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)
@@ -70,14 +74,13 @@ def index(request: HttpRequest):
current_date += timedelta(days=1) current_date += timedelta(days=1)
messages.success(request, "Réservation(s) créée(s) avec succès.") messages.success(request, "Réservation(s) créée(s) avec succès.")
if fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION: if fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION:
fonctions_utilitaire.envoyer_mail( fonctions_utilitaire.envoyer_mail(
sujet = "Reservation de salle", sujet = "Reservation de salle",
message = f""" 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}". 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.""", Veuillez vous connecter à la plateforme pour plus de détails.""",
destinataires = fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION destinataires = list(fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION)
) )
return redirect('gestion_salle:reservation-salle') return redirect('gestion_salle:reservation-salle')
@@ -100,12 +103,10 @@ Veuillez vous connecter à la plateforme pour plus de détails.""",
} }
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")
liste_reservation = [] liste_reservation = []
for reservation in reservations: for reservation in reservations:
color = None color = None
if reservation.statut == "en_attente": if reservation.statut == "en_attente":
@@ -115,8 +116,6 @@ def liste_reservation(request:HttpRequest):
else: else:
color = "#dc3545" color = "#dc3545"
print(dict(Reservation.TYPE_CHOICES).get(reservation.salle))
liste_reservation.append({ liste_reservation.append({
"guid": reservation.pk, "guid": reservation.pk,
"title": dict(Reservation.TYPE_CHOICES).get(reservation.salle), "title": dict(Reservation.TYPE_CHOICES).get(reservation.salle),
@@ -134,9 +133,7 @@ def liste_reservation_attente(request):
{ {
**model_to_dict(reservation), **model_to_dict(reservation),
"employe": f"{reservation.employe.user.first_name} {reservation.employe.user.last_name}", "employe": f"{reservation.employe.user.first_name} {reservation.employe.user.last_name}",
"employe_id": reservation.employe.user.id, "salle": dict(Reservation.TYPE_CHOICES).get(reservation.salle)
"salle": dict(Reservation.TYPE_CHOICES).get(reservation.salle),
'statut': dict(Reservation.STATUT).get(reservation.statut),
} for reservation in reservations } for reservation in reservations
] ]
@@ -161,7 +158,6 @@ def detail_reservation(request:HttpRequest, reservation_id:int):
'besoin_ordinateur': reservation.besoin_ordi, 'besoin_ordinateur': reservation.besoin_ordi,
'lien_zoom': reservation.lien_zoom or '', 'lien_zoom': reservation.lien_zoom or '',
} }
return JsonResponse(reservation_json, safe=True) return JsonResponse(reservation_json, safe=True)
@login_required @login_required
@@ -220,31 +216,32 @@ def valider_reservation(request: HttpRequest):
reservation.statut = 'validee' reservation.statut = 'validee'
reservation.save() reservation.save()
if fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION:
fonctions_utilitaire.envoyer_mail( fonctions_utilitaire.envoyer_mail(
sujet = "Reservation de salle", 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.""", 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] destinataires = [reservation.employe.user.email]
) )
messages.success(request, f"La réservation de {request.user.first_name} {request.user.last_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):
"""Refuser une réservation""" """Refuser une réservation"""
if request.method == 'POST': if request.method == 'POST':
reservation_id = json.loads(request.body).get('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 réservation n'existe pas.") messages.error(request, "La réservation n'existe pas.")
return JsonResponse({"message": "Une erreur s'est produite lors de l'annulation de la reservation."}) return redirect("salle")
reservation.statut = 'refusee' reservation.statut = 'refusee'
reservation.save() reservation.save()
if fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION:
fonctions_utilitaire.envoyer_mail( fonctions_utilitaire.envoyer_mail(
sujet = "Reservation de salle", 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.""", 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.""",

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)

View File

@@ -10,7 +10,6 @@ numpy==2.2.6
pandas==2.3.3 pandas==2.3.3
pillow==12.2.0 pillow==12.2.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-decouple==3.8
pytz==2026.1.post1 pytz==2026.1.post1
requests==2.33.1 requests==2.33.1
six==1.17.0 six==1.17.0
@@ -18,5 +17,5 @@ sqlparse==0.5.5
typing_extensions==4.15.0 typing_extensions==4.15.0
tzdata==2026.2 tzdata==2026.2
urllib3==2.6.3 urllib3==2.6.3
webservices==0.7 python-decouple
webservices.naudit==0.7.2 gunicorn

View File

@@ -3,7 +3,7 @@ const $ = (element) => document.getElementById(element);
const url_liste_conge_attente = $("liste-demande-conges").dataset.url const url_liste_conge_attente = $("liste-demande-conges").dataset.url
const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", { const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", {
// layout : "fitColumns", layout : "fitColumns",
columns: [ columns: [
{"title": "Nom et Prénom", "field": "prenom_nom"}, {"title": "Nom et Prénom", "field": "prenom_nom"},
{"title": "Date de début", "field": "date_debut", formatter:"datetime", formatterParams:{ {"title": "Date de début", "field": "date_debut", formatter:"datetime", formatterParams:{
@@ -15,7 +15,7 @@ const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", {
outputFormat:"dd/MM/yy", outputFormat:"dd/MM/yy",
}}, }},
{"title": "Type de congé", "field": "type"}, {"title": "Type de congé", "field": "type"},
{"title": "Date de la demande", "field": "date_demande", responsive: 1}, {"title": "Date de la demande", "field": "date_demande"},
{"title": "Validation par supérieur hiérarchique", "field": "validation_hierarchique", formatter:"tickCross", formatterParams :{ {"title": "Validation par supérieur hiérarchique", "field": "validation_hierarchique", formatter:"tickCross", formatterParams :{
allowEmpty : true , allowEmpty : true ,
}}, }},
@@ -24,7 +24,7 @@ const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", {
}}, }},
], ],
pagination: true, pagination: true,
paginationSize: 5, paginationSize: 5
}) })
const bouton_demande_conges = $("bouton-demande-conge"); const bouton_demande_conges = $("bouton-demande-conge");

View File

@@ -12,7 +12,6 @@ const tableau_liste_employe = new Tabulator("#tableau_liste_employe", {
// ajaxURL: url_liste_employe, // ajaxURL: url_liste_employe,
pagination: true, pagination: true,
paginationSize: 10, paginationSize: 10,
responsiveLayout : "hide"
}) })
fetch(url_liste_employe) fetch(url_liste_employe)

View File

@@ -16,31 +16,27 @@ const tableau_certificat = new Tabulator("#tableau-certificat", {
], ],
ajaxURL: url_certificat, ajaxURL: url_certificat,
}) })
const enregistrerProfil = $("enregistrerProfil"); const enregistrerProfil = $("enregistrerProfil");
enregistrerProfil.addEventListener("click", (e) => { enregistrerProfil.addEventListener("click", (e) => {
const url = $("information-personnelles").dataset.url; const url = $("information-personnelles").dataset.url;
const csrftoken = document.querySelector("[name='csrfmiddlewaretoken']").value; const csrftoken = document.querySelector("[name='csrfmiddlewaretoken']").value;
const formData = new FormData();
formData.append("nom", $("nom").value);
formData.append("prenom", $("prenom").value);
formData.append("email", $("email").value);
formData.append("telephone", $("telephone").value);
formData.append("adresse", $("adresse").value);
formData.append("sexe", $("sexe").value);
formData.append("date_naissance", $("date_naissance").value);
fetch(url, { fetch(url, {
method: "POST", method: 'POST',
headers: { headers: {
"X-CSRFToken": csrftoken 'Content-Type': 'application/json',
'X-CSRFToken': csrftoken
}, },
body: formData body: JSON.stringify({
"nom": $("nom").value,
"prenom": $("prenom").value,
"email": $("email").value,
"telephone": $("telephone").value,
"adresse": $("adresse").value,
"sexe": $("sexe").value,
"date_naissance": $("date_naissance").value,
})
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => alert(data.message)) .then(data => alert(data.message))
.catch(error => console.error("Erreur:", error)); })
});

View File

@@ -1,5 +1,4 @@
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');
@@ -22,30 +21,3 @@ btnEnregistrerBailleur.addEventListener('click', function() {
} }
}); });
}); });
document.addEventListener("DOMContentLoaded", function () {
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

@@ -15,6 +15,7 @@ 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;
@@ -29,10 +30,11 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
$("besoin_ordinateur").checked=data.besoin_ordinateur; $("besoin_ordinateur").checked=data.besoin_ordinateur;
$("lien_zoom").value=data.lien_zoom; $("lien_zoom").value=data.lien_zoom;
if (data.besoin_zoom === false){ if(data.statut !== "annulee"){
$("lien_zoom_container").className = "d-none"; $("motif_refus_container").className = "d-none";
}else{
$("motif_refus").value=data.motif_refus;
} }
}) })
} }
}); });
@@ -78,11 +80,6 @@ $("bouton-annuler").addEventListener("click", (e) => {
const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value; const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
const url_annuler = $("formulaire-details").dataset.urlannuler; const url_annuler = $("formulaire-details").dataset.urlannuler;
console.log("URL d'annulation :");
console.log($("formulaire-details"))
e.preventDefault();
fetch( fetch(
url_annuler, url_annuler,
{ {
@@ -133,9 +130,6 @@ if($("refuserReservation")){
const url = e.currentTarget.dataset.lienrefus; const url = e.currentTarget.dataset.lienrefus;
const idRes = $("id_reservation_detail").value; const idRes = $("id_reservation_detail").value;
console.log(idRes);
e.preventDefault();
fetch(url, { fetch(url, {
method: "POST", method: "POST",
headers: { headers: {
@@ -167,11 +161,16 @@ const tableau_reservation_attente = new Tabulator("#tableau-reservation-attente"
tableau_reservation_attente.on("rowClick", (row, rowData) => { tableau_reservation_attente.on("rowClick", (row, rowData) => {
const data = rowData.getData(); const data = rowData.getData();
console.log(data);
if(data.besoin_zoom === false){ if(data.besoin_zoom === false){
$("lien_zoom_container").className = 'd-none'; $("lien_zoom_container").className = 'd-none';
} }
const id_user = $("current-user-id").dataset.userid; 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;
@@ -179,18 +178,14 @@ 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_debut").value = data.date_debut; $("date_evenement").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;
if (id_user != data.employe_id){
$("bouton-annuler").className = "d-none";
}
const modal = new bootstrap.Modal($("modalDetailReservation")); const modal = new bootstrap.Modal($("modalDetailReservation"));
bootstrap.Modal.getOrCreateInstance($("modalReservationAttente")).hide(); bootstrap.Modal.getOrCreateInstance($("modalReservationAttente")).hide();