20 Commits

Author SHA1 Message Date
Fatimac2804
1da9d4ca5f Affichage des livrables sur les details de l'activité
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
Organisation/sirh/pipeline/pr-main This commit looks good
SGL/sirh/pipeline/head This commit looks good
2026-09-02 17:17:23 +02:00
Fatimac2804
e81c29a586 Affichage des détails de la réservation de salle
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
Organisation/sirh/pipeline/pr-main This commit looks good
SGL/sirh/pipeline/head This commit looks good
2026-09-02 14:43:27 +02:00
Fatimac2804
d28bf85556 Affichage de la liste des departements pour la modification de l'employer
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
Organisation/sirh/pipeline/pr-main This commit looks good
SGL/sirh/pipeline/head This commit looks good
2026-09-02 14:33:51 +02:00
Fatimac2804
3b587d2a1a Affichage des detait de la reservation de salle
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-09-02 13:46:07 +02:00
Fatimac2804
e3f0befff6 Correction sur l'afficha de la liste de departement pour la modification des informations de l'employer
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-09-02 12:48:35 +02:00
Soriba SYLLA
55e5a2819c Ajout du contexte processor
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-08-31 09:48:21 +00:00
Soriba SYLLA
7e28b25497 Merge branch 'staging' 2026-08-23 15:14:24 +00:00
643205a4bb Premiere implementation de sso
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-08-04 09:34:31 +00:00
4934b42ed9 Implementation de sso 2026-07-29 10:26:22 +00:00
f9ca539670 Supprimer gestion_salle/migrations/0001_initial.py
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-07-14 09:17:13 +00:00
7b4778252f Supprimer gestion_projet/migrations/0001_initial.py
Some checks failed
Organisation/sirh/pipeline/head There was a failure building this commit
2026-07-14 09:17:02 +00:00
2c086187e7 Supprimer gestion_employe/migrations/0001_initial.py
Some checks are pending
Organisation/sirh/pipeline/head Build queued...
2026-07-14 09:16:51 +00:00
5aff49600e Supprimer gestion_conge/migrations/0001_initial.py
Some checks failed
Organisation/sirh/pipeline/head There was a failure building this commit
2026-07-14 09:16:28 +00:00
8c6e59e15a Correction enregistrement profil
Some checks failed
Organisation/sirh/pipeline/head There was a failure building this commit
2026-07-14 09:03:28 +00:00
415860f233 Filtrage des reservations par salle
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-05-15 17:13:02 +00:00
8503689aa4 Bug: Correction du refus des reservations et d'envoi de mail
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-05-15 16:31:11 +00:00
b03a4214c0 Bug: Validation des conges
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-05-11 15:33:04 +00:00
d4f4b7d954 Bug: Validation des conges
Some checks failed
Organisation/sirh/pipeline/head There was a failure building this commit
2026-05-11 15:27:50 +00:00
489eeb439f Correction Bug : Validation 2026-05-11 14:46:59 +00:00
5277b7f355 Bug: Validation de reservation
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-05-11 13:20:13 +00:00
68 changed files with 524 additions and 300 deletions

2
Jenkinsfile vendored
View File

@@ -39,6 +39,8 @@ pipeline
python manage.py makemigrations
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 "Deploiement reussi"

View File

@@ -24,9 +24,10 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
DEBUG = config('DEBUG', default=True, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[]).split(',')
#CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", default=[]).split(",")
# Application definition
@@ -40,12 +41,9 @@ INSTALLED_APPS = [
'gestion_employe',
'gestion_conge',
'gestion_projet',
'gestion_salle',
'simple_sso.sso_server'
'gestion_salle'
]
LOGIN_URL = 'login'
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
@@ -68,6 +66,7 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'contexte_universel.contexte_universel',
],
},
},
@@ -80,24 +79,16 @@ WSGI_APPLICATION = 'SIRH.wsgi.application'
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
if config('ENVIRONMENT') == 'local':
DATABASES = {
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': config('DATABASE_NAME'),
'USER': config('DATABASE_USER'),
'PASSWORD': config('DATABASE_PASSWORD'),
'HOST': config('DATABASE_HOST'),
'PORT': config('DATABASE_PORT'),
}
'ENGINE': 'django.db.backends.mysql',
'NAME': config("DB_NAME"),
'USER': config("DB_USER"),
'PASSWORD': config("DB_PASSWORD"),
'HOST': config("DB_HOST"),
'PORT': config("DB_PORT", cast=int),
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
@@ -150,12 +141,21 @@ MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Connexion sso
# LOGIN_URL = '/client/'
LOGIN_URL = 'simple-sso-login'
SSO_PRIVATE_KEY = config('SSO_PRIVATE_KEY')
SSO_PUBLIC_KEY = config('SSO_PUBLIC_KEY')
SSO_SERVER = config('SSO_SERVER')
#SSO_LOGOUT_URL = 'http://test-authentification.cerfig.org:89/deconnexion'
# Configuration de l'email
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'ssl0.ovh.net'
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_USE_TLS = False
EMAIL_HOST_USER = 'support.it@cerfig.org'
EMAIL_HOST_PASSWORD = 'Cerfig2025'
EMAIL_HOST_USER = config("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

39
SIRH/sso.py Normal file
View File

@@ -0,0 +1,39 @@
# 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()
print("Un petit test", user_data_copy)
user_data.pop("departement", None)
user_data.pop("chef_departement", None)
user_data.pop("groups", None)
user = super().build_user(user_data)
departement = None
if "departement" in user_data_copy:
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", None),
}
)
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,8 +15,9 @@
<body>
<div class="container-fluid">
<div class="row flex-nowrap">
{% include 'parts/menu_application.html' %}
{% include 'parts/menu_principal.html' %}
<div class="col-9 p-4">
<div class="col p-4">
{% block 'contenu' %} {% endblock %}
</div>
</div>

View File

@@ -24,15 +24,16 @@
{% endfor %}
{% endif %}
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}">
<div class="mb-3">
<label for="mail">Votre adresse email :</label>
<input type="text" name="mail" class="form-control" placeholder="Entrez votre e-mail" required>
<label for="mail">Votre adresse email :</label>
<input type="text" name="mail" class="form-control" placeholder="Entrez votre e-mail" required>
</div>
<div class="mb-3">
<label for="mot_de_passe">Mot de passe</label>
<input type="password" name="mot_de_passe" class="form-control" placeholder="Entrez votre mot de passe" required>
<i class="bi bi-eye toggle-password" onclick="togglePassword()"
style="position:absolute; right:10px; top:38px; cursor:pointer;"></i>
<label for="mot_de_passe">Mot de passe</label>
<input type="password" name="mot_de_passe" class="form-control" placeholder="Entrez votre mot de passe" required>
<i class="bi bi-eye toggle-password" onclick="togglePassword()"
style="position:absolute; right:10px; top:38px; cursor:pointer;"></i>
</div>
<button type="submit" class="btn btn-primary w-100">Se connecter</button>
</form>

View File

@@ -0,0 +1,8 @@
<div class="col-1 bg-danger border d-flex flex-column vh-100 pt-5 sticky-top">
<a href="{{ SIRH_LIEN }}" 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="{{ SGL_LIEN }}" 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,35 +13,36 @@
<i class="bi bi-person-circle text-white" style="font-size:60px;"></i>
{% endif %}
<div class="text-white mt-2 fw-bold fs-5">
{{ user.username }}
{{ user.first_name }} {{ user.last_name }}
</div>
</div>
<a href="{% url 'gestion_employe:mon-profil' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<a href="{% url 'gestion_employe:mon-profil' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;">
<i class="bi bi-person-circle"></i> Mon profil
</a>
{% 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.4em">
<a href="{% url 'gestion_employe:index' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;">
<i class="bi bi-speedometer2"></i> Tableau de bord
</a>
{% endif %}
{% 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.4em">
<a href="{% url 'gestion_projet:index' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;">
<i class="bi bi-folder"></i> Gestion des Projets
</a>
{% endif %}
<a href="{% url 'gestion_conges:conge' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<a href="{% url 'gestion_conges:conge' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;">
<i class="bi bi-airplane"></i> Gestion des congés
</a>
{% 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.4em">
<a href="{% url 'gestion_projet:activites-projet' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;">
<i class="bi bi-list-task"></i> Suivi des Activités
</a>
{% endif %}
<a href="{% url 'gestion_salle:reservation-salle' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<a href="{% url 'gestion_salle:reservation-salle' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size: 1.2em;">
<i class="bi bi-calendar-check"></i> Réservation
</a>
<a href="{% url 'deconnexion' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
{% comment %} {% url 'deconnexion' %} {% endcomment %}
<a href="{% url '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
</a>
</div>

View File

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

View File

@@ -1,37 +1,16 @@
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth import logout
from django.shortcuts import redirect
from django.urls import reverse_lazy
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')
from django.conf import settings
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)
def connexion(request):
"""Redirige vers le serveur SSO pour l'authentification."""
return redirect(reverse_lazy("simple-sso-login"))
if user is None:
messages.error(request, "Nom d’utilisateur 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."""
def deconnexion(request):
"""Gérez la déconnexion des utilisateurs."""
logout(request)
return redirect('login')
#return redirect(settings.SSO_LOGOUT_URL)
return redirect("http://authentification.cerfig.org/login")

7
contexte_universel.py Normal file
View File

@@ -0,0 +1,7 @@
from decouple import config
def contexte_universel(request):
return {
"SIRH_LIEN": config("SIRH_LIEN"),
"SGL_LIEN": config("SGL_LIEN")
}

View File

@@ -7,7 +7,11 @@ QUOTA_CONGE_ANNUEL = 30
NOMBRE_PAGINATION = 8
DEBUT_RAPPEL = 60
DUREE_FIN_CONTRAT = 90
EMAIL_ASSISTANTE_DE_DIRECTION = list(Employe.objects.filter(fonction="assistant_direction").values_list('user__email', flat=True))
try:
EMAIL_ASSISTANTE_DE_DIRECTION = list(Employe.objects.filter(fonction="assistant_direction").values_list('user__email', flat=True))
except:
pass
def solde_conge(employe):
"""Fonction de calcul du solde de congé restant l'employé"""

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.13 on 2026-04-17 12:03
# Generated by Django 5.2.13 on 2026-06-16 11:06
import django.db.models.deletion
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 tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", {
layout : "fitColumns",
// layout : "fitColumns",
columns: [
{"title": "Nom et Prénom", "field": "prenom_nom"},
{"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",
}},
{"title": "Type de congé", "field": "type"},
{"title": "Date de la demande", "field": "date_demande"},
{"title": "Date de la demande", "field": "date_demande", responsive: 1},
{"title": "Validation par supérieur hiérarchique", "field": "validation_hierarchique", formatter:"tickCross", formatterParams :{
allowEmpty : true ,
}},
@@ -24,7 +24,7 @@ const tableau_liste_demande_conge = new Tabulator("#liste-demande-conges", {
}},
],
pagination: true,
paginationSize: 5
paginationSize: 5,
})
const bouton_demande_conges = $("bouton-demande-conge");

View File

@@ -7,6 +7,7 @@
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %}">{{message}}</div>
{% endfor %}
{% endif %}
<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="card-header fw-bold">
@@ -32,11 +33,17 @@
<h5 class="fw-bold">{{ nombre_conges_valide }}</h5>
</div>
</div>
<div class="d-flex justify-content-between my-4">
</div>
<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>
<button class='btn btn-primary' id="bouton-demande-conge">Demande de congé</button>
</div>
<div class="table-responsive">
</div>
<div class="row">
<div class="col">
<div id="liste-demande-conges" data-url="{% url 'gestion_conges:liste-des-conges' %}"></div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.13 on 2026-04-17 12:03
# Generated by Django 5.2.13 on 2026-06-16 11:06
import django.db.models.deletion
from django.conf import settings
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
fields=[
('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)),
('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)),
('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)),
('date_embauche', models.DateField(blank=True, null=True)),
('adresse', models.CharField(blank=True, max_length=100, null=True)),
('telephone', models.CharField(blank=True, max_length=15, null=True)),
@@ -48,7 +48,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')], 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'), ('convention_bourse_entretien', "Convention de bourse d'entretien")], max_length=50)),
('date_debut', models.DateField()),
('date_fin', models.DateField(blank=True, null=True)),
('salaire_mensuel', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),

View File

@@ -1,18 +0,0 @@
# 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,6 +12,7 @@ const tableau_liste_employe = new Tabulator("#tableau_liste_employe", {
// ajaxURL: url_liste_employe,
pagination: true,
paginationSize: 10,
responsiveLayout : "hide"
})
fetch(url_liste_employe)

View File

@@ -18,29 +18,34 @@ const tableau_certificat = new Tabulator("#tableau-certificat", {
})
const enregistrerProfil = $("enregistrerProfil");
enregistrerProfil.addEventListener("click", (e) => {
// enregistrerProfil.addEventListener("click", (e) => {
const url = $("information-personnelles").dataset.url;
const csrftoken = document.querySelector("[name='csrfmiddlewaretoken']").value;
// const url = $("information-personnelles").dataset.url;
// const csrftoken = document.querySelector("[name='csrfmiddlewaretoken']").value;
const formData = new FormData();
// 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);
// 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, {
method: "POST",
headers: {
"X-CSRFToken": csrftoken
},
body: formData
})
.then(response => response.json())
.then(data => alert(data.message))
.catch(error => console.error("Erreur:", error));
});
// console.log("---------------------------------------------")
// console.log("Données du formulaire:", url);
// console.log("Données du formulaire:", formData);
// fetch(url, {
// method: "POST",
// headers: {
// "X-CSRFToken": csrftoken
// },
// body: formData
// })
// .then(response => response.json())
// .then(data => alert(data.message))
// .catch(error => console.error("Erreur:", error));
// });

View File

@@ -38,7 +38,7 @@
</h2>
<div id="collapseProfil" class="accordion-collapse" aria-labelledby="headingContrats" data-bs-parent="#accordionInformationEmploye">
<div class="accordion-body">
<div class="row" id="information-personnelles" data-url="{% url 'gestion_employe:modifier-employe' %}">
<form class="row" action = "{% url 'gestion_employe:modifier-employe' %}" method = "POST" enctype = "multipart/form-data" id="information-personnelles" data-url="{% url 'gestion_employe:modifier-employe' %}">
{% csrf_token %}
<div class="col">
<div class="form-group mb-2">
@@ -50,19 +50,19 @@
</div>
<div class="form-group mb-2">
<label>Matricule :</label>
<input type="text" class="form-control" id="matricule" value="{{ employe.matricule|default:'' }}" readonly>
<input type="text" class="form-control" id="matricule" name = 'matricule' value="{{ employe.matricule|default:'' }}" readonly>
</div>
<div class="form-group mb-2">
<label>Nom :</label>
<input type="text" class="form-control" id="nom" value="{{ employe.user.last_name }}">
<input type="text" class="form-control" id="nom" name = 'nom' value="{{ employe.user.last_name }}">
</div>
<div class="form-group mb-2">
<label>Prénom :</label>
<input type="text" class="form-control" id="prenom" value="{{ employe.user.first_name }}">
<input type="text" class="form-control" id="prenom" name = 'prenom' value="{{ employe.user.first_name }}">
</div>
<div class="form-group mb-2">
<label>Sexe :</label>
<select id='sexe' class="form-select">
<select id='sexe' name = 'sexe' class="form-select">
<option value='h' {% if employe.sexe == 'h' %}selected{% endif %}>Homme</option>
<option value='f' {% if employe.sexe == 'f' %}selected{% endif %}>Femme</option>
</select>
@@ -72,27 +72,27 @@
<div class="col">
<div class="form-group mb-2">
<label>Date de naissance :</label>
<input type="date" class="form-control" id="date_naissance" value="{{ employe.date_naissance|date:'Y-m-d' }}">
<input type="date" class="form-control" id="date_naissance" name = 'date_naissance' value="{{ employe.date_naissance|date:'Y-m-d' }}">
</div>
<div class="form-group mb-2">
<label>Département :</label>
<input type="text" class="form-control" id="departement" value="{{ employe.departement.nom|default:'' }}" readonly>
<input type="text" class="form-control" id="departement" name = 'departement' value="{{ employe.departement.nom|default:'' }}" readonly>
</div>
<div class="form-group mb-2">
<label>Email :</label>
<input type="email" class="form-control" id="email" value="{{ employe.user.email|default:'' }}">
<input type="email" class="form-control" id="email" name = 'email' value="{{ employe.user.email|default:'' }}">
</div>
<div class="form-group mb-2">
<label>Téléphone :</label>
<input type="text" class="form-control" id="telephone" value="{{ employe.telephone|default:'' }}">
<input type="text" class="form-control" id="telephone" name = 'telephone' value="{{ employe.telephone|default:'' }}">
</div>
<div class="form-group mb-2">
<label>Adresse :</label>
<input type="text" class="form-control" id="adresse" value="{{ employe.adresse|default:'' }}">
<input type="text" class="form-control" id="adresse" name = 'adresse' value="{{ employe.adresse|default:'' }}">
</div>
</div>
</div>
<button type="submit" class="btn btn-success d-block m-auto" id="enregistrerProfil">Enregistrer</button>
<button type="submit" class="btn btn-success d-block m-auto" id="enregistrerProfil">Enregistrer</button>
</form>
</div>
</div>
</div>

View File

@@ -20,6 +20,9 @@ from fonction_utilitaire import fonctions_utilitaire
@login_required
def index(request):
"""Vue d'index"""
if not request.user.groups.filter(name="ressource_humaine").exists():
return redirect('gestion_employe:mon-profil')
employes = Employe.objects.all().order_by('-user__date_joined')
nombre_employes = Employe.objects.count()
nombre_cps = Contrat.objects.filter(type_contrat='contrat_prestation').count()
@@ -94,7 +97,7 @@ def affecter_employe_projet(request):
elif total_affectation + temps_nouveau > 100:
messages.warning(
request,
f"Les pourcentages d'affectation de l'employé {employe.first_name} {employe.last_name} dépasse 100% sur les différents projets ({total_affectation + temps_nouveau}%)."
f"Les pourcentages d'affectation de l'employé {employe.user.first_name} {employe.user.last_name} dépasse 100% sur les différents projets ({total_affectation + temps_nouveau}%)."
)
return redirect('gestion_employe:index')
@@ -191,25 +194,26 @@ def modifier_employer(request):
except Employe.DoesNotExist:
return JsonResponse({"message": "Employé non trouvé."})
if request.method == "POST":
data = json.loads(request.body)
user = User.objects.get(username=request.user)
user.last_name = data['nom']
user.first_name = data['prenom']
user.email = data['email']
employe.telephone = data['telephone']
employe.adresse = data['adresse']
employe.sexe = data['sexe']
user.last_name = request.POST['nom']
user.first_name = request.POST['prenom']
user.email = request.POST['email']
employe.telephone = request.POST['telephone']
employe.adresse = request.POST['adresse']
employe.sexe = request.POST['sexe']
if request.FILES.get("photo"):
employe.photo = request.FILES["photo"]
if data['date_naissance']:
difference = relativedelta(timezone.now().date(), datetime.strptime(data['date_naissance'], "%Y-%m-%d").date())
if request.POST['date_naissance']:
difference = relativedelta(timezone.now().date(), datetime.strptime(request.POST['date_naissance'], "%Y-%m-%d").date())
if difference.years >= 18:
employe.date_naissance = data['date_naissance']
employe.date_naissance = request.POST['date_naissance']
else:
return JsonResponse({"message": "Veuillez entrez une date de naissance correcte."})
messages.error(request, "Veuillez entrez une date de naissance correcte.")
employe.save()
user.save()
return JsonResponse({"message": "Profil mis à jour avec succès."})
messages.success(request, "Profil mis à jour avec succès.")
return redirect("gestion_employe:mon-profil")
def enregistrement_document(request):
"""Vue pour permettre à un utilisateur de télécharger et enregistrer des documents liés à son profil"""

View File

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

View File

@@ -15,6 +15,11 @@ const tableau_liste_activite = new Tabulator("#tableau-liste-activite", {
})
tableau_liste_activite.on("rowClick", function (row, rowData) {
const data = rowData.getData();
console.log('=== ACTIVITÉ SÉLECTIONNÉE ===');
console.log('ID:', data.id);
console.log('Budget prévu:', data.budget_prevu);
console.log('Budget dépensé:', data.budget_depense);
$("idDetailActivite").value = data.id;
$("titreDetailActivite").value = data.titre;
$("descriptionDetailActivite").value = data.description;
@@ -22,14 +27,79 @@ tableau_liste_activite.on("rowClick", function (row, rowData) {
$("date_finDetailActivite").value = data.date_fin;
$("statutDetailActivite").value = data.statut;
$("budget_prevuDetailActivite").value = data.budget_prevu;
$("budget_depenseDetailActivite").value = data.budget_depense || 0;
// Vérifier si l'activité est annulée
const isAnnulee = data.statut && data.statut.toLowerCase().includes('annul');
console.log('Activité annulée?', isAnnulee);
// Masquer/Afficher les boutons d'action selon le statut
const btnAnnuler = $("btnAnnulerActivite");
const btnMiseAJour = $("btnMiseAJourDepense");
const btnAjouterLivrable = $("btnOuvrirAjouterLivrable");
if (isAnnulee) {
// Activité annulée : masquer les boutons
if (btnAnnuler) btnAnnuler.style.display = "none";
if (btnMiseAJour) btnMiseAJour.style.display = "none";
if (btnAjouterLivrable) btnAjouterLivrable.style.display = "none";
console.log('✓ Boutons masqués - activité annulée');
} else {
// Activité active : afficher les boutons
if (btnAnnuler) btnAnnuler.style.display = "block";
if (btnMiseAJour) btnMiseAJour.style.display = "block";
if (btnAjouterLivrable) btnAjouterLivrable.style.display = "block";
console.log('✓ Boutons affichés - activité active');
}
// Vérifier si les budgets sont différents
const budgetPrevu = parseFloat(data.budget_prevu) || 0;
const budgetDepense = parseFloat(data.budget_depense) || 0;
const isDifferent = budgetDepense !== budgetPrevu;
console.log('Budget prévu:', budgetPrevu);
console.log('Budget dépensé:', budgetDepense);
console.log('Sont-ils différents?', isDifferent);
// Masquer/Afficher le conteneur du motif
const motifContainer = $("motif_container");
if (motifContainer) {
if (isDifferent) {
motifContainer.style.display = "block";
$("motif_changement_budgetDetailActivite").value = data.motif_changement_budget || "";
console.log('✓ Motif AFFICHÉ - budgets différents');
} else {
motifContainer.style.display = "none";
$("motif_changement_budgetDetailActivite").value = "";
console.log('✓ Motif MASQUÉ - budgets identiques');
}
} else {
console.error('❌ Élément motif_container introuvable!');
}
$("besoin_ressources_materiellesDetailActivite").value = data.besoin_ressource_materielle;
const modal = new bootstrap.Modal($("modalDetailActivite"));
modal.show();
fetch(`liste-des-livrables/${data.id}/`)
.then(response => response.json())
const url_livrables = `/gestion-projet/activite/liste-des-livrables/${data.id}/`;
console.log('URL des livrables:', url_livrables);
fetch(url_livrables)
.then(response => {
console.log('Réponse status:', response.status);
return response.json();
})
.then(livrables => {
console.log('Livrables reçus:', livrables);
console.log('Nombre de livrables:', livrables.length);
tableau_liste_livrable.setData(livrables);
$("nombreLivrables").textContent = livrables.length;
console.log('Tableau mis à jour avec', livrables.length, 'livrables');
})
.catch(error => {
console.error(' Erreur lors du chargement des livrables:', error);
tableau_liste_livrable.setData([]);
$("nombreLivrables").textContent = 0;
})
})
@@ -40,6 +110,7 @@ const tableau_liste_livrable = new Tabulator("#listeLivrables", {
],
placeholder: "Aucun livrable trouvé",
})
console.log('✓ Tableau des livrables initialisé');
$("btnMiseAJourDepense").addEventListener("click", function() {
const modal = new bootstrap.Modal($("modalDepenseActivite"));
@@ -57,3 +128,4 @@ $("btnAnnulerActivite").addEventListener("click", function(event) {
$("id_activite_annulation").value = $("idDetailActivite").value;
bootstrap.Modal.getOrCreateInstance($("modalDetailActivite")).hide();
})

View File

@@ -1,58 +1,111 @@
<div class="modal fade" id="modalDetailActivite" tabindex="-1">
<div class="modal-dialog">
<div class="modal fade" id="modalDetailActivite" tabindex="-1"
aria-labelledby="modalDetailActiviteLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Détails de l'activité</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<div class="modal-header bg-orange-dark text-black">
<h5 class="modal-title" id="modalDetailActiviteLabel">
<i class="bi bi-clipboard-check me-2"></i>
Détails de l'activité
</h5>
<button type="button"class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Fermer">
</button>
</div>
<div class="modal-body">
<div class="modal-body" data-url-livrables="{% url 'gestion_projet:liste-livrables-activite' activite_id=0 %}">
<input type="hidden" id="idDetailActivite">
<div class="row g-3">
<div class="">
<input type="hidden" id="idDetailActivite">
<div class="form-group mb-2">
<label>Titre :</label>
<input type="text" class="form-control" id="titreDetailActivite">
<div class="col-12">
<label for="titreDetailActivite" class="form-label fw-bold">
Titre :
</label>
<input type="text"class="form-control" id="titreDetailActivite"readonly>
</div>
<div class="col-12">
<label for="descriptionDetailActivite" class="form-label fw-bold">
Description :
</label>
<textarea class="form-control"id="descriptionDetailActivite" rows="3" readonly></textarea>
</div>
<div class="col-md-6">
<label for="date_debutDetailActivite"class="form-label fw-bold">
Date de début :
</label>
<input type="date"class="form-control"id="date_debutDetailActivite"readonly>
</div>
<div class="col-md-6">
<label for="date_finDetailActivite"class="form-label fw-bold">
Date de fin :
</label>
<input type="date"class="form-control"id="date_finDetailActivite"readonly>
</div>
<div class="col-md-6">
<label for="statutDetailActivite" class="form-label fw-bold">
Statut :
</label>
<input type="text"class="form-control"id="statutDetailActivite"readonly>
</div>
<div class="col-md-6">
<label for="budget_prevuDetailActivite"class="form-label fw-bold">
Budget prévu :
</label>
<input type="number"class="form-control" id="budget_prevuDetailActivite"readonly>
</div>
<div class="col-md-6">
<label for="budget_depenseDetailActivite"class="form-label fw-bold">
Montant dépensé (GNF) :
</label>
<input type="number"class="form-control" id="budget_depenseDetailActivite"readonly>
</div>
<div class="col-12" id="motif_container">
<label for="motif_changement_budgetDetailActivite"class="form-label fw-bold">
Motif de différence :
</label>
<textarea class="form-control"id="motif_changement_budgetDetailActivite"rows="2"readonly></textarea>
</div>
<div class="col-12">
<label for="besoin_ressources_materiellesDetailActivite"class="form-label fw-bold">
Besoin de ressources matérielles :
</label>
<textarea class="form-control"id="besoin_ressources_materiellesDetailActivite"rows="3"readonly></textarea>
</div>
<div class="col-12 mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0 fw-bold">
<i class="bi bi-box-seam me-2"></i>
Liste des livrables
</h5>
<span class="badge bg-primary" id="nombreLivrables">
0
</span>
</div>
<div class="form-group mb-2">
<label>Description :</label>
<textarea class="form-control" id="descriptionDetailActivite"></textarea>
</div>
<div class="form-group mb-2">
<label>Date de début :</label>
<input type="date" class="form-control" id="date_debutDetailActivite">
</div>
<div class="form-group mb-2">
<label>Date de fin :</label>
<input type="date" class="form-control" id="date_finDetailActivite">
</div>
<div class="form-group mb-2">
<label>Statut:</label>
<input type="text" class="form-control" id="statutDetailActivite">
</div>
<div class="form-group mb-2">
<label>Budget prévu :</label>
<input type="number" class="form-control" id="budget_prevuDetailActivite">
</div>
<div class="form-group mb-2">
<label>Besoin de ressources matérielles:</label>
<textarea class="form-control" id="besoin_ressources_materiellesDetailActivite"></textarea>
</div>
<p><strong>Liste des livrables :</strong></p>
<div id="listeLivrables"></div>
<button class="btn btn-danger d-block mx-auto" id="btnAnnulerActivite">
</div>
<div class="col-12 mt-3 text-center">
<button type="button"class="btn btn-danger"id="btnAnnulerActivite">
<i class="bi bi-x-circle me-2"></i>
Annuler cette activité
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-success" id="btnMiseAJourDepense">
<i class="bi bi-plus-circle"></i> Mise à jour du budget
<button type="button"class="btn btn-success"id="btnMiseAJourDepense">
<i class="bi bi-cash-stack me-2"></i>
Mise à jour du budget
</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAjouterLivrable">
<i class="bi bi-plus-circle"></i> Ajouter un livrable
<button type="button"class="btn btn-primary"id="btnOuvrirAjouterLivrable"data-bs-toggle="modal"
data-bs-target="#modalAjouterLivrable">
<i class="bi bi-plus-circle me-2"></i>
Ajouter un livrable
</button>
<button type="button" class="btn btn-secondary"data-bs-dismiss="modal">
Fermer
</button>
</div>
</div>
</div>
</div>

View File

@@ -2,7 +2,6 @@ from datetime import date
from decimal import Decimal, InvalidOperation
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils import timezone
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@@ -382,15 +381,32 @@ def liste_activites_projet(request):
@login_required
def liste_livrables_activite(request, activite_id):
"""Vue pour retourner la liste des livrables attendus d'une activité spécifique"""
livrables = LivrablesLivres.objects.filter(activite__id=activite_id)
data = []
for livrable in livrables:
print(livrable.fichier.url)
data.append({
"titre": livrable.nom,
"lien": livrable.fichier.url if livrable.fichier else "",
})
return JsonResponse(data, safe=False)
try:
livrables = LivrablesLivres.objects.filter(activite__id=activite_id)
data = []
for livrable in livrables:
lien = ""
if livrable.fichier:
try:
lien = livrable.fichier.url
except Exception as e:
print(f"[DEBUG] Erreur accès URL fichier {livrable.nom}: {e}")
lien = ""
data.append({
"titre": livrable.nom,
"lien": lien,
})
print(f"[DEBUG] ✓ Livrables activité {activite_id}: {len(data)} trouvés")
return JsonResponse(data, safe=False)
except Exception as e:
print(f"[DEBUG] Erreur liste_livrables_activite: {e}")
import traceback
traceback.print_exc()
return JsonResponse({"error": f"Erreur serveur: {str(e)}"}, status=500)
@login_required
def mises_a_jour_depense_activite(request):

View File

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

View File

@@ -33,11 +33,6 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
$("lien_zoom_container").className = "d-none";
}
if(data.statut !== "annulee"){
$("motif_refus_container").className = "d-none";
}else{
$("motif_refus").value=data.motif_refus;
}
})
}
});
@@ -83,6 +78,11 @@ $("bouton-annuler").addEventListener("click", (e) => {
const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
const url_annuler = $("formulaire-details").dataset.urlannuler;
console.log("URL d'annulation :");
console.log($("formulaire-details"))
e.preventDefault();
fetch(
url_annuler,
{
@@ -133,6 +133,9 @@ if($("refuserReservation")){
const url = e.currentTarget.dataset.lienrefus;
const idRes = $("id_reservation_detail").value;
console.log(idRes);
e.preventDefault();
fetch(url, {
method: "POST",
headers: {
@@ -164,16 +167,11 @@ const tableau_reservation_attente = new Tabulator("#tableau-reservation-attente"
tableau_reservation_attente.on("rowClick", (row, rowData) => {
const data = rowData.getData();
console.log(data);
if(data.besoin_zoom === false){
$("lien_zoom_container").className = 'd-none';
}
// if(data.statut !== "refusee"){
// $("motif_refus_container").className = 'd-none';
// }
const id_user = $("current-user-id").dataset.userid;
$("id_reservation_detail").value = data.id;
$("id_reservation_refus").value = data.id;
$("id_reservation_zoom").value = data.id;
@@ -189,7 +187,10 @@ tableau_reservation_attente.on("rowClick", (row, rowData) => {
$("besoin_zoom").checked=data.besoin_zoom;
$("besoin_ordinateur").checked=data.besoin_ordi;
$("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"));
bootstrap.Modal.getOrCreateInstance($("modalReservationAttente")).hide();

View File

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

View File

@@ -1,4 +1,5 @@
<!-- 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-dialog">
<div class="modal-content">
@@ -56,12 +57,12 @@
{% if appartient_au_departement_informatique %}
<button class="btn btn-primary" id="ajoutZoom">Ajout du lien zoom</button>
{% endif %}
{% if appartient_direction and reservation.statut == "en_attente" %}
{% if user|has_group:'direction' %}
<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>
{% endif %}
<span id="current-user-id" data-user-id="{{ request.user.id }}"></span>
<button class="btn btn-danger" id="bouton-annuler">Annuler</button>
<span id="current-user-id" data-userid="{{ request.user.id }}"></span>
<button class="btn btn-danger" id="bouton-annuler">Annuler</button>
</div>
</form>
</div>

View File

@@ -1,4 +1,5 @@
from datetime import timedelta
import json
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
@@ -19,8 +20,6 @@ def index(request: HttpRequest):
messages.error(request, "Profil employé introuvable.")
return redirect('gestion_conges:conge')
if request.method == "POST":
form = ReservationForm(request.POST)
@@ -71,13 +70,14 @@ def index(request: HttpRequest):
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)
destinataires = fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION
)
return redirect('gestion_salle:reservation-salle')
@@ -100,10 +100,12 @@ Veuillez vous connecter à la plateforme pour plus de détails.""",
}
return render(request, "gestion_salle/index.html", context)
def liste_reservation(request:HttpRequest):
"""Vue d'affichage des creneaux disponibles"""
reservations = Reservation.objects.filter(statut = "validee")
liste_reservation = []
for reservation in reservations:
color = None
if reservation.statut == "en_attente":
@@ -113,6 +115,8 @@ def liste_reservation(request:HttpRequest):
else:
color = "#dc3545"
print(dict(Reservation.TYPE_CHOICES).get(reservation.salle))
liste_reservation.append({
"guid": reservation.pk,
"title": dict(Reservation.TYPE_CHOICES).get(reservation.salle),
@@ -130,7 +134,9 @@ def liste_reservation_attente(request):
{
**model_to_dict(reservation),
"employe": f"{reservation.employe.user.first_name} {reservation.employe.user.last_name}",
"salle": dict(Reservation.TYPE_CHOICES).get(reservation.salle)
"employe_id": reservation.employe.user.id,
"salle": dict(Reservation.TYPE_CHOICES).get(reservation.salle),
'statut': dict(Reservation.STATUT).get(reservation.statut),
} for reservation in reservations
]
@@ -155,6 +161,7 @@ def detail_reservation(request:HttpRequest, reservation_id:int):
'besoin_ordinateur': reservation.besoin_ordi,
'lien_zoom': reservation.lien_zoom or '',
}
return JsonResponse(reservation_json, safe=True)
@login_required
@@ -213,36 +220,35 @@ def valider_reservation(request: HttpRequest):
reservation.statut = 'validee'
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.")
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"La réservation de {request.user.first_name} {request.user.last_name} validée avec succès.")
return redirect('gestion_salle:reservation-salle')
@login_required
def refuser_reservation(request: HttpRequest):
"""Refuser une réservation"""
if request.method == 'POST':
reservation_id = request.POST.get('id_reservation')
reservation_id = json.loads(request.body).get('id_reservation')
try:
reservation = Reservation.objects.get(id=reservation_id)
except Reservation.DoesNotExist:
messages.error(request, "La réservation n'existe pas.")
return redirect("salle")
return JsonResponse({"message": "Une erreur s'est produite lors de l'annulation de la reservation."})
reservation.statut = 'refusee'
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é refusée. Veuillez vous connecter à l'Assistante de Direction pour plus de détails.""",
destinataires = [reservation.employe.user.email]
)
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."})

View File

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

View File

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

View File

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

View File

@@ -16,27 +16,31 @@ const tableau_certificat = new Tabulator("#tableau-certificat", {
],
ajaxURL: url_certificat,
})
const enregistrerProfil = $("enregistrerProfil");
enregistrerProfil.addEventListener("click", (e) => {
const url = $("information-personnelles").dataset.url;
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, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrftoken
"X-CSRFToken": csrftoken
},
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,
})
body: formData
})
.then(response => response.json())
.then(data => alert(data.message))
})
.catch(error => console.error("Erreur:", error));
});

View File

@@ -1,4 +1,5 @@
const btnEnregistrerBailleur = document.getElementById('btnEnregistrerBailleur');
let table;
btnEnregistrerBailleur.addEventListener('click', function() {
const form = document.getElementById('formBailleur');
@@ -21,3 +22,30 @@ 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,7 +15,6 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
.then(response => response.json())
.then(data => {
currentReservationId = data.id_reservation;
console.log(data);
$("id_reservation_detail").value = data.id_reservation;
$("id_reservation_refus").value = data.id_reservation;
$("id_reservation_zoom").value = data.id_reservation;
@@ -30,11 +29,10 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
$("besoin_ordinateur").checked=data.besoin_ordinateur;
$("lien_zoom").value=data.lien_zoom;
if(data.statut !== "annulee"){
$("motif_refus_container").className = "d-none";
}else{
$("motif_refus").value=data.motif_refus;
if (data.besoin_zoom === false){
$("lien_zoom_container").className = "d-none";
}
})
}
});
@@ -80,6 +78,11 @@ $("bouton-annuler").addEventListener("click", (e) => {
const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
const url_annuler = $("formulaire-details").dataset.urlannuler;
console.log("URL d'annulation :");
console.log($("formulaire-details"))
e.preventDefault();
fetch(
url_annuler,
{
@@ -130,6 +133,9 @@ if($("refuserReservation")){
const url = e.currentTarget.dataset.lienrefus;
const idRes = $("id_reservation_detail").value;
console.log(idRes);
e.preventDefault();
fetch(url, {
method: "POST",
headers: {
@@ -161,16 +167,11 @@ const tableau_reservation_attente = new Tabulator("#tableau-reservation-attente"
tableau_reservation_attente.on("rowClick", (row, rowData) => {
const data = rowData.getData();
console.log(data);
if(data.besoin_zoom === false){
$("lien_zoom_container").className = 'd-none';
}
if(data.statut !== "refusee"){
$("motif_refus_container").className = 'd-none';
}
const id_user = $("current-user-id").dataset.userid;
$("id_reservation_detail").value = data.id;
$("id_reservation_refus").value = data.id;
$("id_reservation_zoom").value = data.id;
@@ -178,14 +179,18 @@ tableau_reservation_attente.on("rowClick", (row, rowData) => {
$("employe").value=data.employe;
$("salle").value=data.salle;
$("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_fin").value=data.heure_fin;
$("motif_reservation").value=data.motif_reservation;
$("besoin_zoom").checked=data.besoin_zoom;
$("besoin_ordinateur").checked=data.besoin_ordi;
$("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"));
bootstrap.Modal.getOrCreateInstance($("modalReservationAttente")).hide();