92 Commits

Author SHA1 Message Date
41dcb84987 Merge branch 'main' into feature/suppression_reservation
All checks were successful
Organisation/sirh/pipeline/pr-main This commit looks good
2026-10-08 12:43:44 +00:00
Soriba SYLLA
0abbb26149 Fix/Correction affectation departement
Some checks failed
Organisation/sirh/pipeline/head There was a failure building this commit
2026-10-07 10:30:08 +00:00
9fb079c9a7 Ajout de la suppression de la reservation de salle
Some checks failed
Organisation/sirh/pipeline/head There was a failure building this commit
Organisation/sirh/pipeline/pr-main There was a failure building this commit
2026-10-07 09:42:34 +00:00
f061f482d9 Feat: Hierarchisation des validations des demandes de reservation
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-09-15 12:46:19 +00:00
622b412a21 Fix: Permettre à un utilisateur d'annuler sa reservation
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-09-14 11:17:01 +00:00
54dc65f0d8 Feat: Envoie de mail à l'équipe informatique après reservation de salle necessitant un lien zoom ou ordinateur
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-09-14 10:42:12 +00:00
Fatimac2804
29a42ee6be feat:Ajout notification email équipe informatique
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
Organisation/sirh/pipeline/pr-main This commit looks good
2026-09-10 12:23:45 +02:00
904954c735 Fix: Modification de mot de passe
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-09-08 11:09:53 +00:00
99e764ab4d Merge branch 'main' of https://jenkins.cerfig.org/CERFIG/sirh 2026-09-07 12:10:34 +00:00
0eb745d5f3 Fix: Correction de l'affichage des dates dans les détails de la réservation 2026-09-07 12:07:58 +00:00
097648c03e Fix: Correction de l'affichage des dates dans les détails de la réservation
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
2026-09-07 11:33:21 +00:00
1a21998c39 Ajout de la barre de navigation
All checks were successful
Organisation/sirh/pipeline/head This commit looks good
SGL/sirh/pipeline/head This commit looks good
2026-09-04 12:19:27 +00:00
900343a4f8 Merge branch 'main' of https://jenkins.cerfig.org/CERFIG/sirh 2026-09-03 14:34:09 +00:00
3c5c58b5d1 Mise a jour: Integration de toutes les suggestions passées 2026-09-03 14:08:01 +00:00
8b9e73f83b Bug: Correction du refus des reservations et d'envoi de mail
All checks were successful
SGL/sirh/pipeline/head This commit looks good
Organisation/sirh/pipeline/head This commit looks good
2026-09-01 10:04:09 +00:00
8f822fbe85 Bug: Validation de reservation 2026-09-01 09:59:50 +00:00
411f2c43e3 Bug: Affichage des details de la reservation 2026-09-01 09:49:39 +00:00
7826f517f5 Bug : Affichages des conges 2026-08-31 16:02:54 +00:00
0936d6ad02 Bug : Affichage des demandes de conges 2026-08-31 15:54:05 +00:00
472774d78c Bug: Details des reservations 2026-08-31 15:48:22 +00:00
d5a0f1104e Gestion de l'envoie des mails 2026-08-31 15:46:29 +00:00
b9d6ab1186 Verification du statut avant la creation d'un contrat 2026-08-31 15:41:45 +00:00
620a01ec27 clean: remove pycache from tracking 2026-08-31 15:26:41 +00:00
f672b9e9ba Prise en charge de la photo de profil 2026-08-31 15:18:36 +00:00
7b3ba53e97 Ajout de la prise en charge des photos de profil 2026-08-31 15:17:38 +00:00
cb4470e481 clean: remove pycache from tracking 2026-08-31 14:46:54 +00:00
dfb262809a Ajout de dependance 2026-08-31 14:42:26 +00:00
8f91a0ec87 Config env 2026-08-31 14:41:28 +00:00
Soriba SYLLA
65ecef52b5 Ajout du contexte processor 2026-08-31 14:26:02 +00:00
13e9d7dd19 Correction: Affichage detail reservation 2026-08-31 14:26:02 +00:00
88de14aabc Implementation de sso 2026-08-31 14:26:02 +00:00
0576443f02 Supprimer gestion_salle/migrations/0001_initial.py 2026-08-31 14:26:02 +00:00
e306b1d67d Supprimer gestion_projet/migrations/0001_initial.py 2026-08-31 14:26:02 +00:00
f4663c6429 Supprimer gestion_employe/migrations/0001_initial.py 2026-08-31 14:26:02 +00:00
6497d5ce90 Supprimer gestion_conge/migrations/0001_initial.py 2026-08-31 14:26:02 +00:00
a10d52e2df Correction enregistrement profil 2026-08-31 14:25:59 +00:00
2a3a330d38 Affichage de la liste des contrat d'un employé 2026-08-31 14:25:13 +00:00
b4fb5bbd91 Correction sur l'affichage des detail de la reservation 2026-08-31 14:25:13 +00:00
098097ea54 Correction modification profil employé 2026-08-31 14:25:13 +00:00
3abdf9ad6f Filtrage des reservations par salle 2026-08-31 14:25:13 +00:00
4338204f48 Bug: Correction du refus des reservations et d'envoi de mail 2026-08-31 14:25:11 +00:00
ac4e21c336 Bug: Validation des conges 2026-08-31 14:22:38 +00:00
c51ea4d334 Bug: Validation des conges 2026-08-31 14:22:34 +00:00
b88a251694 Correction Bug : Validation 2026-08-31 14:20:11 +00:00
15b48d3db6 Bug: Validation de reservation 2026-08-31 14:20:11 +00:00
c69cf6c8f3 feature: Affichage de tous les detail de la resevation 2026-08-31 14:20:11 +00:00
8456a39226 feature: Affichage du message sur l'tat du contrat 2026-08-31 14:20:11 +00:00
bb4d47c6b1 feature: Affichage des details de la reservation 2026-08-31 14:20:11 +00:00
d721af865a Fonctionnalite : Ajout de la liste des contrats 2026-08-31 14:20:08 +00:00
fd74c06251 Fonctionnalite: Ajout des contrats 2026-08-31 13:25:53 +00:00
be357aefa5 Bug: Affichage de la lite des conges en fonction des profil 2026-08-31 13:25:51 +00:00
e06ee9a434 Bug: Affichage des details de la reservation 2026-08-31 13:24:36 +00:00
a89fdd7216 Bug : Affichages des conges 2026-08-31 13:19:46 +00:00
955083da2b Bug : Affichage des demandes de conges 2026-08-31 13:11:37 +00:00
f6715d62d3 Bug: Details des reservations 2026-08-31 13:02:12 +00:00
17f0b4f8dc Gestion de l'envoie des mails 2026-08-31 13:00:12 +00:00
e5309927a5 Fonctionnalite: verifier qu'aucun contrat n'est actif avant la creation d'un nouveau pour un employe 2026-08-31 12:58:07 +00:00
16b4489088 Verification du statut avant la creation d'un contrat 2026-08-31 12:55:00 +00:00
117bbe255b clean: suppression de pycache du tracking 2026-08-31 12:17:42 +00:00
c35cfc50d4 Prise en charge de la photo de profil 2026-08-31 12:17:23 +00:00
d464d19981 Prise en charge de la photo de profil 2026-08-31 12:16:32 +00:00
f7ce6065ac Prise en charge de la photo de profil 2026-08-31 12:16:32 +00:00
9a44d5440a Prise en charge de la photo de profil 2026-08-31 12:16:32 +00:00
cad7c94541 Ajout de la prise en charge des photos de profil 2026-08-31 12:16:23 +00:00
ef86e03c37 clean: suppression de pycache du tracking 2026-08-31 12:15:23 +00:00
c14170caef Finalisation du Jenkinsfile 2026-08-31 12:13:43 +00:00
559158257e Deploiement automatique 2026-08-31 12:13:43 +00:00
a916e12584 Ajout de dependance 2026-08-31 12:13:43 +00:00
63543143c5 Config des environnement 2026-08-31 12:13:43 +00:00
12b0390306 Config env 2026-08-31 12:13:34 +00:00
8478262c80 Derniere modification du Jenkinsfile 2026-08-31 12:11:21 +00:00
078f164c8b utilisation des credentials 2026-08-31 12:11:21 +00:00
39d9dc810f pipeline9 2026-08-31 12:11:21 +00:00
b14ec02434 pipeline 8 2026-08-31 12:11:21 +00:00
6bb978ab7e pipeline 7 2026-08-31 12:11:21 +00:00
81e8fa3901 pipeline 6 2026-08-31 12:11:20 +00:00
2948f12b89 pipeline 5 2026-08-31 12:11:20 +00:00
b88a21dc5d pipeline 4 2026-08-31 12:11:20 +00:00
486ba88885 pipeline3 2026-08-31 12:11:20 +00:00
5fcc163dae pipeline 2 2026-08-31 12:11:20 +00:00
6857c8b4ac Pipeline 1 2026-08-31 12:11:20 +00:00
31389e677e Test pipeline 2026-08-31 12:11:20 +00:00
500479fd46 Ajout de l'agent 2026-08-31 12:11:20 +00:00
b3ead5eb73 jenkins test 3 2026-08-31 12:11:20 +00:00
d9670dd1b3 renommage jenkinsfile 2026-08-31 12:11:20 +00:00
f65ac75710 Test jenkins 1 2026-08-31 12:11:20 +00:00
741519c341 feature: Affichage de la liste des bailleur 2026-04-30 14:16:42 +02:00
48f069506a feature: Affichage des details de la reservation 2026-04-30 13:59:00 +02:00
3693ef29f9 feature: statut creation contrat 2026-04-30 13:33:39 +02:00
c28b14fb98 clean: remove pycache from tracking 2026-04-30 13:28:57 +02:00
29a93e9bfe clean: remove pycache from tracking 2026-04-30 13:28:53 +02:00
a97c233ddb feature: ajout photo de profil 2026-04-30 13:21:16 +02:00
98 changed files with 1166 additions and 3085 deletions

3
.gitignore vendored
View File

@@ -1,7 +1,8 @@
db.sqlite3
venv/
media/
staticfiles/
.env
migrations/
__pycache__/
*.pyc
db.sqlite3

3
Jenkinsfile vendored
View File

@@ -2,7 +2,6 @@ pipeline
{
agent any
options {
// This is required if you want to clean before build
skipDefaultCheckout(true)
}
@@ -17,7 +16,7 @@ pipeline
{
steps
{
sh 'echo "Debut du pipeline"'
sh 'echo "Debut du pipeline test"'
cleanWs()
checkout scm
}

Binary file not shown.

View File

@@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
import os
from pathlib import Path
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -24,7 +25,7 @@ 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=True, cast=bool)
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=[]).split(',')
#CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", default=[]).split(",")
@@ -78,7 +79,6 @@ WSGI_APPLICATION = 'SIRH.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
@@ -88,7 +88,7 @@ DATABASES = {
'HOST': config("DB_HOST"),
'PORT': config("DB_PORT", cast=int),
}
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators

View File

@@ -1,8 +1,9 @@
# 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
from simple_sso.sso_client.client import Client
from gestion_employe.models import Departement, Employe
User = get_user_model()
@@ -11,8 +12,6 @@ class SIRHClient(Client):
"""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)

View File

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

View File

@@ -1,8 +1,24 @@
<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
{% load static %}
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<img src="{% static 'img/cerfig.jpg' %}" alt="Logo" width="85" height="30" class="d-inline-block align-text-top">
</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>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="nav nav-tabs justify-content-end w-100">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ SIRH_LIEN }}">SIRH</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ SGL_LIEN }}">SGL</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Laboratoire</a>
</li>
</ul>
</div>
</div>
</nav>

View File

@@ -14,10 +14,10 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
# from django.conf.urls.static import static
from django.conf import settings
from . import views
from .sso import SIRHClient
@@ -59,4 +59,3 @@ urlpatterns = [
),
path("client/", include(client_sirh.get_urls())),
]
# + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -2,8 +2,6 @@ from django.contrib.auth import logout
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.conf import settings
def connexion(request):
"""Redirige vers le serveur SSO pour l'authentification."""
@@ -12,5 +10,4 @@ def connexion(request):
def deconnexion(request):
"""Gérez la déconnexion des utilisateurs."""
logout(request)
#return redirect(settings.SSO_LOGOUT_URL)
return redirect("http://authentification.cerfig.org/login")

Binary file not shown.

View File

@@ -1,7 +1,9 @@
from django.conf import settings
from django.core.mail import send_mail
from django.utils import timezone
from gestion_employe.models import Contrat, Employe
from gestion_conge.models import Conge
from gestion_conge.models import Conge
from gestion_employe.models import Contrat, Employe
QUOTA_CONGE_ANNUEL = 30
NOMBRE_PAGINATION = 8
@@ -13,6 +15,21 @@ try:
except:
pass
try:
EMAIL_EQUIPE_INFORMATIQUE = list(
Employe.objects.filter(departement__nom="Systeme informatique").values_list('user__email', flat=True)
) or ["support@cerfig.org"]
except:
EMAIL_EQUIPE_INFORMATIQUE = ["support@cerfig.org"]
def est_equipe_informatique(employe):
"""Vérifie si un employé appartient au département informatique"""
return employe.departement and employe.departement.nom == "Systeme informatique"
def est_assistante_direction(employe):
"""Vérifie si un employé est l'assistante de direction"""
return employe.fonction == "assistant_direction"
def solde_conge(employe):
"""Fonction de calcul du solde de congé restant l'employé"""
contrat = Contrat.objects.filter(employe=employe, statut='actif').order_by('-date_debut').first()
@@ -38,19 +55,6 @@ def solde_conge(employe):
"nombre_jours_valide": jours_conges_valider
}
def envoyer_mail(sujet, message, destinataires):
"""Fonction d'envoi de mail"""
from django.core.mail import send_mail
from django.conf import settings
send_mail(
sujet,
message,
settings.EMAIL_HOST_USER,
destinataires,
fail_silently=False,
)
def destinataire_mail_demande_conges(employe):
"""Fonction de récupération des destinataires pour les mails de demande de congés"""
if employe.chef:
@@ -59,3 +63,14 @@ def destinataire_mail_demande_conges(employe):
if employe.departement:
chefs_departement = Employe.objects.filter(departement=employe.departement, chef=True)
return list(chefs_departement.values_list('user__email', flat=True))
def envoyer_mail(sujet, message, destinataires):
"""Fonction d'envoi de mail"""
send_mail(
sujet,
message,
settings.EMAIL_HOST_USER,
destinataires,
fail_silently=False,
)

View File

@@ -1,11 +1,13 @@
from django import forms
from .models import Conge
class CongeForm(forms.ModelForm):
"""Formulaire de demande de congé."""
class Meta:
model = Conge
fields =['type', 'date_debut', 'date_fin']
fields =('type', 'date_debut', 'date_fin')
widgets = {
'date_debut': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'date_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),

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-09-03 14:33
import django.db.models.deletion
from django.db import migrations, models

View File

@@ -1,19 +1,14 @@
import pandas as pd
from django.db import models
from gestion_employe.models import Employe
class Conge(models.Model):
"""Modèle de création des congés"""
TYPE_CHOICES = [
# ('maladie', 'Maladie'),
TYPE_CHOICES = (
('conge_annuel', 'Conge Annuel'),
# ('conge_maternite', 'Conge Maternité'),
# ('conge_mariage', 'Conge Mariage'),
# ('conge_naissance', 'Conge de Naissance'),
# ('conge_deces_proche', 'Conge de décès d\'un proche'),
# ('conge_mariage_proche', 'Conge de mariage d\'un proche'),
# ('autre', 'Autre'),
]
)
employe = models.ForeignKey(
Employe,

View File

@@ -3,7 +3,6 @@ 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",
columns: [
{"title": "Nom et Prénom", "field": "prenom_nom"},
{"title": "Date de début", "field": "date_debut", formatter:"datetime", formatterParams:{

View File

@@ -1,16 +1,20 @@
import json
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.forms.models import model_to_dict
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.utils import timezone
from fonction_utilitaire import fonctions_utilitaire
from gestion_conge.forms import CongeForm
from gestion_employe.models import Affectation, Employe
from django.forms.models import model_to_dict
from django.utils import timezone
from django.db.models import Q
from fonction_utilitaire import fonctions_utilitaire
from .models import Conge
@login_required
def index(request):
"""Vue de gestion de l'index"""
@@ -115,8 +119,7 @@ def demander_conge(request):
conge_obj = form.save(commit=False)
conge_obj.employe = employe
if conge_obj.type == "conge_annuel":
if retour_quota["nombre_jours_valide"] + conge_obj.nombre_jours > quota_annuel:
if conge_obj.type == "conge_annuel" and retour_quota["nombre_jours_valide"] + conge_obj.nombre_jours > quota_annuel:
messages.error(request, "Quota annuel dépassé (30 jours max).")
return redirect("gestion_conges:conge")
@@ -126,7 +129,7 @@ def demander_conge(request):
fonctions_utilitaire.envoyer_mail(
sujet = "Demande de congé",
message = f"""Bonjour {employe.user.first_name} {employe.user.last_name}, votre demande de congé a été enregistrée. Veuillez consulter votre profil pour plus de détails.""",
destinataires = fonctions_utilitaire.destinataire_mail_demande_conges() + [employe.user.email]
destinataires = fonctions_utilitaire.destinataire_mail_demande_conges(employe)
)
return redirect("gestion_conges:conge")
@@ -219,13 +222,13 @@ def validation_de_conge(request):
motif_refus = request_data.get("motif_refus", "")
if validation_hierarchique is not None:
conge.validation_hierarchique = True if validation_hierarchique == "valide" else False
conge.validation_hierarchique = validation_hierarchique == "valide"
if validation_hierarchique == "refuse" and not motif_refus:
return JsonResponse({"message": "Veuillez fournir un motif de refus."})
conge.motif_refus = motif_refus if validation_hierarchique == "refuse" else ""
if validation_direction is not None:
conge.validation_direction = True if validation_direction == "valide" else False
conge.validation_direction = validation_direction == "valide"
if validation_direction == "refuse" and not motif_refus:
return JsonResponse({"message": "Veuillez fournir un motif de refus."})
conge.motif_refus = motif_refus if validation_direction == "refuse" else ""

View File

@@ -1,6 +1,8 @@
from django.contrib import admin
from .models import Departement, Employe
@admin.register(Departement)
class DepartementAdmin(admin.ModelAdmin):
list_display = ("nom", )

View File

@@ -1,11 +1,15 @@
from django import forms
from .models import Contrat, Departement, Employe,Affectation,Formation
from .models import Affectation, Contrat, Departement, Employe, Formation
class EmployeForm(forms.ModelForm):
"""Formulaire pour modifier les informations de profil d'un employé"""
class Meta:
model = Employe
fields = (
'departement',
'fonction',
'adresse',
'telephone',
'CV',
@@ -14,6 +18,17 @@ class EmployeForm(forms.ModelForm):
'photo',
'casier_judiciaire'
)
widgets = {
'departement': forms.Select(attrs={'class': 'form-select'}),
'fonction': forms.Select(attrs={'class': 'form-select'}),
'adresse': forms.TextInput(attrs={'class': 'form-control'}),
'telephone': forms.TextInput(attrs={'class': 'form-control'}),
'CV': forms.FileInput(attrs={'class': 'form-control'}),
'diplome': forms.FileInput(attrs={'class': 'form-control'}),
'rib': forms.FileInput(attrs={'class': 'form-control'}),
'photo': forms.FileInput(attrs={'class': 'form-control'}),
'casier_judiciaire': forms.FileInput(attrs={'class': 'form-control'}),
}
class AffectationForm(forms.ModelForm):
class Meta:
@@ -37,7 +52,7 @@ class AffectationForm(forms.ModelForm):
class ContratForm(forms.ModelForm):
class Meta:
model = Contrat
fields = [
fields = (
"numero_contrat",
"type_contrat",
"date_debut",
@@ -45,7 +60,7 @@ class ContratForm(forms.ModelForm):
"salaire_mensuel",
"statut",
"fichier_contrat"
]
)
widgets = {
'numero_contrat': forms.TextInput(attrs={'class': 'form-control'}),
@@ -60,19 +75,19 @@ class ContratForm(forms.ModelForm):
class DepartementForm(forms.ModelForm):
class Meta:
model = Departement
fields = ['nom']
fields = ('nom', )
class FormationForm(forms.ModelForm):
class Meta:
model = Formation
fields = [
fields = (
'titre',
'organisme',
'date_obtention',
'date_fin',
'description',
'certificat'
]
)
widgets = {
'titre': forms.TextInput(attrs={"class": "form-control"}),

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-09-03 14:33
import django.db.models.deletion
from django.conf import settings

View File

@@ -1,8 +1,10 @@
from django.db import models
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
from gestion_projet.models import Projet
class Departement(models.Model):
"""Modèle représentant un département de l'entreprise."""
nom = models.CharField(max_length=100)
@@ -12,7 +14,7 @@ class Departement(models.Model):
class Employe(models.Model):
"""Modèle représentant un employé de l'entreprise."""
FONCTION_LISTE = [
FONCTION_LISTE = (
('directeur', 'Directeur'),
('assistant_direction', 'Assistante de direction'),
('assistant_technique_recherche', 'Assistant technique de recherche'),
@@ -24,7 +26,7 @@ class Employe(models.Model):
('qualiticien', 'Qualiticien'),
('technicien_surface', 'Technicien de surface'),
('chauffeur', 'Chauffeur'),
]
)
user = models.OneToOneField(User, on_delete=models.CASCADE)
matricule = models.CharField(max_length=10, unique=True, null=True, blank=True)
@@ -52,20 +54,21 @@ class Employe(models.Model):
class Contrat(models.Model):
"""Modèle représentant un contrat de travail d'un employé."""
TYPE_CONTRAT = [
TYPE_CONTRAT = (
('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'),
]
)
STATUT_CONTRAT = [
STATUT_CONTRAT = (
('actif', 'Actif'),
('termine', 'Terminé'),
('suspendu', 'Suspendu'),
('rupture_contrat', 'Rupture de Contrat'),
]
)
employe = models.ForeignKey(Employe, on_delete=models.CASCADE)
numero_contrat = models.CharField(max_length=100, unique=True)
type_contrat = models.CharField(
@@ -89,7 +92,7 @@ class Contrat(models.Model):
class Affectation(models.Model):
"""Modèle représentant l'affectation d'un employé à un projet avec un rôle spécifique."""
ROLE_CHOICES = [
ROLE_CHOICES = (
('chef_projet', 'Chef de projet'),
('doctorant','Doctorant'),
('mastorant','Mastorant'),
@@ -98,7 +101,7 @@ class Affectation(models.Model):
('laborantin','Laborantin'),
('medecin','Médecin'),
('autre','Autre'),
]
)
employe = models.ForeignKey(Employe, on_delete=models.CASCADE)
projet = models.ForeignKey(Projet, on_delete=models.CASCADE)

View File

@@ -9,7 +9,6 @@ const tableau_liste_employe = new Tabulator("#tableau_liste_employe", {
{"title": "Projet", "field": "projet"},
{"title": "Téléphone", "field": "telephone"},
],
// ajaxURL: url_liste_employe,
pagination: true,
paginationSize: 10,
responsiveLayout : "hide"
@@ -19,7 +18,6 @@ fetch(url_liste_employe)
.then(response => response.json())
.then(data => {
if (data.success){
console.log(data.data)
tableau_liste_employe.setData(data.data);
}else{
alert(data.message);
@@ -29,6 +27,7 @@ fetch(url_liste_employe)
tableau_liste_employe.on("rowClick", function (row, rowData) {
const data = rowData.getData();
document.getElementById('detail-id').value = data.id;
document.getElementById('detail-matricule').value = data.matricule;
document.getElementById('detail-employe').value = data.employe;
@@ -211,23 +210,31 @@ $("enregistrerDetail").addEventListener("click", function() {
const fonction = document.getElementById('detail-fonction').value;
const dateEmbauche = document.getElementById('detail-dateEmbauche').value;
const matricule = document.getElementById('detail-matricule').value;
const departement = document.getElementById('detail-departement').value;
const url_enregistrer_detail = document.getElementById("modalDetailEmployeBody").dataset.url;
console.log("Le lien de l'enregistrement :", document.getElementById("modalDetailEmployeBody").dataset)
const payload = {
"id": id_,
"fonction": fonction,
"date_embauche": dateEmbauche,
"matricule": matricule,
"departement": departement
};
const url_enregistrer_detail = $("modalDetailEmployeBody").dataset.url;
fetch(url_enregistrer_detail, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
"id": id_,
"fonction": fonction,
"date_embauche": dateEmbauche,
"matricule": matricule
})
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
console.log('Réponse du serveur:', data);
alert(data.message);
location.reload();
})
@@ -262,3 +269,69 @@ const listeContratExpirant = new Tabulator("#listeContratExpirant", {
],
ajaxURL: $("boutonContratExpirants").dataset.urlexpirants,
})
/* let tabContrats = null;
document.getElementById("modalContratsEmploye").addEventListener("show.bs.modal", function () {
const employeId = document.getElementById("detail-id").value;
if (!employeId) {
alert("Aucun employé sélectionné");
return;
}
fetch(`/employé/employe/${employeId}/contrats/`)
.then(response => {
if (!response.ok) {
throw new Error("Erreur HTTP " + response.status);
}
return response.json();
})
.then(data => {
document.getElementById("nom-employe-contrat").innerText =
data.employe;
if (tabContrats) {
tabContrats.destroy();
}
tabContrats = new Tabulator(
"#table-contrats-tabulator",
{
data: data.contrats,
layout: "fitColumns",
placeholder: "Aucun contrat",
columns: [
{ title: "Numéro", field: "numero_contrat" },
{ title: "Type", field: "type_contrat" },
{ title: "Date début", field: "date_debut" },
{ title: "Date fin", field: "date_fin" },
{ title: "Salaire", field: "salaire_mensuel" },
{ title: "Statut", field: "statut" },
{
title: "Contrat",
field: "fichier",
formatter: function(cell) {
const url = cell.getValue();
if (!url) {
return "<span class='text-danger'>Aucun fichier</span>";
}
return `<a href="${url}" target="_blank" class="btn btn-sm btn-primary">
<i class="bi bi-file-earmark-pdf"></i> Ouvrir
</a>`;
}
}
]
}
);
})
.catch(error => {
console.error("Erreur :", error);
});
}); */

View File

@@ -13,39 +13,40 @@ const tableau_certificat = new Tabulator("#tableau-certificat", {
inputFormat:"yyyy-MM-dd",
outputFormat:"dd/MM/yy",
}},
{"title": "Fichier", "field": "certificat", formatter:"link", formatterParams:{
label:"Voir le fichier",
target:"_blank",
}},
],
ajaxURL: url_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();
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);
const photoInput = $("photo");
if (photoInput.files.length > 0) {
formData.append("photo", photoInput.files[0]);
}
// 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);
// 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));
// });
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

@@ -12,19 +12,20 @@
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} alert-dismissible fade show" role="alert">
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} alert-dismissible fade show mt-2" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% if not expiration_contrat %}
<div class="alert alert-danger fade show alert-dismissible mt-2">
{% if not has_contrat %}
<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.
</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.
</div>
{% endif %}

View File

@@ -0,0 +1,17 @@
<div class="modal fade" id="modalContratsEmploye" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
Contrats de <span id="nom-employe-contrat"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="table-contrats-tabulator"></div>
</div>
</div>
</div>
</div>

View File

@@ -23,7 +23,12 @@
</div>
<div class="form-group mb-2">
<label>Département :</label>
<input type="text" class="form-control" id="detail-departement">
<select class="form-select" id="detail-departement">
<option value="">-- Sélectionner --</option>
{% for dept in departements %}
<option value="{{ dept.id }}">{{ dept.nom }}</option>
{% endfor %}
</select>
</div>
<div class="form-group mb-2">
<label>Sexe :</label>
@@ -93,9 +98,6 @@
</h5>
{% if user|has_group:"ressource_humaine" %}
<div>
<button class="btn btn-secondary" disabled>
<i class="bi bi-file-earmark-lock"></i> Contrat actif
</button>
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#modalContrat{{ item.employe.id }}">
<i class="bi bi-file-earmark-text"></i> Créer contrat
</button>

View File

@@ -6,7 +6,7 @@
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form method="POST" action="{% url 'gestion_employe:modifier-mot-passe' %}">
<form method="POST" action="{% url 'gestion_employe:modifier-mot-passe' %}" id="formulaire_mot_de_passe">
{% csrf_token %}
<div class="form-group mb-2">
<label>Votre mot de passe actuel :</label>

View File

@@ -6,6 +6,7 @@ la gestion de l'accès des utilisateurs aux différentes pages.
from django import template
from django.utils import timezone
from gestion_employe.models import Affectation
register = template.Library()

View File

@@ -1,6 +1,7 @@
from . import views
from django.urls import path
from . import views
app_name = 'gestion_employe'
urlpatterns = [
@@ -29,11 +30,17 @@ urlpatterns = [
views.creation_contrat,
name='creation-contrat'
),
path(
'employe/<int:employe_id>/contrats/'
, views.contrats_employe,
name="contrats-employe"
),
path(
'contrat/supprimer/',
views.suppression_contrat,
name='supprimer-contrat'
),
path(
'Affectation/affectation/',
views.affecter_employe_projet,

View File

@@ -1,28 +1,44 @@
import json
from datetime import date, timedelta, datetime
from datetime import datetime, timedelta
import requests
from dateutil.relativedelta import relativedelta
from django.utils import timezone
from decouple import config
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.forms.models import model_to_dict
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db.models import Sum
from django.forms.models import model_to_dict
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.utils import timezone
from .models import Employe, Contrat, Affectation, Formation
from .forms import AffectationForm, ContratForm, FormationForm
from fonction_utilitaire import fonctions_utilitaire
from .forms import AffectationForm, ContratForm, FormationForm
from .models import Affectation, Contrat, Departement, Employe, Formation
@login_required
def index(request):
"""Vue d'index"""
if not request.user.groups.filter(name="ressource_humaine").exists():
return redirect('gestion_employe:mon-profil')
response = requests.get(f"{config('SERVEUR_AUTHENTIFICATION')}recuperer-list-departement/")
if response.status_code == 200:
try:
departements_data = response.json()
for dept in departements_data:
Departement.objects.update_or_create(
nom = dept['nom']
)
except json.JSONDecodeError as e:
print(f"[ERROR] Erreur lors du décodage JSON : {e}")
employes = Employe.objects.all().order_by('-user__date_joined')
nombre_employes = Employe.objects.count()
nombre_cps = Contrat.objects.filter(type_contrat='contrat_prestation').count()
@@ -41,7 +57,8 @@ def index(request):
'nombre_stage': nombre_stage,
'nombre_expirants': nombre_expirants,
'affectation_form': AffectationForm(),
'contrat_form': ContratForm()
'contrat_form': ContratForm(),
'departements': Departement.objects.all()
})
@login_required
@@ -132,13 +149,17 @@ def mon_profil(request):
try:
employe = Employe.objects.get(user__username=request.user)
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")
contrats = Contrat.objects.filter(employe=employe, statut='actif').first()
projets = Affectation.objects.filter(
employe = employe,
date_fin_daffectation__gte = timezone.now().date()
employe=employe,
date_fin_daffectation__gte=timezone.now().date()
).select_related('projet')
return render(
@@ -146,12 +167,14 @@ def mon_profil(request):
'gestion_employe/monprofil.html',
{
'employe': employe,
'contrats': [{
**model_to_dict(contrats),
"type_contrat": dict(Contrat.TYPE_CONTRAT).get(contrats.type_contrat),
"statut": dict(Contrat.STATUT_CONTRAT).get(contrats.statut),
"fichier_contrat": contrats.fichier_contrat.url if contrats.fichier_contrat else "",
} if contrats else []],
}] if contrats else [],
'projets': [
{
**model_to_dict(a.projet),
@@ -161,32 +184,14 @@ def mon_profil(request):
"pourcentage_temps_affectation": a.pourcentage_temps_affectation
} for a in projets
],
"formation_form": FormationForm(),
"has_contrat": contrats is not None,
"expiration_contrat": contrats.nombre_jours_restant <= fonctions_utilitaire.DUREE_FIN_CONTRAT if contrats else False,
"contrat_nb_jours_restant": contrats.nombre_jours_restant if contrats else None
"contrat_nb_jours_restant": max(0, contrats.nombre_jours_restant if contrats else 0)
}
)
@login_required
def modifier_mot_passe(request):
"""Vue pour permettre à un utilisateur de modifier son mot de passe et ses informations de profil"""
user = User.objects.get(username=request.user)
if request.method == "POST":
ancien_mdp = request.POST["ancien-mdp"]
nouveau_mdp = request.POST["nouveau-mdp"]
confirmation_mdp = request.POST["confirmation-mdp"]
if authenticate(request, username=request.user, password=ancien_mdp) is None:
messages.error(request, "Ancien mot de passe incorrect.")
elif nouveau_mdp != confirmation_mdp:
messages.error(request, "Les deux nouveaux ne correspondent pas.")
else:
user.set_password(nouveau_mdp)
user.save()
messages.success(request, "Mot de passe modifié avec succès.")
return redirect("gestion_employe:mon-profil")
def modifier_employer(request):
"""Vue pour permettre à un utilisateur de modifier les informations d'un employé"""
try:
@@ -214,7 +219,6 @@ def modifier_employer(request):
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"""
employe = Employe.objects.get(user=request.user)
@@ -265,9 +269,10 @@ def creation_contrat(request):
except Employe.DoesNotExist:
messages.error(request, "Employé non trouvé.")
return redirect('employe-index')
contrat_actif = Contrat.objects.filter(
employe=employe,
date_fin__gte=date.today()
date_fin__gte=timezone.now().date()
).exists()
if request.method == "POST":
@@ -293,6 +298,45 @@ def creation_contrat(request):
'contrat_form': form
})
@login_required
def contrats_employe(request, employe_id):
try:
employe = Employe.objects.get(id=employe_id)
except Employe.DoesNotExist:
return JsonResponse(
{"error": "Employé non trouvé"},
status=404
)
contrats = Contrat.objects.filter(
employe=employe
).order_by("-date_debut")
data = [
{
"id": contrat.id,
"numero_contrat": contrat.numero_contrat,
"type_contrat": dict(Contrat.TYPE_CONTRAT).get(
contrat.type_contrat
),
"date_debut": contrat.date_debut.strftime("%Y-%m-%d"),
"date_fin": contrat.date_fin.strftime("%Y-%m-%d") if contrat.date_fin else "",
"salaire_mensuel": contrat.salaire_mensuel,
"statut": dict(Contrat.STATUT_CONTRAT).get(
contrat.statut
),
"fichier": contrat.fichier_contrat.url if contrat.fichier_contrat else "",
}
for contrat in contrats
]
return JsonResponse({
"employe": employe.user.get_full_name(),
"contrats": data
})
@login_required
def enregistrer_detail_employe(request):
"""Vue pour permettre à un utilisateur de modifier les détails d'un employé via une requête AJAX"""
@@ -303,10 +347,27 @@ def enregistrer_detail_employe(request):
except Employe.DoesNotExist:
return JsonResponse({"error": "Employé non trouvé."}, status=404)
employe.fonction = data['fonction']
employe.date_embauche = data['date_embauche']
employe.matricule = data['matricule']
employe.fonction = data.get('fonction')
employe.date_embauche = data.get('date_embauche')
employe.matricule = data.get('matricule')
# Traiter le département
dept_value = data.get('departement')
if dept_value and str(dept_value).strip():
try:
dept_id = int(dept_value)
departement = Departement.objects.get(id=dept_id)
employe.departement = departement
except Departement.DoesNotExist:
employe.departement = None
except (ValueError, TypeError) as e:
employe.departement = None
else:
employe.departement = None
employe.save()
return JsonResponse({"message": "Détails de l'employé mis à jour avec succès."})
else:
return JsonResponse({"message": "Méthode non autorisée."}, status=405)
@@ -368,7 +429,7 @@ def liste_employe(request):
"affectations": affectations,
"projet": projets,
"contrats": contrats,
"departement": employe.departement.nom if employe.departement else "",
"departement": employe.departement.id if employe.departement else "",
"fonction": employe.fonction,
"date_embauche": employe.date_embauche,
"adresse": employe.adresse,
@@ -440,3 +501,47 @@ def supprimer_formation(request, id_formation):
formation.delete()
messages.success(request, "Formation supprimée ")
return redirect("mes_formations")
@login_required
def modifier_mot_passe(request):
"""Vue pour permettre à un employé de modifier son mot de passe"""
if request.method == "POST":
username = request.user.username
ancien_mdp = request.POST.get("ancien-mdp")
nouveau_mdp = request.POST.get("nouveau-mdp")
confirmation_mdp = request.POST.get("confirmation-mdp")
try:
validate_password(nouveau_mdp, user=request.user)
except ValidationError as e:
messages.error(request, f"Erreur : {' '.join(e.messages)}")
return redirect("gestion_employe:mon-profil")
donnees = {
"username": username,
"ancien_mdp": ancien_mdp,
"nouveau_mdp": nouveau_mdp,
"confirmation_mdp": confirmation_mdp
}
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
response = requests.post(
f"{config('SERVEUR_AUTHENTIFICATION')}modifier-mot-de-passe/",
json=donnees,
headers=headers
)
data = response.json()
cle = next(iter(data.keys()))
valeur = next(iter(data.values()))
if cle == "success":
messages.success(request, valeur)
else:
messages.error(request, valeur)
return redirect("gestion_employe:mon-profil")

View File

@@ -1,14 +1,16 @@
from django import forms
from gestion_projet.models import Projet
from .models import (
ActiviteProjet,
Bailleur,
DocumentProjet,
FinancementProjet,
LivrablesLivres,
DomaineDeRecherche
)
class ProjetForm(forms.ModelForm):
"""Formulaire de création et de modification d'un projet, avec validation des dates et personnalisation des champs."""
class Meta:
@@ -20,7 +22,6 @@ class ProjetForm(forms.ModelForm):
'date_fin',
'numero_convention',
'domaine_recherche',
'type_projet',
'budget',
'budget_RH',
'description'
@@ -35,7 +36,6 @@ class ProjetForm(forms.ModelForm):
'nom_projet': forms.TextInput(attrs={'class': "form-control"}),
'numero_convention': forms.TextInput(attrs={'class': "form-control"}),
'domaine_recherche': forms.SelectMultiple(attrs={'class': "form-control"}),
'type_projet': forms.Select(attrs={'class': "form-select"}),
'budget': forms.NumberInput(attrs={'class': "form-control"}),
'budget_RH': forms.NumberInput(attrs={'class': "form-control"}),
'description': forms.Textarea(attrs={'class': "form-control"}),

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-09-03 14:33
import django.db.models.deletion
from django.db import migrations, models

View File

@@ -1,7 +1,7 @@
from django.db import models
from datetime import date
from django.utils import timezone
class Bailleur(models.Model):
"""Modèle représentant un bailleur de fonds pour les projets de recherche."""
nom_organisme = models.CharField(
@@ -29,12 +29,14 @@ class Bailleur(models.Model):
class DomaineDeRecherche(models.Model):
"""Modèle représentant les domaines de recherche"""
DOMAINE_RECHERCHE = [
DOMAINE_RECHERCHE = (
('sciences_sociales', 'Sciences sociales'),
('naturelles', 'Naturelles'),
('humaines', 'Humaines'),
('veterinaires', 'Vétérinaires')
]
('epidemiologie', 'Epidémiologie'),
('sciences_biomedicales', 'Sciences biomédicales'),
('service_pedagogique', 'Service Pédagogique'),
('recherche_clinique', 'Recherche clinique')
)
nom = models.CharField(
max_length=100,
@@ -52,13 +54,7 @@ class DomaineDeRecherche(models.Model):
class Projet(models.Model):
"""Modèle représentant un projet de recherche avec ses caractéristiques et son bailleur associé."""
TYPE_PROJET = [
('laboratoire', 'Laboratoire'),
('épidémiologie', 'Épidémiologie'),
('sciences sociales', 'Sciences sociales'),
('cliniques', 'Cliniques'),
('autre', 'Autre'),
]
id_projet = models.CharField(
max_length=100,
blank=True,
@@ -83,12 +79,6 @@ class Projet(models.Model):
description = models.TextField(
verbose_name="Description"
)
type_projet = models.CharField(
max_length=100,
choices=TYPE_PROJET,
default='épidémiologie',
verbose_name="Type de projet"
)
domaine_recherche = models.ManyToManyField(DomaineDeRecherche)
budget=models.DecimalField(
max_digits=12,
@@ -111,13 +101,13 @@ class Projet(models.Model):
@property
def statut(self):
if self.date_fin < date.today():
if self.date_fin < timezone.now().date():
return "Terminé"
return "En cours"
@property
def avancement(self):
aujourd_hui = date.today()
aujourd_hui = timezone.now().date()
if (self.date_debut and self.date_fin) and (self.date_debut < self.date_fin):
duree_projet = (self.date_fin - self.date_debut).days
temps_ecoule = (aujourd_hui - self.date_debut).days
@@ -155,7 +145,7 @@ class FinancementProjet(models.Model):
class DocumentProjet(models.Model):
"""Modèle représentant un document associé à un projet, avec des métadonnées et un fichier attaché."""
NOM_DOCUMENT_CHOICES = [
NOM_DOCUMENT_CHOICES = (
('protocole', 'Protocole d’étude'),
('ethique', "Approbation du comité d'éthique"),
('autorisation', 'Autorisation (DNLP)'),
@@ -165,7 +155,7 @@ class DocumentProjet(models.Model):
('convention', 'Convention'),
('rapport_final', 'Rapport final'),
('autre', 'Autre'),
]
)
projet = models.ForeignKey(
Projet,

View File

@@ -23,9 +23,8 @@ btnEnregistrerBailleur.addEventListener('click', function() {
});
});
document.addEventListener("DOMContentLoaded", function () {
table = new Tabulator("#table-bailleurs", {
table = new Tabulator("#table-bailleurs", {
ajaxURL: "/gestion-projet/bailleurs/",
layout: "fitColumns",
pagination: "local",
@@ -45,7 +44,4 @@ document.addEventListener("DOMContentLoaded", function () {
supprimerBailleur(data.id);
}
}
});
});

View File

@@ -1,5 +1,6 @@
const $ = (element) => document.getElementById(element)
const url_liste_projet = $("tableau-liste-projet").dataset.url;
const tableau_liste_projet = new Tabulator("#tableau-liste-projet", {
fitColumns: true,
responsiveLayout : true,
@@ -39,7 +40,6 @@ tableau_liste_projet.on("rowClick", function (row, rowData) {
$("detail_date_debut").value = data.date_debut;
$("detail_date_fin").value = data.date_fin;
$("detail_numero_convention").value = data.numero_convention;
$("detail_type_projet").value = data.type_projet;
Array.from($("detail_domaine_recherche").options).forEach(option => {
if (data.domaine_recherche.includes(option.value)) {
option.selected = true;

View File

@@ -15,11 +15,6 @@ 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;
@@ -27,79 +22,14 @@ 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();
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();
})
fetch(`liste-des-livrables/${data.id}/`)
.then(response => 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;
})
})
@@ -110,7 +40,6 @@ 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"));
@@ -128,4 +57,3 @@ $("btnAnnulerActivite").addEventListener("click", function(event) {
$("id_activite_annulation").value = $("idDetailActivite").value;
bootstrap.Modal.getOrCreateInstance($("modalDetailActivite")).hide();
})

View File

@@ -1,111 +1,58 @@
<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 fade" id="modalDetailActivite" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<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 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>
<div class="modal-body" data-url-livrables="{% url 'gestion_projet:liste-livrables-activite' activite_id=0 %}">
<input type="hidden" id="idDetailActivite">
<div class="modal-body">
<div class="row g-3">
<div class="col-12">
<label for="titreDetailActivite" class="form-label fw-bold">
Titre :
</label>
<input type="text"class="form-control" id="titreDetailActivite"readonly>
<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>
<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 class="form-group mb-2">
<label>Description :</label>
<textarea class="form-control" id="descriptionDetailActivite"></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 class="form-group mb-2">
<label>Date de début :</label>
<input type="date" class="form-control" id="date_debutDetailActivite">
</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 class="form-group mb-2">
<label>Date de fin :</label>
<input type="date" class="form-control" id="date_finDetailActivite">
</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 class="form-group mb-2">
<label>Statut:</label>
<input type="text" class="form-control" id="statutDetailActivite">
</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 class="form-group mb-2">
<label>Budget prévu :</label>
<input type="number" class="form-control" id="budget_prevuDetailActivite">
</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 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>
</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>
<button class="btn btn-danger d-block mx-auto" id="btnAnnulerActivite">
Annuler cette activité
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button"class="btn btn-success"id="btnMiseAJourDepense">
<i class="bi bi-cash-stack me-2"></i>
Mise à jour du budget
<button class="btn btn-success" id="btnMiseAJourDepense">
<i class="bi bi-plus-circle"></i> Mise à jour du budget
</button>
<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 class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAjouterLivrable">
<i class="bi bi-plus-circle"></i> Ajouter un livrable
</button>
</div>
</div>
</div>
</div>

View File

@@ -31,23 +31,15 @@
<label>Date de fin :</label>
<input type="date" class="form-control" id="detail_date_fin" name='date_fin'>
</div>
<div class="form-group mb-2">
<label> Type de projet </label>
<select class="form-select" id="detail_type_projet" name='type_projet'>
<option value="laboratoire">Laboratoire</option>
<option value="épidémiologie">Épidémiologie</option>
<option value="sciences sociales">Sciences sociales</option>
<option value="cliniques">Cliniques</option>
<option value="autre">Autre</option>
</select>
</div>
<div class="form-group mb-2">
<label> Domaine de recherche </label>
<select class="form-select" multiple id="detail_domaine_recherche" name="domaine_recherche">
<option value="sciences_sociales">Sciences sociales</option>
<option value="naturelles">Naturelles</option>
<option value="humaines">Humaines</option>
<option value="veterinaires">Vétérinaires</option>
<option value="epidemiologie">Epidémiologie</option>
<option value="sciences_biomedicales">Sciences biomédicales</option>
<option value="service_pedagogique">Service Pédagogique</option>
<option value="recherche_clinique">Recherche clinique</option>
</select>
</div>
<div class="form-group mb-2">

View File

@@ -90,12 +90,6 @@ urlpatterns = [
views.liste_activites_projet,
name='liste-activites-projet'
),
# path(
# 'projet/ajout-de-document/',
# views.ajouter_document_projet,
# name='ajouter-document'
# ),
path(
'projet/liste-des-documents/',
views.liste_documents_projet,

View File

@@ -1,30 +1,34 @@
from datetime import date
import datetime
from decimal import Decimal, InvalidOperation
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.utils import timezone
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.forms.models import model_to_dict
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.utils import timezone
from gestion_employe.forms import AffectationForm
from gestion_employe.models import Affectation, Employe
from gestion_projet.forms import ProjetForm
from gestion_projet.models import Projet
from .models import (
DocumentProjet,
Bailleur,
FinancementProjet,
ActiviteProjet,
LivrablesLivres,
)
from .forms import (
ActiviteProjetForm,
BailleurForm,
DocumentProjetForm,
FinancementProjetFrom,
BailleurForm,
LivrablesLivresForm
LivrablesLivresForm,
)
from .models import (
ActiviteProjet,
Bailleur,
DocumentProjet,
FinancementProjet,
LivrablesLivres,
)
def liste_projet(request):
""" Vue pour retourner la liste de tous les projet """
@@ -40,7 +44,6 @@ def liste_projet(request):
"date_fin": p.date_fin,
"numero_convention": p.numero_convention,
"description": p.description,
"type_projet": p.type_projet,
"budget": p.budget,
"budget_RH": p.budget_RH,
"created_at": p.created_at,
@@ -88,7 +91,7 @@ def index(request):
'nombre_personnel': nombre_personnel,
'budget_total': budget_total,
'formulaire_creation_projet': ProjetForm(),
'projet_en_cours': Projet.objects.filter(date_fin__gte=date.today()).count(),
'projet_en_cours': Projet.objects.filter(date_fin__gte=datetime.datetime.now(tz=datetime.timezone.utc).date()).count(),
}
return render(request, 'gestion_projet/index.html', context)
@@ -137,8 +140,7 @@ def mises_a_jour_projet(request):
@login_required
def creation_bailleur(request):
form = BailleurForm(request.POST)
if request.method == 'POST':
if form.is_valid():
if request.method == 'POST' and form.is_valid():
form.save()
return JsonResponse({'success': True})
return JsonResponse({'success': False})
@@ -381,33 +383,16 @@ 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"""
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 = ""
print(livrable.fichier.url)
data.append({
"titre": livrable.nom,
"lien": lien,
"lien": livrable.fichier.url if livrable.fichier else "",
})
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):
"""Vue pour retourner la liste des activités d'un projet spécifique avec leurs dépenses mises à jour"""

View File

@@ -1,3 +1 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,10 +1,12 @@
from django import forms
from .models import Reservation
class ReservationForm(forms.ModelForm):
class Meta:
model = Reservation
fields = ['salle', 'date_debut', 'date_fin', 'heure_debut', 'heure_fin', 'motif_reservation', 'besoin_zoom', 'besoin_ordi']
fields = ('salle', 'date_debut', 'date_fin', 'heure_debut', 'heure_fin', 'motif_reservation', 'besoin_zoom', 'besoin_ordi')
widgets = {
'date_debut': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'date_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),

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-09-03 14:33
import django.db.models.deletion
from django.db import migrations, models

View File

@@ -1,20 +1,21 @@
from django.db import models
from gestion_employe.models import Employe
class Reservation(models.Model):
"""Modèle de création des réservations"""
TYPE_CHOICES = [
TYPE_CHOICES = (
('formation', 'Salle de formation'),
('reunion', 'Salle de réunion'),
('lien_zoom', 'Lien Zoom'),
]
STATUT = [
)
STATUT = (
('en_attente', 'En attente'),
('validee', 'Validée'),
('refusee', 'Refusée'),
('annulee', 'Annulée'),
]
)
employe = models.ForeignKey(Employe, on_delete=models.CASCADE)
salle = models.CharField(max_length=100, choices=TYPE_CHOICES)

View File

@@ -2,7 +2,71 @@ const $ = (element) => document.getElementById(element);
const { Schedule } = calendarjs;
let dateAUtiliser = new Date().toISOString().split('T')[0];
let currentReservationId = null;
function afficherDetailsReservation(data) {
const reservationId = data.id_reservation || data.id;
$("id_reservation_detail").value = reservationId;
$("id_reservation_refus").value = reservationId;
$("id_reservation_zoom").value = reservationId;
$("employe").value = data.employe;
$("salle").value = data.salle;
$("statut-reservation").textContent = data.statut;
$("date_evenement").value = data.date_evenement || data.date_debut;
$("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_ordinateur ?? data.besoin_ordi;
$("lien_zoom").value = data.lien_zoom || "";
$("lien_zoom_container").classList.toggle("d-none", !data.besoin_zoom);
const currentUserId = $("current-user-id").dataset.userId;
const isOwner = data.est_proprietaire === true
|| String(data.employe_id) === currentUserId;
const isPending = data.est_en_attente === true
|| data.statut === "En attente"
|| data.statut === "en_attente";
$("bouton-supprimer-reservation").classList.toggle(
"d-none",
!(isOwner && isPending)
);
for (const id of ["refuserReservation", "refuserReservationInfo", "bouton-valider", "bouton-valider-info"]) {
const button = $(id);
if (button) {
button.style.display = isPending && !isOwner ? "inline-block" : "none";
}
}
}
function supprimerReservation(reservationId) {
if (!window.confirm("Voulez-vous vraiment supprimer cette demande de réservation en attente ?")) {
return;
}
const form = $("formulaire-details");
const csrf = form.querySelector("[name=csrfmiddlewaretoken]").value;
const url = $("bouton-supprimer-reservation").dataset.url.replace(
"/0/",
`/${reservationId}/`
);
fetch(url, {
method: "POST",
headers: {"X-CSRFToken": csrf},
})
.then(response => response.json().then(data => ({response, data})))
.then(({response, data}) => {
window.alert(data.message);
if (response.ok && data.success) {
window.location.reload();
}
})
.catch(() => {
window.alert("La suppression a échoué. Veuillez réessayer.");
});
}
const calendrier = Schedule(document.getElementById('planning-reservation'), {
type: 'weekdays',
@@ -12,28 +76,16 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
const modal = new bootstrap.Modal($("modalDetailReservation"));
modal.show();
fetch (`/gestion-salle/revervation/details/${event.guid}`)
.then(response => response.json())
.then(data => {
currentReservationId = data.id_reservation;
$("id_reservation_detail").value = data.id_reservation;
$("id_reservation_refus").value = data.id_reservation;
$("id_reservation_zoom").value = data.id_reservation;
$("employe").value=data.employe;
$("salle").value=data.salle;
$("statut-reservation").innerHTML=data.statut;
$("date_evenement").value=data.date_evenement;
$("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_ordinateur;
$("lien_zoom").value=data.lien_zoom;
if (data.besoin_zoom === false){
$("lien_zoom_container").className = "d-none";
.then(response => {
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
afficherDetailsReservation(data);
})
.catch(error => console.error("Impossible de charger les détails de la réservation :", error));
}
});
@@ -46,6 +98,10 @@ $('semaineDate').addEventListener('change', () => {
calendrier.render();
})
$("bouton-supprimer-reservation").addEventListener("click", () => {
supprimerReservation($("id_reservation_detail").value);
});
evenement_defini = null
$("liste-salle").addEventListener("change", (e) => {
@@ -64,40 +120,22 @@ $("liste-salle").addEventListener("change", (e) => {
function chargement_evenement(){
const url = $("planning-reservation").dataset.url;
fetch (url)
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
calendrier.setData(data);
})
.catch(error => console.error("Impossible de charger les réservations :", error));
}
document.addEventListener("DOMContentLoaded", function () {
chargement_evenement()
})
$("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,
{
method: "POST",
headers: {
"X-Requested-With": "XMTHttpRequest",
"X-CSRFToken": csrf
},
body: new FormData($("formulaire-details"))
}
)
.then(response => response.json())
.then(data => console.log(data))
})
if($("bouton-valider")){
$("bouton-valider").addEventListener("click", (e) => {
const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
@@ -129,6 +167,8 @@ if($("ajoutZoom")){
if($("refuserReservation")){
$("refuserReservation").addEventListener("click", (e) => {
e.preventDefault();
const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
const url = e.currentTarget.dataset.lienrefus;
const idRes = $("id_reservation_detail").value;
@@ -155,7 +195,7 @@ const tableau_reservation_attente = new Tabulator("#tableau-reservation-attente"
columns: [
{title: "Employé", field: "employe"},
{title: "Salle", field: "salle"},
{title: "Date de l'evenement", field: "date_debut"},
{title: "Date de l'evenement", field: "date_evenement"},
{title: "Heure de début", field: "heure_debut"},
{title: "Heure de fin", field: "heure_fin"},
{title: "Motif de reservation", field: "motif_reservation"},
@@ -167,30 +207,7 @@ const tableau_reservation_attente = new Tabulator("#tableau-reservation-attente"
tableau_reservation_attente.on("rowClick", (row, rowData) => {
const data = rowData.getData();
if(data.besoin_zoom === false){
$("lien_zoom_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;
$("employe").value=data.employe;
$("salle").value=data.salle;
$("statut-reservation").innerHTML=data.statut;
$("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;
if (id_user != data.employe_id){
$("bouton-annuler").className = "d-none";
}
afficherDetailsReservation(data);
const modal = new bootstrap.Modal($("modalDetailReservation"));
bootstrap.Modal.getOrCreateInstance($("modalReservationAttente")).hide();

View File

@@ -16,7 +16,7 @@
{% endif %}
<div class="row">
<div class="col py-2 px-4">
<h1 class="text-center">Reservation de salle</h1>
<h1 class="text-center">Réservation de salle</h1>
<hr>
<div class="row d-flex justify-content-around">
<div class="form-group col-5 me-2">
@@ -34,7 +34,7 @@
</div>
<div class="row mt-2">
<div class="col d-flex justify-content-center">
<button class="btn btn-lg btn-primary me-2" data-bs-toggle="modal" data-bs-target="#modalReservation">Nouvelle reservation</button>
<button class="btn btn-lg btn-primary me-2" data-bs-toggle="modal" data-bs-target="#modalReservation">Nouvelle réservation</button>
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#modalReservationAttente">
Validation en attente <span class="badge badge-light">{{ nb_reservation_attente }}</span>
</button>
@@ -55,5 +55,5 @@
{% block 'js' %}
<script src="https://cdn.jsdelivr.net/npm/lemonadejs/dist/lemonade.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@calendarjs/ce/dist/index.min.js"></script>
<script src="{% static 'gestion_salle/js/index.js' %}"></script>
<script src="{% static 'gestion_salle/js/index.js' %}?v=5"></script>
{% endblock %}

View File

@@ -4,7 +4,7 @@
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modalReservationLabel">
Nouvelle reservation
Nouvelle réservation
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>

View File

@@ -5,14 +5,14 @@
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modalDetailReservationLabel">
Détails de la reservation (<span id='statut-reservation'></span>)
Détails de la réservation (<span id='statut-reservation'></span>)
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST" data-urlannuler="{% url 'gestion_salle:annuler-reservation' %}" data-urlvalider="{% url 'gestion_salle:valider-reservation' %}" id="formulaire-details">
<form method="POST" data-urlvalider="{% url 'gestion_salle:valider-reservation' %}" id="formulaire-details">
{% csrf_token %}
<input type='hidden' class="form-control" id="id_reservation_detail" name='id_reservation'>
<input type='' class="form-control" id="id_reservation_detail" name='id_reservation' hidden>
<div class="form-group mb-2">
<label>Employé :</label>
<input class="form-control" id="employe" readonly>
@@ -22,12 +22,8 @@
<input class="form-control" id="salle" readonly>
</div>
<div class="form-group mb-2">
<label>Date de debut :</label>
<input type='date' class="form-control" id="date_debut" readonly >
</div>
<div class="form-group mb-2">
<label>Date de fin :</label>
<input type='date' class="form-control" id="date_fin" readonly >
<label>Date de l'évènement :</label>
<input type='date' class="form-control" id="date_evenement" readonly >
</div>
<div class="form-group mb-2">
<label>Heure de début :</label>
@@ -38,7 +34,7 @@
<input type='time' class="form-control" id="heure_fin" readonly >
</div>
<div class="form-group mb-2">
<label>Motif de la reservation :</label>
<label>Motif de la réservation :</label>
<textarea class="form-control" id="motif_reservation" readonly ></textarea>
</div>
<div class="form-check mb-2">
@@ -50,19 +46,35 @@
<input type="url" class="form-control" id="lien_zoom" readonly >
</div>
<div class="form-check mb-2">
<label label="form-check-label">Besoin d'un ordinateur</label>
<label class="form-check-label">Besoin d'un ordinateur {{ reservation.statut }}</label>
<input type="checkbox" class="form-check-input" id="besoin_ordinateur" readonly >
</div>
<div class='d-flex justify-content-around mt-2'>
{% if appartient_au_departement_informatique %}
<button class="btn btn-primary" id="ajoutZoom">Ajout du lien zoom</button>
<hr />
<div class='d-flex justify-content-center mt-2'>
{% if est_equipe_informatique %}
<button class="btn btn-primary me-1" id="ajoutZoom">Ajout du lien zoom</button>
{% endif %}
{% 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>
{% if est_assistante_direction %}
<button class="btn btn-danger me-1" id="refuserReservation" data-lienrefus="{% url 'gestion_salle:refuser-reservation' %}">Refuser</button>
<button class="btn btn-success me-1" id="bouton-valider">Valider</button>
{% endif %}
<span id="current-user-id" data-userid="{{ request.user.id }}"></span>
<button class="btn btn-danger" id="bouton-annuler">Annuler</button>
{% if est_equipe_informatique %}
<button class="btn btn-danger me-1" id="refuserReservationInfo" data-lienrefus="{% url 'gestion_salle:refuser-reservation' %}">Refuser</button>
<button class="btn btn-success me-1" id="bouton-valider-info">Valider</button>
{% endif %}
<span id="current-user-id" data-user-id="{{ user.id }}"></span>
<button
type="button"
class="btn btn-outline-danger d-none"
id="bouton-supprimer-reservation"
data-url="{% url 'gestion_salle:supprimer-reservation' 0 %}"
>
Supprimer ma demande
</button>
<button type="button" class="btn btn-secondary" id="bouton-annuler" data-bs-dismiss="modal">
Fermer
</button>
</div>
</form>
</div>

View File

@@ -4,7 +4,7 @@
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modalReservationAttenteLabel">
Liste des reservation en attente de validation
Liste des réservation en attente de validation
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
app_name = "gestion_salle"
@@ -44,5 +45,10 @@ urlpatterns = [
'liste-reservation-attente',
views.liste_reservation_attente,
name="liste-reservation-attente"
),
path(
'reservation/<int:reservation_id>/supprimer',
views.supprimer_reservation,
name="supprimer-reservation",
)
]

View File

@@ -1,25 +1,32 @@
from datetime import timedelta
import json
from datetime import timedelta
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from django.http import JsonResponse, HttpRequest
from django.db.models import Q
from django.forms import model_to_dict
from django.http import HttpRequest, JsonResponse
from django.shortcuts import redirect, render
from fonction_utilitaire import fonctions_utilitaire
from gestion_employe.models import Employe
from gestion_salle.forms import ReservationForm
from .models import Reservation
@login_required
def index(request: HttpRequest):
"""Vue d'affichage de la page d'accueil de la gestion des salles"""
try:
employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist:
messages.error(request, "Profil employé introuvable.")
return redirect('gestion_conges:conge')
est_equipe_info = fonctions_utilitaire.est_equipe_informatique(employe)
est_assistante = fonctions_utilitaire.est_assistante_direction(employe)
if request.method == "POST":
form = ReservationForm(request.POST)
@@ -34,11 +41,11 @@ def index(request: HttpRequest):
besoin_ordi = form.cleaned_data['besoin_ordi']
if date_fin < date_debut:
messages.error(request, "Date fin invalide.")
messages.error(request, "La date de fin est antérieure à la date de début.")
return redirect('gestion_salle:reservation-salle')
if heure_fin <= heure_debut:
messages.error(request, "Heure invalide.")
messages.error(request, "L'heure de fin est antérieure à l'heure de début.")
return redirect('gestion_salle:reservation-salle')
if not request.user.first_name.strip() or not request.user.last_name.strip():
@@ -51,8 +58,9 @@ def index(request: HttpRequest):
created = []
current_date = date_debut
while current_date <= date_fin:
print(f"Création de réservations pour {employe}")
while current_date <= date_fin:
reservation = Reservation.objects.create(
employe=employe,
date_debut=current_date,
@@ -71,20 +79,35 @@ def index(request: HttpRequest):
messages.success(request, "Réservation(s) créée(s) avec succès.")
if fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION:
# Nouvelle logique d'envoi de mails à la création :
# 1. Une demande de salle (formation/réunion) est envoyée à l'assistante de direction pour validation.
# - Si elle nécessite un lien Zoom ou un ordinateur, l'équipe informatique sera notifiée
# seulement après la validation par l'assistante.
# 2. Une demande de lien Zoom directement est envoyée à l'équipe informatique.
if salle != "lien_zoom" and fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION:
fonctions_utilitaire.envoyer_mail(
sujet = "Reservation de salle",
message = f"""
sujet="Demande de réservation de salle",
message=f"""Bonjour,
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}".
{"Cette demande nécessite un lien Zoom et/ou un ordinateur et sera traitée par l'équipe informatique après votre validation." if besoin_zoom or besoin_ordi else ""}
Veuillez vous connecter à la plateforme pour plus de détails.""",
destinataires = fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION
destinataires=fonctions_utilitaire.EMAIL_ASSISTANTE_DE_DIRECTION
)
if salle == "lien_zoom" and fonctions_utilitaire.EMAIL_EQUIPE_INFORMATIQUE:
fonctions_utilitaire.envoyer_mail(
sujet="Demande de lien Zoom",
message=f"""Bonjour,
Une nouvelle demande de lien Zoom a été effectuée par {employe.user.first_name} {employe.user.last_name} du {date_debut.strftime('%d/%m/%Y')} au {date_fin.strftime('%d/%m/%Y')} de {heure_debut.strftime('%H:%M')} à {heure_fin.strftime('%H:%M')} pour le motif {motif}.
Veuillez vous connecter à la plateforme pour traiter cette demande.""",
destinataires=fonctions_utilitaire.EMAIL_EQUIPE_INFORMATIQUE
)
return redirect('gestion_salle:reservation-salle')
formulaire_reservation = ReservationForm()
departement = employe.departement
appartient_direction = request.user.groups.filter(name='direction').exists()
formulaire_reservation = ReservationForm()
# departement = employe.departement
liste_demande_reservation = Reservation.objects.filter(
employe=employe,
@@ -94,28 +117,33 @@ Veuillez vous connecter à la plateforme pour plus de détails.""",
context = {
'formulaire_reservation': formulaire_reservation,
'nb_reservation_attente': Reservation.objects.filter(statut='en_attente').count(),
'appartient_au_departement_informatique': departement and departement.nom == "Systeme informatique",
'appartient_direction': appartient_direction,
'appartient_au_departement_informatique': est_equipe_info,
'est_assistante_direction': est_assistante,
'est_equipe_informatique': est_equipe_info,
'liste_demande_reservation': list(liste_demande_reservation),
}
return render(request, "gestion_salle/index.html", context)
@login_required
def liste_reservation(request:HttpRequest):
"""Vue d'affichage des creneaux disponibles"""
reservations = Reservation.objects.filter(statut = "validee")
try:
employe = Employe.objects.get(user=request.user)
reservations = Reservation.objects.filter(
Q(statut="validee")
| Q(statut="en_attente", employe=employe)
)
except Employe.DoesNotExist:
reservations = Reservation.objects.filter(statut="validee")
liste_reservation = []
for reservation in reservations:
color = None
if reservation.statut == "en_attente":
color = "#ffc107"
elif reservation.statut == "validee":
color = "#198754"
else:
color = "#dc3545"
print(dict(Reservation.TYPE_CHOICES).get(reservation.salle))
color = "#198754"
liste_reservation.append({
"guid": reservation.pk,
@@ -124,34 +152,106 @@ def liste_reservation(request:HttpRequest):
"start": reservation.heure_debut,
"end": reservation.heure_fin,
"color": color,
"statut": dict(Reservation.STATUT).get(reservation.statut),
})
return JsonResponse(liste_reservation, safe=False)
@login_required
def liste_reservation_attente(request):
"""Liste des réservations en attente, filtrée selon le profil :
- Assistante de direction : voit les salles à valider (pas les liens choisis).
- Équipe informatique : voit les liens choisis et les salles demandant zoom/ordi non traités.
"""
try:
employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist:
return JsonResponse({"success": False, "message": "Profil employé introuvable"})
est_informatique = fonctions_utilitaire.est_equipe_informatique(employe)
est_assistante = fonctions_utilitaire.est_assistante_direction(employe)
reservations = Reservation.objects.filter(statut="en_attente")
if est_assistante:
# L'assistante voit les demandes de salle (formation / réunion)
# Elle ne reçoit pas les demandes de simple lien (lien_zoom) qui vont à l'équipe info.
reservations = reservations.exclude(salle="lien_zoom")
elif est_informatique:
# L'équipe informatique voit :
# - les demandes de lien zoom directs
# - les demandes de salle avec besoin_zoom ou besoin_ordi (déjà validés par l'assistance)
# pour lesquelles le lien_zoom n'a pas encore était fourni (lien_zoom vide)
reservations = reservations.filter(
Q(salle="lien_zoom") | Q(besoin_zoom=True) | Q(besoin_ordi=True)
)
print("Test :", [reservation.employe for reservation in reservations])
liste_reservation = [
{
**model_to_dict(reservation),
"id": reservation.pk,
"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),
'statut': dict(Reservation.STATUT).get(reservation.statut),
"date_evenement": reservation.date_debut.strftime('%Y-%m-%d'),
} for reservation in reservations
]
return JsonResponse(liste_reservation, safe=False)
def detail_reservation(request:HttpRequest, reservation_id:int):
@login_required
def supprimer_reservation(request: HttpRequest, reservation_id: int):
if request.method != "POST":
return JsonResponse(
{"success": False, "message": "Méthode non autorisée."},
status=405,
)
try:
employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist:
return JsonResponse(
{"success": False, "message": "Profil employé introuvable."},
status=403,
)
try:
reservation = Reservation.objects.get(
pk=reservation_id,
employe=employe,
)
except Reservation.DoesNotExist:
return JsonResponse(
{"success": False, "message": "Réservation introuvable ou accès non autorisé."},
status=404,
)
if reservation.statut != "en_attente":
return JsonResponse(
{
"success": False,
"message": "Seules les réservations en attente peuvent être supprimées.",
},
status=400,
)
reservation.delete()
return JsonResponse(
{"success": True, "message": "Votre réservation en attente a été supprimée."}
)
def detail_reservation(request:HttpRequest, reservation_id:int):
reservation = Reservation.objects.get(id=reservation_id)
employe = reservation.employe.user
reservation_json = {
'id_reservation': reservation_id,
'employe': f"{employe.first_name} {employe.last_name}",
'salle': reservation.salle,
'salle': dict(Reservation.TYPE_CHOICES).get(reservation.salle),
'statut': dict(Reservation.STATUT).get(reservation.statut),
'date_evenement': reservation.date_debut.strftime('%Y-%m-%d'),
'heure_debut': reservation.heure_debut.strftime('%H:%M'),
@@ -160,55 +260,107 @@ def detail_reservation(request:HttpRequest, reservation_id:int):
'besoin_zoom': reservation.besoin_zoom,
'besoin_ordinateur': reservation.besoin_ordi,
'lien_zoom': reservation.lien_zoom or '',
'est_proprietaire': request.user.is_authenticated and reservation.employe.user_id == request.user.id,
'est_en_attente': reservation.statut == "en_attente",
}
return JsonResponse(reservation_json, safe=True)
@login_required
def ajouter_lien_zoom(request:HttpRequest):
"""Vue de gestion de l'ajout du lien zoom"""
"""Traitement de la réservation par l'équipe informatique :
- Ajoute lien zoom si demandé.
- Valide une réservation qui exigeait un traitement par l'équipe informatique.
- Envoie un mail à toutes les équipes et au demandeur après validation.
"""
reservation = None
if request.method == 'POST':
reservation_id= request.POST['id_reservation']
reservation_id = request.POST.get('id_reservation')
try:
reservation = Reservation.objects.get(id=reservation_id)
except reservation.DoesNotExist:
messages.error(request, "La resevertion selectionné n'existe pas.")
return redirect("salle")
if not (reservation.besoin_zoom or reservation.salle == "lien_zoom"):
messages.error(request, "Cette réservation ne nécessite pas de lien Zoom.")
return redirect('gestion_salle:reservation-salle')
elif reservation.statut in ["annulee", "refusee"]:
messages.error(request, "Le lien Zoom ne peut être ajouté pour les réservations annulée ou refusée.")
except (Reservation.DoesNotExist, ValueError):
messages.error(request, "La réservation sélectionnée n'existe pas.")
return redirect('gestion_salle:reservation-salle')
if request.method == "POST":
try:
employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist:
messages.error(request, "Profil employé introuvable.")
return redirect('gestion_salle:reservation-salle')
# Vérification : seule l'équipe informatique peut traiter ces demandes
if not fonctions_utilitaire.est_equipe_informatique(employe):
messages.error(request, "Vous n'êtes pas autorisé à traiter cette demande.")
return redirect('gestion_salle:reservation-salle')
if reservation.salle == "lien_zoom" or reservation.besoin_zoom:
lien = request.POST.get("lien_zoom")
if not lien:
messages.error(request, "Veuillez fournir un lien Zoom.")
return redirect('gestion_salle:reservation-salle')
reservation.lien_zoom = lien
reservation.statut = "validee"
reservation.save()
messages.success(request, "Le lien Zoom a été ajouté et la réservation a été validée. ")
messages.success(request, "La demande a été traitée et validée. ")
# Mail au demandeur
fonctions_utilitaire.envoyer_mail(
sujet="Réservation traitée",
message=f"""Bonjour {reservation.employe.user.first_name} {reservation.employe.user.last_name}, votre demande de {dict(Reservation.TYPE_CHOICES).get(reservation.salle)}{' avec lien Zoom' if reservation.lien_zoom else ''} a été traitée. Veuillez vous connecter à la plateforme pour plus de détails.""",
destinataires=[reservation.employe.user.email]
)
# Mail aux emails de l'équipe informatique (hors utilisateur courant)
autres_membres = Employe.objects.filter(departement__nom="Systeme informatique").exclude(id=employe.id).values_list('user__email', flat=True)
if autres_membres:
fonctions_utilitaire.envoyer_mail(
sujet="Réservation validée par l'équipe informatique",
message=f"""Bonjour,
La réservation {dict(Reservation.TYPE_CHOICES).get(reservation.salle)} du {reservation.date_debut.strftime('%d/%m/%Y')} pour {reservation.employe.user.first_name} {reservation.employe.user.last_name} a été traitée et validée par {employe.user.first_name} {employe.user.last_name}.\n\nVeuillez vous connecter à la plateforme pour plus de détails.""",
destinataires=list(autres_membres)
)
return redirect('gestion_salle:reservation-salle')
@login_required
def annuler_reservation(request:HttpRequest):
"""Vue de gestion de l'annulation de la reservation"""
"""Vue de gestion de l'annulation de la reservation, réservée à l'employé demandeur ou aux validateurs."""
if request.method == 'POST':
reservation_id= request.POST['id_reservation']
try:
reservation = Reservation.objects.get(id=reservation_id)
except Reservation.DoesNotExist:
messages.error(request, "La resevertion selectionné n'existe pas.")
return redirect("salle")
messages.error(request, "La reservation selectionné n'existe pas.")
return redirect("gestion_salle:reservation-salle")
try:
employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist:
messages.error(request, "Profil employé introuvable.")
return redirect("gestion_salle:reservation-salle")
# Seul l'employé demandeur ou un validateur (assistante / équipe info) peut annuler
est_demandeur = reservation.employe == employe
est_validateur = fonctions_utilitaire.est_assistante_direction(employe) or fonctions_utilitaire.est_equipe_informatique(employe)
if not est_demandeur and not est_validateur:
messages.error(request, "Vous n'êtes pas autorisé à annuler cette réservation.")
return redirect('gestion_salle:reservation-salle')
reservation.statut = 'annulee'
reservation.save()
messages.success(request, "Votre réservation a été annulée.")
messages.success(request, "La réservation a été annulée.")
return redirect('gestion_salle:reservation-salle')
@login_required
def valider_reservation(request: HttpRequest):
"""Validation d'une réservation"""
"""Validation d'une réservation selon le profil :
- Assistante de direction : valide les demandes de salles (sauf lien_zoom) ;
- Équipe informatique : valide les demandes de lien_zoom et les
demandes de salle avec besoin zoom/ordi restées en attente après la première correction.
"""
if request.method == 'POST':
reservation_id = request.POST.get('id_reservation')
@@ -216,38 +368,121 @@ def valider_reservation(request: HttpRequest):
reservation = Reservation.objects.get(id=reservation_id)
except Reservation.DoesNotExist:
messages.error(request, "La réservation sélectionnée n'existe pas.")
return redirect("salle")
return redirect('gestion_salle:reservation-salle')
reservation.statut = 'validee'
reservation.save()
try:
employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist:
messages.error(request, "Profil employé introuvable.")
return redirect('gestion_salle:reservation-salle')
est_assistante = fonctions_utilitaire.est_assistante_direction(employe)
est_equipe_info = fonctions_utilitaire.est_equipe_informatique(employe)
if est_assistante:
# L'assistante ne doit pas traiter les simple lien_zoom
if reservation.salle == "lien_zoom":
messages.error(request, "Les demandes de lien Zoom sont traitées par l'équipe informatique.")
return redirect('gestion_salle:reservation-salle')
if reservation.besoin_zoom or reservation.besoin_ordi:
# La demande reste en attente, l'équipe informatique doit être notifiée
# L'assistante a donné sa validation "salle", on garde le statut "en_attente"
# pour que l'équipe informatique puisse traiter, et on envoie le mail à l'équipe.
if fonctions_utilitaire.EMAIL_EQUIPE_INFORMATIQUE:
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]
sujet="Réservation de salle à traiter par l'équipe informatique",
message=f"""Bonjour,
L'assistante de direction a validé la réservation de la salle {dict(Reservation.TYPE_CHOICES).get(reservation.salle)} pour {reservation.employe.user.first_name} {reservation.employe.user.last_name} le {reservation.date_debut.strftime('%d/%m/%Y')} (de {reservation.heure_debut.strftime('%H:%M')} à {reservation.heure_fin.strftime('%H:%M')}).
Cette demande nécessite {"un lien zoom" if reservation.besoin_zoom else ""}{" - un ordinateur" if reservation.besoin_ordi else ""}.
Veuillez vous connecter à la plateforme pour traiter cette demande.""",
destinataires=fonctions_utilitaire.EMAIL_EQUIPE_INFORMATIQUE
)
messages.success(request, f"La réservation de {request.user.first_name} {request.user.last_name} validée avec succès.")
messages.success(request, "Réservation validée, une demande a été envoyée à l'équipe informatique.")
reservation.statut = "en_attente" # reste en attente pour l'équipe info
reservation.save()
else:
# Validation directe sans traitement informatique
reservation.statut = "validee"
reservation.save()
fonctions_utilitaire.envoyer_mail(
sujet="Réservation de salle validée",
message=f"""Bonjour {reservation.employe.user.first_name} {reservation.employe.user.last_name}, votre réservation de la {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]
)
elif est_equipe_info:
# L'équipe informatique traite le reste : lien_zoom et besoins info
if reservation.salle == "lien_zoom" or reservation.besoin_zoom or reservation.besoin_ordi:
if reservation.besoin_zoom and not reservation.lien_zoom:
# La demande exige un lien zoom qui n'a pas été encore rempli,
# il est préférable de passer par ajouter_lien_zoom. On laisse traiter.
messages.success(request, "Veuillez ajouter le lien Zoom via le champs dédié.")
return redirect('gestion_salle:reservation-salle')
reservation.statut = "validee"
reservation.save()
# Mail au demandeur + autres membres de l'équipe
fonctions_utilitaire.envoyer_mail(
sujet="Réservation validée",
message=f"""Bonjour {reservation.employe.user.first_name} {reservation.employe.user.last_name}, votre demande de {dict(Reservation.TYPE_CHOICES).get(reservation.salle)} a été traitée et validée. Veuillez vous connecter pour plus de détails.""",
destinataires=[reservation.employe.user.email]
)
autres = Employe.objects.filter(departement__nom="Systeme informatique").exclude(id=employe.id).values_list('user__email', flat=True)
if autres:
fonctions_utilitaire.envoyer_mail(
sujet="Réservation traitée par un collègue",
message=f"""Bonjour {request.user.first_name} {request.user.last_name}, la demande de {dict(Reservation.TYPE_CHOICES).get(reservation.salle)} du {reservation.date_debut.strftime('%d/%m/%Y')} pour {reservation.employe.user.first_name} {reservation.employe.user.last_name} a été traitée et validée par {employe.user.first_name} {employe.user.last_name}. Veuillez vous connecter pour plus de détails.""",
destinataires=list(autres)
)
else:
messages.error(request, "Cette réservation ne relève pas de l'équipe informatique.")
return redirect('gestion_salle:reservation-salle')
else:
messages.error(request, "Vous n'êtes pas autorisé à effectuer cette validation.")
return redirect('gestion_salle:reservation-salle')
messages.success(request, f"La réservation de {request.user.first_name} {request.user.last_name} a été 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 = json.loads(request.body).get('id_reservation')
def refuser_reservation(request:HttpRequest):
"""Vue de gestion de refus de la reservation, autorisée à l'assistant de direction
et l'équipe informatique selon le type de demande."""
data = json.loads(request.body)
reservation_id = data.get("id_reservation")
try:
reservation = Reservation.objects.get(id=reservation_id)
except Reservation.DoesNotExist:
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 JsonResponse({"message": "La reservation selectionné n'existe pas."})
reservation.statut = 'refusee'
try:
employe = Employe.objects.get(user=request.user)
except Employe.DoesNotExist:
return JsonResponse({"message": "Profil employé introuvable."})
est_assistante = fonctions_utilitaire.est_assistante_direction(employe)
est_equipe_info = fonctions_utilitaire.est_equipe_informatique(employe)
peut_refuser = False
if est_assistante and reservation.salle != "lien_zoom":
peut_refuser = True
elif est_equipe_info and (reservation.salle == "lien_zoom" or reservation.besoin_zoom or reservation.besoin_ordi):
peut_refuser = True and (not reservation.besoin_zoom or reservation.lien_zoom)
if not peut_refuser:
return JsonResponse({"message": "Vous n'êtes pas autorisé à refuser cette réservation."})
reservation.statut = "refusee"
reservation.save()
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.""",
sujet = "Réservation refusée",
message = f"""Bonjour {reservation.employe.user.first_name} {reservation.employe.user.last_name}, votre demande de réservation du {reservation.date_debut.strftime('%d/%m/%Y')} de {reservation.date_debut.strftime('%d/%m/%Y')} de {reservation.heure_debut.strftime('%H:%M')} à {reservation.heure_fin.strftime('%H:%M')} a été refusée.""",
destinataires = [reservation.employe.user.email]
)

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class GestionCongeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gestion_conge'

View File

@@ -3,7 +3,6 @@ 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",
columns: [
{"title": "Nom et Prénom", "field": "prenom_nom"},
{"title": "Date de début", "field": "date_debut", formatter:"datetime", formatterParams:{

View File

@@ -9,7 +9,6 @@ const tableau_liste_employe = new Tabulator("#tableau_liste_employe", {
{"title": "Projet", "field": "projet"},
{"title": "Téléphone", "field": "telephone"},
],
// ajaxURL: url_liste_employe,
pagination: true,
paginationSize: 10,
responsiveLayout : "hide"
@@ -19,7 +18,6 @@ fetch(url_liste_employe)
.then(response => response.json())
.then(data => {
if (data.success){
console.log(data.data)
tableau_liste_employe.setData(data.data);
}else{
alert(data.message);
@@ -29,6 +27,7 @@ fetch(url_liste_employe)
tableau_liste_employe.on("rowClick", function (row, rowData) {
const data = rowData.getData();
document.getElementById('detail-id').value = data.id;
document.getElementById('detail-matricule').value = data.matricule;
document.getElementById('detail-employe').value = data.employe;
@@ -211,23 +210,31 @@ $("enregistrerDetail").addEventListener("click", function() {
const fonction = document.getElementById('detail-fonction').value;
const dateEmbauche = document.getElementById('detail-dateEmbauche').value;
const matricule = document.getElementById('detail-matricule').value;
const departement = document.getElementById('detail-departement').value;
const url_enregistrer_detail = document.getElementById("modalDetailEmployeBody").dataset.url;
console.log("Le lien de l'enregistrement :", document.getElementById("modalDetailEmployeBody").dataset)
const payload = {
"id": id_,
"fonction": fonction,
"date_embauche": dateEmbauche,
"matricule": matricule,
"departement": departement
};
const url_enregistrer_detail = $("modalDetailEmployeBody").dataset.url;
fetch(url_enregistrer_detail, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
"id": id_,
"fonction": fonction,
"date_embauche": dateEmbauche,
"matricule": matricule
})
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
console.log('Réponse du serveur:', data);
alert(data.message);
location.reload();
})
@@ -262,3 +269,69 @@ const listeContratExpirant = new Tabulator("#listeContratExpirant", {
],
ajaxURL: $("boutonContratExpirants").dataset.urlexpirants,
})
/* let tabContrats = null;
document.getElementById("modalContratsEmploye").addEventListener("show.bs.modal", function () {
const employeId = document.getElementById("detail-id").value;
if (!employeId) {
alert("Aucun employé sélectionné");
return;
}
fetch(`/employé/employe/${employeId}/contrats/`)
.then(response => {
if (!response.ok) {
throw new Error("Erreur HTTP " + response.status);
}
return response.json();
})
.then(data => {
document.getElementById("nom-employe-contrat").innerText =
data.employe;
if (tabContrats) {
tabContrats.destroy();
}
tabContrats = new Tabulator(
"#table-contrats-tabulator",
{
data: data.contrats,
layout: "fitColumns",
placeholder: "Aucun contrat",
columns: [
{ title: "Numéro", field: "numero_contrat" },
{ title: "Type", field: "type_contrat" },
{ title: "Date début", field: "date_debut" },
{ title: "Date fin", field: "date_fin" },
{ title: "Salaire", field: "salaire_mensuel" },
{ title: "Statut", field: "statut" },
{
title: "Contrat",
field: "fichier",
formatter: function(cell) {
const url = cell.getValue();
if (!url) {
return "<span class='text-danger'>Aucun fichier</span>";
}
return `<a href="${url}" target="_blank" class="btn btn-sm btn-primary">
<i class="bi bi-file-earmark-pdf"></i> Ouvrir
</a>`;
}
}
]
}
);
})
.catch(error => {
console.error("Erreur :", error);
});
}); */

View File

@@ -13,18 +13,20 @@ const tableau_certificat = new Tabulator("#tableau-certificat", {
inputFormat:"yyyy-MM-dd",
outputFormat:"dd/MM/yy",
}},
{"title": "Fichier", "field": "certificat", formatter:"link", formatterParams:{
label:"Voir le fichier",
target:"_blank",
}},
],
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);
@@ -32,6 +34,10 @@ enregistrerProfil.addEventListener("click", (e) => {
formData.append("adresse", $("adresse").value);
formData.append("sexe", $("sexe").value);
formData.append("date_naissance", $("date_naissance").value);
const photoInput = $("photo");
if (photoInput.files.length > 0) {
formData.append("photo", photoInput.files[0]);
}
fetch(url, {
method: "POST",

View File

@@ -23,9 +23,8 @@ btnEnregistrerBailleur.addEventListener('click', function() {
});
});
document.addEventListener("DOMContentLoaded", function () {
table = new Tabulator("#table-bailleurs", {
table = new Tabulator("#table-bailleurs", {
ajaxURL: "/gestion-projet/bailleurs/",
layout: "fitColumns",
pagination: "local",
@@ -45,7 +44,4 @@ document.addEventListener("DOMContentLoaded", function () {
supprimerBailleur(data.id);
}
}
});
});

View File

@@ -1,5 +1,6 @@
const $ = (element) => document.getElementById(element)
const url_liste_projet = $("tableau-liste-projet").dataset.url;
const tableau_liste_projet = new Tabulator("#tableau-liste-projet", {
fitColumns: true,
responsiveLayout : true,
@@ -39,7 +40,6 @@ tableau_liste_projet.on("rowClick", function (row, rowData) {
$("detail_date_debut").value = data.date_debut;
$("detail_date_fin").value = data.date_fin;
$("detail_numero_convention").value = data.numero_convention;
$("detail_type_projet").value = data.type_projet;
Array.from($("detail_domaine_recherche").options).forEach(option => {
if (data.domaine_recherche.includes(option.value)) {
option.selected = true;

View File

@@ -12,7 +12,12 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
const modal = new bootstrap.Modal($("modalDetailReservation"));
modal.show();
fetch (`/gestion-salle/revervation/details/${event.guid}`)
.then(response => response.json())
.then(response => {
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
currentReservationId = data.id_reservation;
$("id_reservation_detail").value = data.id_reservation;
@@ -29,11 +34,18 @@ const calendrier = Schedule(document.getElementById('planning-reservation'), {
$("besoin_ordinateur").checked=data.besoin_ordinateur;
$("lien_zoom").value=data.lien_zoom;
const zoomContainer = $("lien_zoom_container");
if (!data.besoin_zoom) {
zoomContainer.classList.add("d-none");
} else {
zoomContainer.classList.remove("d-none");
}
if (data.besoin_zoom === false){
$("lien_zoom_container").className = "d-none";
}
})
.catch(error => console.error("Impossible de charger les détails de la réservation :", error));
}
});
@@ -75,12 +87,11 @@ document.addEventListener("DOMContentLoaded", function () {
})
$("bouton-annuler").addEventListener("click", (e) => {
e.preventDefault();
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(
@@ -129,6 +140,8 @@ if($("ajoutZoom")){
if($("refuserReservation")){
$("refuserReservation").addEventListener("click", (e) => {
e.preventDefault();
const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
const url = e.currentTarget.dataset.lienrefus;
const idRes = $("id_reservation_detail").value;
@@ -155,7 +168,7 @@ const tableau_reservation_attente = new Tabulator("#tableau-reservation-attente"
columns: [
{title: "Employé", field: "employe"},
{title: "Salle", field: "salle"},
{title: "Date de l'evenement", field: "date_debut"},
{title: "Date de l'evenement", field: "date_evenement"},
{title: "Heure de début", field: "heure_debut"},
{title: "Heure de fin", field: "heure_fin"},
{title: "Motif de reservation", field: "motif_reservation"},
@@ -167,11 +180,14 @@ 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';
}
const id_user = $("current-user-id").dataset.userid;
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;
@@ -179,8 +195,7 @@ tableau_reservation_attente.on("rowClick", (row, rowData) => {
$("employe").value=data.employe;
$("salle").value=data.salle;
$("statut-reservation").innerHTML=data.statut;
$("date_debut").value = data.date_debut;
$("date_fin").value = data.date_fin;
$("date_evenement").value = data.date_evenement;
$("heure_debut").value=data.heure_debut;
$("heure_fin").value=data.heure_fin;
$("motif_reservation").value=data.motif_reservation;
@@ -188,6 +203,14 @@ tableau_reservation_attente.on("rowClick", (row, rowData) => {
$("besoin_ordinateur").checked=data.besoin_ordi;
$("lien_zoom").value=data.lien_zoom;
const refuserReservation = document.querySelector("#refuserReservation");
const bouton_valider = document.querySelector("#bouton-valider");
if((refuserReservation | bouton_valider) && data.statut === "En attente"){
refuserReservation.style.display = "inline-block";
bouton_valider.style.display = "inline-block";
}
if (id_user != data.employe_id){
$("bouton-annuler").className = "d-none";
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,278 +0,0 @@
/* === Print === */
@media print {
nav, .btn, .sidebar, .form-select, .form-control {
display: none !important;
}
body {
font-size: 12pt;
color: black;
}
table {
border-collapse: collapse;
}
}
@media (max-width: 768px) {
main {
margin-left: 0;
}
}
/* === Body & Container === */
body {
background-color: #e4dccc;
background-attachment: fixed;
}
.dashboard-container {
background-color: #ffffff;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
border-radius: 8px;
padding: 20px;
margin: 30px auto;
max-width: 1200px;
}
.sidebar {
position: fixed;
width: 220px;
}
main {
margin-left: 220px;
}
/* === Sidebar === */
.sidebar {
background: linear-gradient(180deg, #b35400, #993d00); /* dégradé orange foncé */
color: white;
min-height: 100vh; /* prend toute la hauteur */
top: 0;
z-index: 1000;
overflow-y: auto;
padding: 1rem;
}
.sidebar .nav-link {
color: white;
font-weight: 700;
}
.sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 5px;
color: white;
}
.sidebar .dropdown-menu {
display: none;
position: relative;
left: 10px;
top: -5px;
border: none;
padding-left: 1rem;
box-shadow: none;
}
.sidebar .dropdown:hover .dropdown-menu {
display: block;
}
.sidebar a {
color: white;
}
.sidebar a:hover {
color: white;
}
/* === Navbar / Logo / Profil === */
.logo {
max-width: 80px;
}
.img-profil {
width: 80px !important;
height: 80px !important;
object-fit: cover;
border-radius: 50%;
}
/* === Cards === */
.card {
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border-radius: 8px;
transition: transform 0.3s ease;
}
.card:hover {
transform: scale(1.02);
}
.card-header {
background-color: #f8f9fa;
font-weight: bold !important;
color: black !important;
font-size: 1rem;
border-bottom: none;
}
.card-body h5 {
color: black !important;
font-weight: bold;
font-size: 2.2rem;
}
/* === Stat cards === */
.stat-card {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(113, 11, 11, 0.1);
}
/* === Employé / Projet details === */
.employe-detail {
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.employe-detail .card-title {
font-size: 1.8rem;
margin-bottom: 0.5rem;
}
.employe-detail p {
margin-bottom: 0.4rem;
font-size: 1rem;
}
.employe-detail a.btn {
font-weight: 500;
}
.employe-card, .projet-card {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 2.5rem;
max-width: 1400px;
margin: 0 auto;
font-size: 1.1rem;
}
/* === Forms === */
.form-wrapper {
max-width: 1000px;
margin: auto;
background-color: rgba(255, 255, 255, 0.95);
}
form label {
font-weight: 600;
margin-top: 1rem;
display: block;
}
form input, form select, form textarea {
width: 100%;
padding: 0.5rem;
margin-top: 0.3rem;
border-radius: 5px;
border: 1px solid #ccc;
}
/* === Formation badges === */
.formation-badge {
position: relative;
display: inline-block;
}
.formation-actions {
position: absolute;
top: -5px;
right: -5px;
display: none;
}
.formation-badge:hover .formation-actions {
display: inline-block;
}
.formation-actions button {
background: none;
border: none;
color: #fff;
margin-left: 5px;
font-size: 0.9rem;
}
/* === Context menu === */
#contextMenu {
width: 150px;
}
/* === Transitions / Animations === */
.fade-out {
opacity: 0;
transition: opacity 0.5s ease;
}
#main-content {
transition: opacity 0.3s ease;
}
.login-box {
background: linear-gradient(to right, #f5f7fa, #c3cfe2);
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
width: 100%;
max-width: 400px;
}
/* === Responsive === */
@media (max-width: 576px) {
.modal-dialog {
max-width: 100%;
margin: 0;
height: 100%;
}
.modal-content {
height: 100%;
border-radius: 0;
}
.modal-body {
overflow-y: auto;
}
.modal-footer button {
width: 100%;
}
.img-profil {
width: 50px;
height: 50px;
}
.sidebar {
width: 100%;
padding: 0.5rem 1rem;
}
.sidebar .nav-link {
font-size: 0.9rem;
padding: 0.5rem 1rem;
}
}