Veuillez lire et accepter notre politique d'utilisation avant d'accéder à votre compte.
+
Confidentialité des données
+
Vos informations personnelles et professionnelles sont protégées. Toute divulgation non autorisée est strictement interdite.
+
Utilisation autorisée
+
L’application est réservée à un usage professionnel. Toute utilisation à des fins personnelles ou non autorisées est prohibée.
+
Sécurité des comptes
+
Ne partagez jamais vos identifiants. Changez votre mot de passe régulièrement et en cas de suspicion d’intrusion.
+
Responsabilités de l’utilisateur
+
Vous êtes responsable des actions effectuées via votre compte. Signalez toute anomalie ou problème à l’équipe RH ou à l’administrateur.
+
Acceptation et conformité
+
En cliquant sur “J’accepte”, vous confirmez avoir lu et accepté cette politique. Le non-respect peut entraîner une suspension ou une révocation de l’accès.
\ No newline at end of file
diff --git a/SIRH/urls.py b/SIRH/urls.py
new file mode 100644
index 0000000..50b9078
--- /dev/null
+++ b/SIRH/urls.py
@@ -0,0 +1,67 @@
+"""
+URL configuration for SIRH project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/5.2/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+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.contrib import admin
+from django.urls import include, path
+from django.conf.urls.static import static
+from django.conf import settings
+# from simple_sso.sso_server.server import Server
+
+from . import views
+
+# server_sso = Server()
+
+urlpatterns = [
+ path(
+ '',
+ views.login_view,
+ name='index'
+ ),
+
+ path('login/',
+ views.login_view,
+ name='login'
+ ),
+ path(
+ 'deconnexion/',
+ views.deconnexion_view,
+ name='deconnexion'
+ ),
+ path(
+ 'employé/',
+ include("gestion_employe.urls")
+ ),
+ path(
+ 'gestion-conge/',
+ include("gestion_conge.urls")
+ ),
+ path(
+ 'gestion-projet/',
+ include("gestion_projet.urls")
+ ),
+ path(
+ 'gestion-salle/',
+ include("gestion_salle.urls")
+ ),
+ path(
+ 'admin/',
+ admin.site.urls
+ ),
+ # path(
+ # 'sso',
+ # include(server_sso.get_urls())
+ # )
+] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/SIRH/views.py b/SIRH/views.py
new file mode 100644
index 0000000..77a057d
--- /dev/null
+++ b/SIRH/views.py
@@ -0,0 +1,37 @@
+from django.contrib.auth import authenticate, login, logout
+from django.shortcuts import render, redirect
+from django.contrib import messages
+
+def login_view(request):
+ """
+ Gère la connexion des utilisateurs avec redirection selon le rôle et
+ vérification de l'acceptation de la politique d'utilisation.
+ """
+ if request.method == 'POST':
+ email = request.POST.get('mail')
+ password = request.POST.get('mot_de_passe')
+
+ if not (email and password):
+ messages.error(request, "Veuillez remplir tous les champs.")
+ return render(request, 'login.html')
+
+ user = authenticate(request, username=email, password=password)
+
+ if user is None:
+ messages.error(request, "Nom d’utilisateur ou mot de passe incorrect.")
+ return render(request, 'login.html')
+
+ if not user.is_active:
+ messages.error(request, "Compte inactif. Contactez l'administrateur.")
+ return render(request, 'login.html')
+
+ login(request, user)
+
+ return redirect("gestion_conges:conge")
+
+ return render(request, 'login.html')
+
+def deconnexion_view(request):
+ """Gère la déconnexion de l'utilisateur."""
+ logout(request)
+ return redirect('login')
\ No newline at end of file
diff --git a/SIRH/wsgi.py b/SIRH/wsgi.py
new file mode 100644
index 0000000..5f96c06
--- /dev/null
+++ b/SIRH/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for SIRH project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'SIRH.settings')
+
+application = get_wsgi_application()
diff --git a/fonction_utilitaire/fonctions_utilitaire.py b/fonction_utilitaire/fonctions_utilitaire.py
new file mode 100644
index 0000000..1ccdba0
--- /dev/null
+++ b/fonction_utilitaire/fonctions_utilitaire.py
@@ -0,0 +1,34 @@
+from django.utils import timezone
+from gestion_employe.models import Contrat
+from gestion_conge.models import Conge
+
+
+QUOTA_CONGE_ANNUEL = 30
+NOMBRE_PAGINATION = 8
+DEBUT_RAPPEL = 60
+DUREE_FIN_CONTRAT = 90
+
+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()
+
+ if contrat is None or not contrat.date_debut:
+ return {
+ "success": False,
+ "message": "Votre contrat de travail n'a pas été correctement renseigner. Veuillez contacter les ressources humaines."
+ }
+
+ conges = Conge.objects.filter(employe=employe, validation_direction=True, date_fin__year = timezone.now().date().year)
+ jours_conges_valider = sum([conge.nombre_jours for conge in conges])
+
+ if jours_conges_valider >= QUOTA_CONGE_ANNUEL:
+ return {
+ "success": False,
+ "message": "Vous avez atteint le nombre maximal de jours de congés. Veuillez contacter l'administration."
+ }
+
+ return {
+ "success": True,
+ "quota_annuel": QUOTA_CONGE_ANNUEL - jours_conges_valider,
+ "nombre_jours_valide": jours_conges_valider
+ }
\ No newline at end of file
diff --git a/gestion_conge/__init__.py b/gestion_conge/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gestion_conge/admin.py b/gestion_conge/admin.py
new file mode 100644
index 0000000..ea5d68b
--- /dev/null
+++ b/gestion_conge/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/gestion_conge/apps.py b/gestion_conge/apps.py
new file mode 100644
index 0000000..c320df3
--- /dev/null
+++ b/gestion_conge/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class GestionCongeConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'gestion_conge'
diff --git a/gestion_conge/forms.py b/gestion_conge/forms.py
new file mode 100644
index 0000000..65a7291
--- /dev/null
+++ b/gestion_conge/forms.py
@@ -0,0 +1,17 @@
+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']
+ widgets = {
+ 'date_debut': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
+ 'date_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
+ 'type': forms.Select(attrs={'class': 'form-select'}),
+ }
+
+ labels = {
+ 'nombre_jours':'Nombre de jours',
+ }
\ No newline at end of file
diff --git a/gestion_conge/migrations/0001_initial.py b/gestion_conge/migrations/0001_initial.py
new file mode 100644
index 0000000..6a7e09d
--- /dev/null
+++ b/gestion_conge/migrations/0001_initial.py
@@ -0,0 +1,30 @@
+# Generated by Django 5.2.13 on 2026-04-17 12:03
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('gestion_employe', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Conge',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('date_debut', models.DateField(verbose_name='Date de Début')),
+ ('date_fin', models.DateField(verbose_name='Date de Fin')),
+ ('type', models.CharField(choices=[('conge_annuel', 'Conge Annuel')], max_length=100, verbose_name='Type de Congé')),
+ ('date_demande', models.DateField(auto_now_add=True, verbose_name='Date de Demande')),
+ ('validation_hierarchique', models.BooleanField(default=None, null=True)),
+ ('validation_direction', models.BooleanField(default=None, null=True)),
+ ('motif_refus', models.TextField(blank=True, null=True)),
+ ('employe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='employe', to='gestion_employe.employe')),
+ ],
+ ),
+ ]
diff --git a/gestion_conge/migrations/__init__.py b/gestion_conge/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gestion_conge/models.py b/gestion_conge/models.py
new file mode 100644
index 0000000..f72bbe8
--- /dev/null
+++ b/gestion_conge/models.py
@@ -0,0 +1,36 @@
+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'),
+ ('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,
+ on_delete=models.CASCADE,
+ related_name="employe"
+ )
+ date_debut = models.DateField(verbose_name='Date de Début')
+ date_fin = models.DateField(verbose_name='Date de Fin')
+ type = models.CharField(max_length=100, choices=TYPE_CHOICES, verbose_name='Type de Congé')
+ date_demande = models.DateField(auto_now_add=True, verbose_name="Date de Demande")
+ validation_hierarchique = models.BooleanField(default=None, null=True)
+ validation_direction = models.BooleanField(default=None, null=True)
+
+ motif_refus = models.TextField(blank=True, null=True)
+
+ @property
+ def nombre_jours(self):
+ if self.date_debut and self.date_fin:
+ jours = pd.bdate_range(start=self.date_debut, end=self.date_fin)
+ return len(jours)
\ No newline at end of file
diff --git a/gestion_conge/static/gestion_conge/js/detail_conges.js b/gestion_conge/static/gestion_conge/js/detail_conges.js
new file mode 100644
index 0000000..fd39b94
--- /dev/null
+++ b/gestion_conge/static/gestion_conge/js/detail_conges.js
@@ -0,0 +1,75 @@
+const bouton_enregistrer_detail = document.getElementById("bouton-enregistrer-detail-conge");
+
+if(bouton_enregistrer_detail){
+ bouton_enregistrer_detail.addEventListener("click", () => {
+ const form = document.getElementById("form-detail-conge");
+ const csrftoken = new FormData(form).get("csrfmiddlewaretoken");
+ const actionUrl = form.action;
+ const id_conge = document.getElementById("id_conge").value;
+ const validation_hierarchique_input = document.querySelector('input[name="validation_hierarchique"]:checked');
+ const validation_hierarchique = validation_hierarchique_input ? validation_hierarchique_input.value : null;
+ const validation_direction_input = document.querySelector('input[name="validation_direction"]:checked');
+ const validation_direction = validation_direction_input ? validation_direction_input.value : null;
+ const motif_refus = document.getElementById("motif_refus").value;
+
+ fetch(actionUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-CSRFToken": csrftoken
+ },
+ body: JSON.stringify({
+ id_conge,
+ validation_hierarchique,
+ validation_direction,
+ motif_refus
+ })
+ })
+ .then(response => response.json())
+ .then(data => {
+ alert(data.message);
+ navigation.reload();
+ });
+ })
+}
+
+if(document.getElementById("validation_hierarchique_refuse")){
+ document.getElementById("validation_hierarchique_refuse").addEventListener('click', function(){
+ if(this.checked){
+ alert("coucou");
+ document.getElementById("motif_refus_container").className="d-block form-group mt-3";
+ }else{
+ document.getElementById("motif_refus_container").className="d-none";
+ }
+ })
+}
+
+if(document.getElementById("validation_hierarchique_refuse")){
+ document.getElementById("validation_hierarchique_refuse").addEventListener('click', function(){
+ if(this.checked){
+ document.getElementById("motif_refus_container").className="d-block form-group mt-3";
+ }else{
+ document.getElementById("motif_refus_container").className="d-none";
+ }
+ })
+}
+
+if(document.getElementById("validation_hierarchique_valide")){
+ document.getElementById("validation_hierarchique_valide").addEventListener('click', function(){
+ if(this.checked){
+ document.getElementById("motif_refus_container").className="d-none";
+ }else{
+ document.getElementById("motif_refus_container").className="d-block form-group mt-3";
+ }
+ })
+}
+
+if(document.getElementById("validation_direction_valide")){
+ document.getElementById("validation_direction_valide").addEventListener('click', function(){
+ if(this.checked){
+ document.getElementById("motif_refus_container").className="d-block form-group mt-3";
+ }else{
+ document.getElementById("motif_refus_container").className="d-none";
+ }
+ })
+}
\ No newline at end of file
diff --git a/gestion_conge/static/gestion_conge/js/index.js b/gestion_conge/static/gestion_conge/js/index.js
new file mode 100644
index 0000000..1d156cf
--- /dev/null
+++ b/gestion_conge/static/gestion_conge/js/index.js
@@ -0,0 +1,71 @@
+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:{
+ inputFormat:"yyyy-MM-dd",
+ outputFormat:"dd/MM/yy",
+ }},
+ {"title": "Date de fin", "field": "date_fin", formatter:"datetime", formatterParams:{
+ inputFormat:"yyyy-MM-dd",
+ outputFormat:"dd/MM/yy",
+ }},
+ {"title": "Type de congé", "field": "type"},
+ {"title": "Date de la demande", "field": "date_demande"},
+ {"title": "Validation par supérieur hiérarchique", "field": "validation_hierarchique", formatter:"tickCross", formatterParams :{
+ allowEmpty : true ,
+ }},
+ {"title": "Validation par supérieur hiérarchique", "field": "validation_direction", formatter:"tickCross", formatterParams :{
+ allowEmpty : true ,
+ }},
+ ],
+ pagination: true,
+ paginationSize: 5
+})
+
+const bouton_demande_conges = $("bouton-demande-conge");
+
+bouton_demande_conges.addEventListener("click", (e) => {
+ var modalDemandeConge = new bootstrap.Modal(document.getElementById('modalDemandeConge'));
+ modalDemandeConge.show();
+})
+
+tableau_liste_demande_conge.on("rowClick", function(row, rowData) {
+ const data = rowData.getData();
+ $("id_conge").value = data.id;
+ $("employe").value = data.prenom_nom;
+ $("type_conge").value = data.type;
+ $("date_debut").value = data.date_debut;
+ $("date_fin").value = data.date_fin;
+ $("date_demande").value = data.date_demande;
+ $("nombre_jours").value = data.nombre_jours;
+ $("solde_restant").value = data.solde_conge;
+ $("motif_refus").value = data.motif_refus;
+
+ if($("validation_hierarchique_valide") & $("validation_hierarchique_refuse")){
+ $("validation_hierarchique_valide").checked = data.validation_hierarchique === true;
+ $("validation_hierarchique_refuse").checked = data.validation_hierarchique === false;
+ }
+
+ if($("validation_direction_valide") & $("validation_direction_refuse")){
+ $("validation_direction_valide").checked = data.validation_direction === true;
+ $("validation_direction_refuse").checked = data.validation_direction === false;
+ }
+
+ const modal = new bootstrap.Modal(document.getElementById('detailsCongeModal'));
+ modal.show();
+});
+
+fetch(url_liste_conge_attente)
+ .then(response => response.json())
+ .then(data => {
+ if(data.success === true){
+ tableau_liste_demande_conge.setData(data.data)
+ }else{
+ alert(data.message)
+ }
+ })
diff --git a/gestion_conge/templates/gestion_conge/index.html b/gestion_conge/templates/gestion_conge/index.html
new file mode 100644
index 0000000..17d1ff6
--- /dev/null
+++ b/gestion_conge/templates/gestion_conge/index.html
@@ -0,0 +1,51 @@
+{% extends "BASE.html" %}
+{% load static %}
+{% block 'titre_page' %} Gestion des congés {% endblock %}
+{% block 'contenu' %}
+ {% if messages %}
+ {% for message in messages %}
+
{{message}}
+ {% endfor %}
+ {% endif %}
+
+
+
+ Congés refusés
+
+
+
{{ nombre_conges_refuse }}
+
+
+
+
+ Congés en attente
+
+
+
{{ nombre_conges_en_attente }}
+
+
+
+
+ Congés Validé
+
+
+
{{ nombre_conges_valide }}
+
+
+
+
Liste des demandes de congé
+
+
+
+
+
+
+{% endblock %}
+{% block 'modal' %}
+ {% include 'gestion_conge/parts/modalDemandeConge.html' %}
+ {% include 'gestion_conge/parts/modalDetailConge.html' %}
+{% endblock %}
+{% block 'js' %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/gestion_conge/templates/gestion_conge/parts/modalDemandeConge.html b/gestion_conge/templates/gestion_conge/parts/modalDemandeConge.html
new file mode 100644
index 0000000..511d7dc
--- /dev/null
+++ b/gestion_conge/templates/gestion_conge/parts/modalDemandeConge.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ Nouvelle demande de congé
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_conge/templates/gestion_conge/parts/modalDetailConge.html b/gestion_conge/templates/gestion_conge/parts/modalDetailConge.html
new file mode 100644
index 0000000..d3d3051
--- /dev/null
+++ b/gestion_conge/templates/gestion_conge/parts/modalDetailConge.html
@@ -0,0 +1,92 @@
+
+
+
+
+
Détails de la demande de congés
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_conge/templates/gestion_conge/parts/modalModificationConge.html b/gestion_conge/templates/gestion_conge/parts/modalModificationConge.html
new file mode 100644
index 0000000..7a920ca
--- /dev/null
+++ b/gestion_conge/templates/gestion_conge/parts/modalModificationConge.html
@@ -0,0 +1,48 @@
+{% for conge in conges %}
+
+ {{ projet.nom_projet }} ({{ projet.pourcentage_temps_affectation }}%) du {{ projet.date_debut|date:"d/m/Y" }} au
+ {{ projet.date_fin|date:"d/m/Y" }} en tant que {{ projet.role }}.
+
\ No newline at end of file
diff --git a/gestion_employe/templates/gestion_employe/parts/modalAjoutFormation.html b/gestion_employe/templates/gestion_employe/parts/modalAjoutFormation.html
new file mode 100644
index 0000000..104b791
--- /dev/null
+++ b/gestion_employe/templates/gestion_employe/parts/modalAjoutFormation.html
@@ -0,0 +1,21 @@
+
+
+
+
+
Ajouter un certificat
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_employe/templates/gestion_employe/parts/modalCreationContrat.html b/gestion_employe/templates/gestion_employe/parts/modalCreationContrat.html
new file mode 100644
index 0000000..fa0bb2b
--- /dev/null
+++ b/gestion_employe/templates/gestion_employe/parts/modalCreationContrat.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ Contrat de
+
+
+
+
+
+ {% comment %}
+ {% endcomment %}
+
+
+
+
\ No newline at end of file
diff --git a/gestion_employe/templates/gestion_employe/parts/modalDetailEmploye.html b/gestion_employe/templates/gestion_employe/parts/modalDetailEmploye.html
new file mode 100644
index 0000000..187ef49
--- /dev/null
+++ b/gestion_employe/templates/gestion_employe/parts/modalDetailEmploye.html
@@ -0,0 +1,136 @@
+{% load tags_personnaliser %}
+
\ No newline at end of file
diff --git a/gestion_employe/templates/gestion_employe/parts/modalDocument.html b/gestion_employe/templates/gestion_employe/parts/modalDocument.html
new file mode 100644
index 0000000..4eeb124
--- /dev/null
+++ b/gestion_employe/templates/gestion_employe/parts/modalDocument.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Documents RH de
+
+
+
+
+
+
+
+
+ Diplôme :
+
+
+
+ CV :
+
+
+
+ RIB :
+
+
+
+ Casier judiciaire :
+
+
+
+
+
+
+
+
+
+
diff --git a/gestion_employe/templates/gestion_employe/parts/modalListeContratExpirants.html b/gestion_employe/templates/gestion_employe/parts/modalListeContratExpirants.html
new file mode 100644
index 0000000..9051355
--- /dev/null
+++ b/gestion_employe/templates/gestion_employe/parts/modalListeContratExpirants.html
@@ -0,0 +1,17 @@
+
+
+
+
+
Contrats expirants dans un maximum de 60 jours
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_employe/templates/gestion_employe/parts/modificationMotPasse.html b/gestion_employe/templates/gestion_employe/parts/modificationMotPasse.html
new file mode 100644
index 0000000..79366ba
--- /dev/null
+++ b/gestion_employe/templates/gestion_employe/parts/modificationMotPasse.html
@@ -0,0 +1,36 @@
+
+
+
+
+
Modifier le mot de passe
+
+
+
+
+
+
+
+
+
diff --git a/gestion_employe/templatetags/tags_personnaliser.py b/gestion_employe/templatetags/tags_personnaliser.py
new file mode 100644
index 0000000..c1859f7
--- /dev/null
+++ b/gestion_employe/templatetags/tags_personnaliser.py
@@ -0,0 +1,24 @@
+"""Tags personnalisés pour les templates d'authentification.
+
+Ce fichier contient des tags personnalisés pour les templates du projet lié à
+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()
+
+@register.filter
+def has_group(user, group_name):
+ """Vérifiez si un utilisateur appartient à un groupe spécifique."""
+ return user.groups.filter(name=group_name).exists()
+
+@register.filter
+def is_chef_projet(user):
+ try:
+ affectation = Affectation.objects.get(employe__user__username = user, date_fin_daffectation__gte = timezone.now().date())
+ except Affectation.DoesNotExist:
+ return False
+ return affectation.role == "chef_projet"
\ No newline at end of file
diff --git a/gestion_employe/tests.py b/gestion_employe/tests.py
new file mode 100644
index 0000000..de8bdc0
--- /dev/null
+++ b/gestion_employe/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/gestion_employe/urls.py b/gestion_employe/urls.py
new file mode 100644
index 0000000..992dbd7
--- /dev/null
+++ b/gestion_employe/urls.py
@@ -0,0 +1,102 @@
+from . import views
+from django.urls import path
+
+app_name = 'gestion_employe'
+
+urlpatterns = [
+ path(
+ '',
+ views.index,
+ name='index'
+ ),
+ path(
+ 'liste-employes/',
+ views.liste_employe,
+ name='liste-employes'
+ ),
+ path(
+ 'enregistrer-detail-employe/',
+ views.enregistrer_detail_employe,
+ name='enregistrer-detail-employe'
+ ),
+ path(
+ 'affectation/supprimer/',
+ views.suppression_affectation,
+ name='supprimer-affectation'
+ ),
+ path(
+ 'contrat/creation/',
+ views.creation_contrat,
+ name='creation-contrat'
+ ),
+ path(
+ 'contrat/supprimer/',
+ views.suppression_contrat,
+ name='supprimer-contrat'
+ ),
+ path(
+ 'Affectation/affectation/',
+ views.affecter_employe_projet,
+ name='affecter_employe_projet'
+ ),
+ path(
+ 'mon-profil/',
+ views.mon_profil,
+ name='mon-profil'
+ ),
+ path(
+ 'enregistrement-document-rh',
+ views.enregistrement_document,
+ name='enregistrement-document-rh'
+ ),
+ path(
+ 'modifier-employe',
+ views.modifier_employer,
+ name='modifier-employe'
+ ),
+ path(
+ "mes-formations/ajouter/",
+ views.ajouter_formation,
+ name="ajouter_formation"
+ ),
+ path(
+ "mes-formations/liste/",
+ views.liste_formation,
+ name="liste-formation"
+ ),
+ path(
+ "mes-formations/modifier//",
+ views.modifier_formation,
+ name="modifier_formation"
+ ),
+ path(
+ "mes-formations/supprimer//",
+ views.supprimer_formation,
+ name="supprimer_formation"
+ ),
+ path(
+ 'modifier-mot-passe/',
+ views.modifier_mot_passe,
+ name='modifier-mot-passe'
+ ),
+ # path(
+ # 'creation-departement/',
+ # views.creation_departement,
+ # name='creation-departement'
+ # ),
+ # path(
+ # 'modifier-departement/',
+ # views.modifier_departement,
+ # name='modifier-departement'
+ # ),
+ # path(
+ # 'suppression-departement/',
+ # views.supprimer_departement,
+ # name='suppression-departement/'
+ # ),
+ path(
+ "liste-contrat-expirants",
+ views.liste_contrat_expirants,
+ name='liste-contrat-expirants'
+ )
+]
\ No newline at end of file
diff --git a/gestion_employe/views.py b/gestion_employe/views.py
new file mode 100644
index 0000000..f9ef342
--- /dev/null
+++ b/gestion_employe/views.py
@@ -0,0 +1,447 @@
+import json
+from datetime import timedelta, datetime
+from dateutil.relativedelta import relativedelta
+
+from django.utils import timezone
+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.db.models import Sum
+
+from .models import Employe, Contrat, Affectation, Formation
+from .forms import AffectationForm, ContratForm, FormationForm
+from fonction_utilitaire import fonctions_utilitaire
+
+@login_required
+def index(request):
+ """Vue d'index"""
+ employes = Employe.objects.all().order_by('-user__date_joined')
+ nombre_employes = Employe.objects.count()
+ nombre_cps = Contrat.objects.filter(type_contrat='contrat_prestation').count()
+ nombre_stage = Contrat.objects.filter(type_contrat='contrat_stage').count()
+ date_limite = timezone.now().date() + timedelta(days=60)
+ nombre_expirants = Contrat.objects.filter(
+ date_fin__lte=date_limite,
+ date_fin__gte=timezone.now().date(),
+ statut='actif'
+ ).count()
+
+ return render(request, 'gestion_employe/index.html', {
+ 'employes': employes,
+ 'nombre_employes': nombre_employes,
+ 'nombre_cps': nombre_cps,
+ 'nombre_stage': nombre_stage,
+ 'nombre_expirants': nombre_expirants,
+ 'affectation_form': AffectationForm(),
+ 'contrat_form': ContratForm()
+ })
+
+@login_required
+def liste_contrat_expirants(request):
+ """ Liste des contrats proches """
+
+ date_limite = timezone.now().date() + timedelta(days = fonctions_utilitaire.DUREE_FIN_CONTRAT)
+ contats_expirants = [
+ {
+ 'employe': f"{contrat.employe.user.first_name} {contrat.employe.user.last_name}",
+ 'type_contrat': dict(Contrat.TYPE_CONTRAT).get(contrat.type_contrat),
+ 'date_debut': contrat.date_debut,
+ 'date_fin': contrat.date_fin,
+ 'statut': contrat.statut,
+ 'fichier_contrat': contrat.fichier_contrat.url if contrat.fichier_contrat else ""
+ }
+ for contrat in
+ Contrat.objects.filter(date_fin__lte=date_limite, date_fin__gte=timezone.now().date(), statut='actif')
+ ]
+
+ return JsonResponse(contats_expirants, safe=False)
+
+@login_required
+def affecter_employe_projet(request):
+ """Vue pour affecter un employé à un projet avec vérification des contraintes d'affectation"""
+ if request.method == 'POST':
+ employe_id = request.POST.get('affecter_employe_id')
+ try:
+ employe = Employe.objects.get(id=employe_id)
+ except Employe.DoesNotExist:
+ messages.error(request, "L'employé spécifié n'existe pas.")
+ return redirect('gestion_employe:index')
+
+ form = AffectationForm(request.POST)
+ if form.is_valid():
+ projet = form.cleaned_data['projet']
+ date_fin_affectation = form.cleaned_data['date_fin_daffectation']
+ temps_nouveau = form.cleaned_data['pourcentage_temps_affectation']
+ date_affectation = form.cleaned_data['date_affectation']
+
+ if (date_fin_affectation and date_affectation):
+ total_affectation = (
+ Affectation.objects.filter(employe=employe)
+ .aggregate(total_pourcentage_affectation = Sum('pourcentage_temps_affectation'))
+ ['total_pourcentage_affectation'] or 0
+ )
+ if (date_fin_affectation < date_affectation):
+ messages.warning(request, "La date de fin d'affectation ne peut pas être antérieure à la date de début.")
+ return redirect('gestion_employe:index')
+ elif date_fin_affectation > projet.date_fin:
+ messages.warning(request, f"La date de fin de l'affectation ({date_fin_affectation}) ne peut pas dépasser la date de fin du projet ({projet.date_fin}).")
+ return redirect('gestion_employe:index')
+ elif total_affectation + temps_nouveau > 100:
+ messages.warning(
+ request,
+ f"Les pourcentages d'affectation de l'employé {employe.first_name} {employe.last_name} dépasse 100% sur les différents projets ({total_affectation + temps_nouveau}%)."
+ )
+ return redirect('gestion_employe:index')
+
+ Affectation.objects.update_or_create(
+ employe=employe,
+ projet=projet,
+ defaults={
+ 'date_affectation': form.cleaned_data['date_affectation'],
+ 'date_fin_daffectation': date_fin_affectation,
+ 'role': form.cleaned_data['role'],
+ 'pourcentage_temps_affectation': temps_nouveau,
+ }
+ )
+ messages.success(request, f"L'employé {employe.user.first_name} {employe.user.last_name} a été affecté au projet {projet.nom_projet}.")
+ return redirect('gestion_employe:index')
+ else:
+ messages.error(request, "Erreur : Formulaire non valide.")
+ return redirect('gestion_employe:index')
+ else:
+ return redirect('gestion_employe:index')
+
+@login_required
+def mon_profil(request):
+ """Vue pour afficher et modifier le profil de l'utilisateur connecté"""
+ 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.")
+ 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()
+ ).select_related('projet')
+
+ return render(
+ 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 []],
+ 'projets': [
+ {
+ **model_to_dict(a.projet),
+ "date_affectation": a.date_affectation,
+ "date_fin_daffectation": a.date_fin_daffectation,
+ "role": dict(Affectation.ROLE_CHOICES).get(a.role),
+ "pourcentage_temps_affectation": a.pourcentage_temps_affectation
+ } for a in projets
+ ],
+ "formation_form": FormationForm(),
+ "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
+ }
+ )
+
+@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:
+ employe = Employe.objects.get(user__username=request.user)
+ except Employe.DoesNotExist:
+ return JsonResponse({"message": "Employé non trouvé."})
+ if request.method == "POST":
+ data = json.loads(request.body)
+ user = User.objects.get(username=request.user)
+ user.last_name = data['nom']
+ user.first_name = data['prenom']
+ user.email = data['email']
+ employe.telephone = data['telephone']
+ employe.adresse = data['adresse']
+ employe.sexe = data['sexe']
+ if request.FILES.get("photo"):
+ employe.photo = request.FILES["photo"]
+ if data['date_naissance']:
+ difference = relativedelta(timezone.now().date(), datetime.strptime(data['date_naissance'], "%Y-%m-%d").date())
+ if difference.years >= 18:
+ employe.date_naissance = data['date_naissance']
+ else:
+ return JsonResponse({"message": "Veuillez entrez une date de naissance correcte."})
+ employe.save()
+ user.save()
+ return JsonResponse({"message": "Profil mis à jour avec succès."})
+
+def enregistrement_document(request):
+ employe = Employe.objects.get(user=request.user)
+ if request.method == "POST":
+ if request.FILES.get("photo"):employe.photo = request.FILES["photo"]
+ if "cv" in request.FILES:employe.CV = request.FILES["cv"]
+ if "diplome" in request.FILES: employe.diplome = request.FILES["diplome"]
+ if "rib" in request.FILES: employe.rib = request.FILES["rib"]
+ if "casier_judiciaire" in request.FILES:employe.casier_judiciaire = request.FILES["casier_judiciaire"]
+ employe.save()
+ messages.success(request, "Documents enregistrés avec succès.")
+
+ return redirect("gestion_employe:mon-profil")
+
+def suppression_contrat(request):
+ """Vue pour permettre à un utilisateur de supprimer un contrat"""
+ id_contrat = json.loads(request.body)['id']
+ try:
+ contrat = Contrat.objects.get(numero_contrat = id_contrat)
+ except Contrat.DoesNotExist:
+ messages.error(request, "Contrat non trouvé.")
+ return JsonResponse({"message": "Contrat non trouvé."}, status=404)
+
+ contrat.delete()
+ return JsonResponse({"message": "Contrat supprimé avec succès."})
+
+def suppression_affectation(request):
+ """Vue pour permettre à un utilisateur de supprimer une affectation"""
+ id_affectation = json.loads(request.body)['id']
+ try:
+ affectation = Affectation.objects.get(id=id_affectation)
+ except Affectation.DoesNotExist:
+ return JsonResponse({"message": "Affectation non trouvée."}, status=404)
+
+ affectation.delete()
+ return JsonResponse({"message": "Affectation supprimée avec succès."})
+
+def creation_contrat(request):
+ """Vue pour permettre à un utilisateur de créer un contrat pour un employé"""
+ try:
+ employe = Employe.objects.get(id=request.POST.get('employe_id'))
+ except Employe.DoesNotExist:
+ messages.error(request, "Employé non trouvé.")
+ return redirect('employe-index')
+
+ if request.method == "POST":
+ form = ContratForm(request.POST, request.FILES)
+ if form.is_valid():
+ contrat = form.save(commit=False)
+ contrat.employe = employe
+ contrat.save()
+ messages.success(request, "Contrat créé avec succès.")
+ return redirect('gestion_employe:index')
+ messages.error(request, "Formulaire non valide")
+ else:
+ form = ContratForm(initial={'employe': employe})
+ return render(request, 'gestion_employe/index.html', {'contrat_form': form})
+
+@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"""
+ if request.method == "POST":
+ data = json.loads(request.body)
+ try:
+ employe = Employe.objects.get(id=data['id'])
+ 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.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)
+
+@login_required
+def liste_employe(request):
+ """ Vue pour retourner la liste de tous les employés """
+ employes = Employe.objects.exclude(user__first_name = '', user__last_name = '')
+
+ data = []
+ for employe in employes:
+ if employe.user.first_name != ' ' and employe.user.last_name != ' ':
+ projets = [
+ ", ".join([
+ a.projet.nom_projet for a in Affectation.objects.filter(
+ employe=employe,
+ date_fin_daffectation__gte=timezone.now().date()
+ )
+ ])
+ ]
+ formations = [
+ {
+ "titre": formation.titre,
+ "organisme": formation.organisme,
+ "description": formation.description,
+ "date_obtention": formation.date_obtention,
+ "date_fin": formation.date_fin,
+ "certificat": formation.certificat.url if formation.certificat else "",
+ } for formation in Formation.objects.filter(employe=employe)
+ ]
+
+ contrats = [
+ {
+ "numero_contrat": contrat.numero_contrat,
+ "type_contrat": contrat.type_contrat,
+ "date_debut": contrat.date_debut,
+ "date_fin": contrat.date_fin,
+ "salaire_mensuel": contrat.salaire_mensuel,
+ "statut": contrat.statut,
+ "fichier_contrat": contrat.fichier_contrat.url if contrat.fichier_contrat else "",
+ } for contrat in Contrat.objects.filter(employe=employe, statut='actif')
+ ]
+
+ affectations = [
+ {**model_to_dict(affectation), "projet": affectation.projet.nom_projet}
+ for affectation in Affectation.objects.filter(
+ employe=employe,
+ date_fin_daffectation__gte=timezone.now().date()
+ )
+ ]
+
+ data.append(
+ {
+ "id": employe.id,
+ "employe": f"{employe.user.first_name} {employe.user.last_name}",
+ "matricule": employe.matricule,
+ "email": employe.user.email,
+ "formations": formations,
+ "affectations": affectations,
+ "projet": projets,
+ "contrats": contrats,
+ "departement": employe.departement.nom if employe.departement else "",
+ "fonction": employe.fonction,
+ "date_embauche": employe.date_embauche,
+ "adresse": employe.adresse,
+ "telephone": employe.telephone,
+ "sexe": employe.sexe,
+ "CV": employe.CV.url if employe.CV else "",
+ "diplome": employe.diplome.url if employe.diplome else "",
+ "rib": employe.rib.url if employe.rib else "",
+ "photo": employe.photo.url if employe.photo else "",
+ "casier_judiciaire": employe.casier_judiciaire.url if employe.casier_judiciaire else "",
+ "date_naissance": employe.date_naissance,
+ }
+ )
+ return JsonResponse({'success': True, 'data': data}, safe=False)
+
+@login_required
+def ajouter_formation(request):
+ """Vue pour permettre à un employé d'ajouter une formation à son profil"""
+ employe = Employe.objects.get(user__username=request.user)
+ if request.method == "POST":
+ formation = FormationForm(request.POST, request.FILES)
+ if formation.is_valid():
+ ma_formation = formation.save(commit=False)
+ ma_formation.employe = employe
+ ma_formation.save()
+ messages.success(request, "Formation ajoutée avec succès ")
+ return redirect("gestion_employe:mon-profil")
+ messages.error(request, "Formulaire non valide. Veuillez vérifier les informations saisies.")
+ return redirect("gestion_employe:mon-profil")
+
+def liste_formation(request):
+ formations = Formation.objects.filter(employe__user__username=request.user).order_by("-date_obtention")
+ return JsonResponse([
+ {
+ **model_to_dict(formation),
+ "certificat": formation.certificat.url if formation.certificat else ""
+ }
+ for formation in formations
+ ], safe=False)
+
+@login_required
+def modifier_formation(request, id_formation):
+ """Vue pour permettre à un employé de modifier une formation de son profil"""
+ try:
+ formation = Formation.objects.get(id=id_formation, employes=request.user)
+ except Formation.DoesNotExist:
+ messages.error(request, "Formation non trouvée.")
+ return redirect("mes_formations")
+
+ if request.method == "POST":
+ formation = FormationForm(request.POST, request.FILES, instance=formation)
+ if formation.is_valid():
+ messages.success(request, "Formation mise à jour ")
+
+ formation.save()
+ messages.error(request, "Formulaire non valide. Veuillez vérifier les informations saisies.")
+ return redirect("mes_formations")
+
+@login_required
+def supprimer_formation(request, id_formation):
+ """Vue pour permettre à un employé de supprimer une formation de son profil"""
+ try:
+ formation = Formation.objects.get(id=id_formation, employes=request.user)
+ except Formation.DoesNotExist:
+ messages.error(request, "Formation non trouvée.")
+ return redirect("mes_formations")
+
+ if request.method == "POST":
+ formation.delete()
+ messages.success(request, "Formation supprimée ")
+ return redirect("mes_formations")
+
+# @login_required
+# def creation_departement(request):
+# """Gère la création d'un nouveau département via un formulaire."""
+# if request.method == 'POST':
+# form_departement = DepartementForm(request.POST)
+# if form_departement.is_valid():
+# form_departement.save()
+# messages.success(request, "Département ajouté avec succès.")
+# else:
+# messages.error(request, "Erreur lors de l'ajout du département.")
+# return redirect('parametres-rh')
+
+# @login_required
+# def modifier_departement(request, id):
+# """Gère la modification d'un département existant via un formulaire pré-rempli."""
+# departement = Departement.objects.get(id=id)
+# form = DepartementForm(instance=departement)
+# if request.method == 'POST':
+# nouveau_nom_departement = request.POST.get('nom')
+# if nouveau_nom_departement:
+# departement.nom = nouveau_nom_departement
+# departement.save()
+# messages.success(request, "Département modifié avec succès.")
+# return redirect('parametres-rh')
+# return render(request, 'gestion_employe/edit_departement.html', {
+# 'form': form,
+# 'departement': departement
+# })
+
+# @login_required
+# def supprimer_departement(request, id):
+# """Gère la suppression d'un département existant."""
+# if request.method == "POST":
+# departement = Departement.objects.get(id=id)
+# departement.delete()
+# messages.success(request, "Département supprimé avec succès !")
+# return redirect('parametres-rh')
diff --git a/gestion_projet/__init__.py b/gestion_projet/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gestion_projet/admin.py b/gestion_projet/admin.py
new file mode 100644
index 0000000..d0ae965
--- /dev/null
+++ b/gestion_projet/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+from .models import DomaineDeRecherche
+
+@admin.register(DomaineDeRecherche)
+class DomaineDeRecherche(admin.ModelAdmin):
+ list_display = ('nom',)
diff --git a/gestion_projet/forms.py b/gestion_projet/forms.py
new file mode 100644
index 0000000..8874d67
--- /dev/null
+++ b/gestion_projet/forms.py
@@ -0,0 +1,176 @@
+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:
+ model = Projet
+ fields = (
+ 'id_projet',
+ 'nom_projet',
+ 'date_debut',
+ 'date_fin',
+ 'numero_convention',
+ 'domaine_recherche',
+ 'type_projet',
+ 'budget',
+ 'budget_RH',
+ 'description'
+ )
+ # domaine_recherche = forms.ModelMultipleChoiceField(
+ # queryset=DomaineDeRecherche.objects.all(),
+ # to_field_name="nom",
+ # required=False
+ # )
+ widgets = {
+ 'id_projet': forms.TextInput(attrs={'class': "form-control"}),
+ '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"}),
+ 'date_debut': forms.DateInput(attrs={'type': 'date', 'class': "form-control"}),
+ 'date_fin': forms.DateInput(attrs={'type': 'date', 'class': "form-control"}),
+ }
+
+class BailleurForm(forms.ModelForm):
+ """
+ Formulaire de création et de modification d'un bailleur,
+ avec validation des champs et personnalisation des labels.
+ """
+ class Meta:
+ model = Bailleur
+ fields = ('nom_organisme', 'contact', 'email', 'pays')
+ widgets = {
+ 'nom_organisme':forms.TextInput(attrs={
+ 'class':'form-control',
+ }),
+ 'contact':forms.TextInput(attrs={
+ 'class':'form-control',
+ }),
+ 'email':forms.TextInput(attrs={
+ 'class':'form-control',
+ }),
+ 'pays':forms.TextInput(attrs={
+ 'class':'form-control',
+ }),
+ }
+
+class DocumentProjetForm(forms.ModelForm):
+ """
+ Formulaire pour ajouter ou modifier un document associé à un projet,
+ avec validation des champs et personnalisation des labels.
+ """
+ class Meta:
+ model = DocumentProjet
+ fields = [
+ 'nom_document',
+ 'numero',
+ 'date_validite',
+ 'fichier',
+ 'description'
+ ]
+ widgets = {
+ 'nom_document': forms.Select(attrs={'class': 'form-select'}),
+ 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
+ 'numero': forms.TextInput(attrs={'class': 'form-control'}),
+ 'date_validite': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
+ 'fichier': forms.ClearableFileInput(attrs={'class': 'form-control'}),
+ }
+
+class ActiviteProjetForm(forms.ModelForm):
+ """Formulaire pour créer ou modifier une activité de projet, avec validation des champs et personnalisation des widgets."""
+ class Meta:
+ model = ActiviteProjet
+ fields = (
+ 'titre',
+ 'date_debut',
+ 'date_fin',
+ 'besoin_ressource_materielle',
+ 'budget_prevu',
+ 'description',
+ )
+
+ widgets = {
+ 'titre':forms.TextInput(attrs={
+ 'class':'form-control',
+ 'placeholder':'Titre de l’activité'
+ }),
+ 'description':forms.Textarea(attrs={
+ 'class':'form-control',
+ 'rows':3,
+ 'placeholder':'Description de l’activité'
+ }),
+ 'date_debut':forms.DateInput(attrs={
+ 'class':'form-control',
+ 'type':'date'
+ }),
+ 'date_fin':forms.DateInput(attrs={
+ 'class':'form-control',
+ 'type':'date'
+ }),
+ 'besoin_ressource_materielle': forms.Textarea(attrs={
+ 'class':'form-control',
+ 'rows':3,
+ 'placeholder':'Besoin de ressources matérielles'
+ }),
+ 'budget_prevu': forms.NumberInput(attrs={
+ 'class':'form-control',
+ 'placeholder':'Budget prévu'
+ }),
+ }
+
+class FinancementProjetFrom(forms.ModelForm):
+ """Formulaire pour créer ou modifier le financement relatif à un projet."""
+ class Meta:
+ model = FinancementProjet
+ fields = (
+ 'projet',
+ 'bailleur',
+ 'pourcentage',
+ )
+
+ widgets = {
+ 'projet':forms.Select(attrs={
+ 'class':'form-select',
+ }),
+ 'bailleur':forms.Select(attrs={
+ 'class':'form-select',
+ }),
+ 'pourcentage':forms.NumberInput(attrs={
+ 'class':'form-control',
+ }),
+ }
+
+class LivrablesLivresForm(forms.ModelForm):
+ """Formulaire pour créer ou modifier un livrable livré dans le cadre d'une activité de projet."""
+ class Meta:
+ model = LivrablesLivres
+ fields = (
+ 'activite',
+ 'nom',
+ 'fichier',
+ )
+
+ widgets = {
+ 'activite': forms.Select(attrs={
+ 'class':'form-select',
+ }),
+ 'nom': forms.TextInput(attrs={
+ 'class':'form-control',
+ 'placeholder':'Nom du livrable'
+ }),
+ 'fichier': forms.ClearableFileInput(attrs={
+ 'class':'form-control',
+ }),
+ }
\ No newline at end of file
diff --git a/gestion_projet/migrations/0001_initial.py b/gestion_projet/migrations/0001_initial.py
new file mode 100644
index 0000000..c85eb6d
--- /dev/null
+++ b/gestion_projet/migrations/0001_initial.py
@@ -0,0 +1,107 @@
+# Generated by Django 5.2.13 on 2026-04-17 12:03
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ActiviteProjet',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('titre', models.CharField(max_length=200, verbose_name="Titre de l'activité")),
+ ('description', models.TextField(blank=True, null=True, verbose_name="Description de l'activité")),
+ ('date_debut', models.DateField(verbose_name='Date de début')),
+ ('date_fin', models.DateField(verbose_name='Date de fin')),
+ ('annuler', models.BooleanField(default=False, verbose_name="Annuler l'activité")),
+ ('motif_annulation', models.TextField(blank=True, null=True, verbose_name="Motif d'annulation")),
+ ('motif_changement_budget', models.TextField(blank=True, null=True, verbose_name='Motif de changement de budget')),
+ ('budget_prevu', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Budget prévu')),
+ ('budget_depense', models.DecimalField(decimal_places=2, default=0, max_digits=15, verbose_name='Budget dépensé')),
+ ('besoin_ressource_materielle', models.TextField(verbose_name='Besoin de ressources matérielles')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Bailleur',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('nom_organisme', models.CharField(max_length=200, unique=True)),
+ ('contact', models.CharField(blank=True, max_length=100, null=True)),
+ ('email', models.EmailField(blank=True, max_length=254, null=True)),
+ ('pays', models.CharField(blank=True, max_length=100, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='DomaineDeRecherche',
+ fields=[
+ ('nom', models.CharField(choices=[('sciences_sociales', 'Sciences sociales'), ('naturelles', 'Naturelles'), ('humaines', 'Humaines'), ('veterinaires', 'Vétérinaires')], max_length=100, primary_key=True, serialize=False, verbose_name='Domaine de recherche')),
+ ],
+ options={
+ 'verbose_name': 'Domaine de recherche',
+ 'verbose_name_plural': 'Domaines de recherche',
+ },
+ ),
+ migrations.CreateModel(
+ name='LivrablesLivres',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('nom', models.CharField(max_length=255, verbose_name='Nom du livrable')),
+ ('fichier', models.FileField(blank=True, null=True, upload_to='fichier_livrables/')),
+ ('activite', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion_projet.activiteprojet')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Projet',
+ fields=[
+ ('id_projet', models.CharField(blank=True, max_length=100, primary_key=True, serialize=False, unique=True, verbose_name='ID du projet')),
+ ('nom_projet', models.CharField(max_length=200, verbose_name='Nom du projet')),
+ ('date_debut', models.DateField(verbose_name='Date de début')),
+ ('date_fin', models.DateField(verbose_name='Date de fin')),
+ ('numero_convention', models.CharField(max_length=100, verbose_name='Numéro de convention')),
+ ('description', models.TextField(verbose_name='Description')),
+ ('type_projet', models.CharField(choices=[('laboratoire', 'Laboratoire'), ('épidémiologie', 'Épidémiologie'), ('sciences sociales', 'Sciences sociales'), ('cliniques', 'Cliniques'), ('autre', 'Autre')], default='épidémiologie', max_length=100, verbose_name='Type de projet')),
+ ('budget', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Budget')),
+ ('budget_RH', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Budget RH')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('bailleur', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='gestion_projet.bailleur', verbose_name='Bailleur de fonds')),
+ ('domaine_recherche', models.ManyToManyField(to='gestion_projet.domainederecherche')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='DocumentProjet',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('date_ajout', models.DateTimeField(auto_now_add=True, verbose_name="Date d'ajout")),
+ ('nom_document', models.CharField(choices=[('protocole', 'Protocole d’étude'), ('ethique', "Approbation du comité d'éthique"), ('autorisation', 'Autorisation (DNLP)'), ('rapport_technique', 'Rapport technique'), ('rapport_financier', 'Rapport financier'), ('rapport_avancement', "Rapport d'avancement"), ('convention', 'Convention'), ('rapport_final', 'Rapport final'), ('autre', 'Autre')], max_length=100, verbose_name='Type de document')),
+ ('description', models.TextField(blank=True, verbose_name='Description')),
+ ('numero', models.CharField(blank=True, max_length=100, null=True, verbose_name='Numéro du document')),
+ ('date_validite', models.DateField(blank=True, null=True, verbose_name='Date de validité')),
+ ('fichier', models.FileField(upload_to='documents_projets/', verbose_name='Fichier à télécharger')),
+ ('projet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='gestion_projet.projet', verbose_name='Projet')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='activiteprojet',
+ name='projet',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion_projet.projet', verbose_name='Projet'),
+ ),
+ migrations.CreateModel(
+ name='FinancementProjet',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('pourcentage', models.DecimalField(decimal_places=2, max_digits=5)),
+ ('bailleur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion_projet.bailleur')),
+ ('projet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion_projet.projet')),
+ ],
+ options={
+ 'unique_together': {('projet', 'bailleur')},
+ },
+ ),
+ ]
diff --git a/gestion_projet/migrations/__init__.py b/gestion_projet/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gestion_projet/models.py b/gestion_projet/models.py
new file mode 100644
index 0000000..d5723bb
--- /dev/null
+++ b/gestion_projet/models.py
@@ -0,0 +1,309 @@
+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(
+ max_length=200,
+ unique=True
+ )
+ contact = models.CharField(
+ max_length=100,
+ blank=True,
+ null=True
+ )
+ email = models.EmailField(
+ blank=True,
+ null=True
+ )
+ pays = models.CharField(
+ max_length=100,
+ blank=True,
+ null=True
+ )
+
+ def __str__(self):
+ return self.nom_organisme
+
+class DomaineDeRecherche(models.Model):
+ """Modèle représentant les domaines de recherche"""
+
+ DOMAINE_RECHERCHE = [
+ ('sciences_sociales', 'Sciences sociales'),
+ ('naturelles', 'Naturelles'),
+ ('humaines', 'Humaines'),
+ ('veterinaires', 'Vétérinaires')
+ ]
+
+ nom = models.CharField(
+ max_length=100,
+ verbose_name="Domaine de recherche",
+ choices=DOMAINE_RECHERCHE,
+ primary_key=True
+ )
+
+ class Meta:
+ verbose_name = 'Domaine de recherche'
+ verbose_name_plural = 'Domaines de recherche'
+
+ def __str__(self):
+ return self.nom
+
+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,
+ unique=True,
+ primary_key=True,
+ verbose_name="ID du projet"
+ )
+ nom_projet = models.CharField(
+ max_length=200,
+ verbose_name="Nom du projet"
+ )
+ date_debut = models.DateField(
+ verbose_name="Date de début"
+ )
+ date_fin = models.DateField(
+ verbose_name="Date de fin"
+ )
+ numero_convention = models.CharField(
+ max_length=100,
+ verbose_name="Numéro de convention"
+ )
+ 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,
+ decimal_places=2,
+ verbose_name="Budget"
+ )
+ budget_RH = models.DecimalField(
+ max_digits=12,
+ decimal_places=2,
+ verbose_name="Budget RH"
+ )
+ created_at = models.DateTimeField(auto_now_add=True)
+ bailleur = models.ForeignKey(
+ Bailleur,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ verbose_name="Bailleur de fonds"
+ )
+
+ @property
+ def statut(self):
+ if self.date_fin < date.today():
+ return "Terminé"
+ return "En cours"
+
+ @property
+ def avancement(self):
+ aujourd_hui = date.today()
+ 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
+ if duree_projet > 0:
+ return round((temps_ecoule / duree_projet) * 100, 2)
+
+ return 0
+
+ def __str__(self):
+ return f"{self.nom_projet}"
+
+class FinancementProjet(models.Model):
+ """
+ Modèle représentant le financement d'un projet par un bailleur,
+ avec le pourcentage de contribution.
+ """
+ projet = models.ForeignKey(
+ Projet,
+ on_delete=models.CASCADE
+ )
+ bailleur = models.ForeignKey(
+ Bailleur,
+ on_delete=models.CASCADE
+ )
+ pourcentage = models.DecimalField(
+ max_digits = 5,
+ decimal_places=2
+ )
+
+ class Meta:
+ unique_together = ('projet', 'bailleur')
+
+ def __str__(self):
+ return f"{self.bailleur.nom} - {self.projet.nom_projet} ({self.pourcentage}%)"
+
+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 = [
+ ('protocole', 'Protocole d’étude'),
+ ('ethique', "Approbation du comité d'éthique"),
+ ('autorisation', 'Autorisation (DNLP)'),
+ ('rapport_technique', 'Rapport technique'),
+ ('rapport_financier', 'Rapport financier'),
+ ('rapport_avancement', "Rapport d'avancement"),
+ ('convention', 'Convention'),
+ ('rapport_final', 'Rapport final'),
+ ('autre', 'Autre'),
+ ]
+
+ projet = models.ForeignKey(
+ Projet,
+ on_delete=models.CASCADE,
+ related_name='documents',
+ verbose_name="Projet"
+ )
+ date_ajout = models.DateTimeField(
+ auto_now_add=True,
+ verbose_name="Date d'ajout"
+ )
+ nom_document = models.CharField(
+ max_length = 100,
+ choices = NOM_DOCUMENT_CHOICES,
+ verbose_name="Type de document"
+ )
+ description = models.TextField(
+ blank = True,
+ verbose_name = "Description"
+ )
+ numero = models.CharField(
+ max_length = 100,
+ blank = True,
+ null = True,
+ verbose_name = "Numéro du document"
+ )
+ date_validite = models.DateField(
+ blank = True,
+ null = True,
+ verbose_name = "Date de validité"
+ )
+ fichier = models.FileField(
+ upload_to = 'documents_projets/',
+ verbose_name = "Fichier à télécharger"
+ )
+
+ def __str__(self):
+ return f"{self.nom_document} ({self.projet})"
+
+class ActiviteProjet(models.Model):
+ """Modèle représentant le planning d'un projet, avec des activités associées et un statut."""
+ projet = models.ForeignKey(
+ Projet,
+ on_delete = models.CASCADE,
+ verbose_name = "Projet"
+ )
+ titre = models.CharField(
+ max_length = 200,
+ verbose_name = "Titre de l'activité"
+ )
+ description = models.TextField(
+ blank = True,
+ null = True,
+ verbose_name = "Description de l'activité"
+ )
+ date_debut = models.DateField(verbose_name="Date de début")
+ date_fin = models.DateField(verbose_name="Date de fin")
+ annuler = models.BooleanField(
+ default = False,
+ verbose_name = "Annuler l'activité"
+ )
+ motif_annulation = models.TextField(
+ blank = True,
+ null = True,
+ verbose_name = "Motif d'annulation"
+ )
+
+ motif_changement_budget = models.TextField(
+ blank = True,
+ null = True,
+ verbose_name = "Motif de changement de budget"
+ )
+ budget_prevu = models.DecimalField(
+ max_digits = 15,
+ decimal_places = 2,
+ default = 0,
+ verbose_name = "Budget prévu"
+ )
+ budget_depense = models.DecimalField(
+ max_digits = 15,
+ decimal_places = 2,
+ default = 0,
+ verbose_name = "Budget dépensé"
+ )
+ besoin_ressource_materielle = models.TextField(
+ verbose_name="Besoin de ressources matérielles"
+ )
+
+ @property
+ def statut(self):
+ today = timezone.now().date()
+ if not self.annuler:
+ if self.date_fin < today:
+ return 'Terminé'
+ elif self.date_debut > today:
+ return 'À venir'
+ else:
+ return 'En cours'
+ else:
+ return 'Annulé'
+
+ def __str__(self):
+ return f"{self.titre} ({self.projet.nom_projet})"
+
+# class LivrableAttendu(models.Model):
+# """
+# Modèle représentant un livrable attendu pour une activité de projet,
+# avec des critères de validation.
+# """
+# activite = models.ForeignKey(
+# ActiviteProjet,
+# on_delete = models.CASCADE,
+# related_name = "livrables_attendus"
+# )
+# nom = models.CharField(max_length=255)
+
+# def __str__(self):
+# return f"{self.nom} (Activité: {self.activite.titre})"
+
+class LivrablesLivres(models.Model):
+ """Modèle représentant un livrable livré pour une activité de projet."""
+ activite = models.ForeignKey(
+ ActiviteProjet,
+ on_delete = models.CASCADE
+ )
+ # nom = models.ForeignKey(
+ # LivrableAttendu,
+ # on_delete = models.CASCADE
+ # )
+ nom = models.CharField(
+ max_length=255,
+ verbose_name="Nom du livrable"
+ )
+
+ fichier = models.FileField(
+ upload_to = 'fichier_livrables/',
+ blank = True,
+ null = True
+ )
+ def __str__(self):
+ return self.nom
\ No newline at end of file
diff --git a/gestion_projet/static/gestion_projet/js/creation_projet.js b/gestion_projet/static/gestion_projet/js/creation_projet.js
new file mode 100644
index 0000000..36c1a10
--- /dev/null
+++ b/gestion_projet/static/gestion_projet/js/creation_projet.js
@@ -0,0 +1,19 @@
+const boutonEnregistrerProjet = $("btnEnregistrerProjet");
+
+boutonEnregistrerProjet.addEventListener("click", function() {
+ const formulaire = $("formCreationProjet");
+ const formData = new FormData(formulaire);
+ fetch(formulaire.action, {
+ method: "POST",
+ body: formData,
+ headers: {
+ "X-CSRFToken": formData.get("csrfmiddlewaretoken")
+ }
+ })
+ .then(response => {
+ if (response.ok) {
+ window.location.reload();
+ alert("Projet enregistré avec succès !");
+ }
+ });
+});
\ No newline at end of file
diff --git a/gestion_projet/static/gestion_projet/js/enregistrement_bailleur.js b/gestion_projet/static/gestion_projet/js/enregistrement_bailleur.js
new file mode 100644
index 0000000..afe12ea
--- /dev/null
+++ b/gestion_projet/static/gestion_projet/js/enregistrement_bailleur.js
@@ -0,0 +1,23 @@
+const btnEnregistrerBailleur = document.getElementById('btnEnregistrerBailleur');
+
+btnEnregistrerBailleur.addEventListener('click', function() {
+ const form = document.getElementById('formBailleur');
+ const formData = new FormData(form);
+
+ fetch(form.action, {
+ method: 'POST',
+ body: formData,
+ headers: {
+ 'X-CSRFToken': formData.get('csrfmiddlewaretoken')
+ }
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ alert('Bailleur enregistré avec succès !');
+ window.location.reload();
+ } else {
+ alert('Ce bailleur existe déjà dans la base de données.');
+ }
+ });
+});
\ No newline at end of file
diff --git a/gestion_projet/static/gestion_projet/js/enregistrement_financement.js b/gestion_projet/static/gestion_projet/js/enregistrement_financement.js
new file mode 100644
index 0000000..1e9646c
--- /dev/null
+++ b/gestion_projet/static/gestion_projet/js/enregistrement_financement.js
@@ -0,0 +1,22 @@
+const btn_enregistrer_financement = document.getElementById('btn_enregistrer_financement');
+
+btn_enregistrer_financement.addEventListener('click', function() {
+ const form = document.getElementById('form_financement');
+ const formData = new FormData(form);
+ fetch(form.action, {
+ method: 'POST',
+ body: formData,
+ headers: {
+ 'X-CSRFToken': formData.get('csrfmiddlewaretoken')
+ }
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ alert(data.message);
+ window.location.reload();
+ }else {
+ alert(data.message);
+ }
+ });
+});
\ No newline at end of file
diff --git a/gestion_projet/static/gestion_projet/js/index.js b/gestion_projet/static/gestion_projet/js/index.js
new file mode 100644
index 0000000..eb6285e
--- /dev/null
+++ b/gestion_projet/static/gestion_projet/js/index.js
@@ -0,0 +1,64 @@
+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,
+ columns: [
+ {title: "Projet", field: "nom_projet"},
+ {title: "Source de financement", field: "source_financement"},
+ {title: "Budget Total", field: "budget"},
+ {title: "Budget RH", field: "budget_RH"},
+ {title: "Avancement", field: "avancement", formatter: "progress"},
+ {title: "Statut", field: "statut"},
+ ],
+ ajaxURL: url_liste_projet
+})
+
+const employes_affectes_projet = new Tabulator("#employes_affectes_projet", {
+ columns: [
+ {title: "Employé", field: "employe"},
+ {title: "Pourcentage d'affectation", field: "pourcentage_affectation"},
+ ],
+ placeholder: "Aucun employé affecté pour ce projet",
+})
+
+const bailleurs_projet = new Tabulator("#bailleurs_projet", {
+ columns: [
+ {title: "Bailleur", field: "bailleur"},
+ {title: "Pourcentage de financement", field: "pourcentage_financement"},
+ ],
+ placeholder: "Aucun bailleur attribué pour ce projet",
+})
+
+tableau_liste_projet.on("rowClick", function (row, rowData) {
+ const data = rowData.getData();
+ const modal = new bootstrap.Modal($("modalDetailProjet"));
+
+ $("detail_id_projet").value = data.id_projet;
+ $("detail_nom_projet").value = data.nom_projet;
+ $("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;
+ } else {
+ option.selected = false;
+ }
+ });
+ $("detail_budget").value = data.budget;
+ $("detail_budget_rh").value = data.budget_RH;
+ $("detail_description").value = data.description;
+ $("detail_statut").value = data.statut;
+
+ employes_affectes_projet.setData(`projet/liste-employes-par-projet/${$("detail_id_projet").value}`);
+ bailleurs_projet.setData(`projet/bailleurs/${data.id_projet}/`);
+
+ modal.show();
+})
+
+// $('detail-projet-form').addEventListener('submit', (e) => {
+// e.preventDefault();
+// new FormData($("detail-projet-form"));
+// })
\ No newline at end of file
diff --git a/gestion_projet/static/gestion_projet/js/liste_documents_projet.js b/gestion_projet/static/gestion_projet/js/liste_documents_projet.js
new file mode 100644
index 0000000..e586ae1
--- /dev/null
+++ b/gestion_projet/static/gestion_projet/js/liste_documents_projet.js
@@ -0,0 +1,20 @@
+const urlListeDocument = document.getElementById('listeDocuments').dataset.urllistedocument;
+
+const table_liste_documents = new Tabulator(document.getElementById('listeDocuments'), {
+ layout: "fitColumns",
+ placeholder: "Aucun document trouvé",
+ columns: [
+ { title: "Nom du Document", field: "nom_document" },
+ { title: "Numéro", field: "numero" },
+ { title: "Date de Validité", field: "date_validite", formatter: "datetime", formatterParams: {
+ inputFormat: "yyyy-MM-dd",
+ outputFormat: "dd/MM/yyyy"
+ }
+ },
+ { title: "Lien vers le Document", field: "lien_document", formatter:"link", formatterParams:{
+ target:"_blank",
+ }
+ },
+ ],
+ ajaxURL: urlListeDocument,
+});
\ No newline at end of file
diff --git a/gestion_projet/static/gestion_projet/js/suivi-activites.js b/gestion_projet/static/gestion_projet/js/suivi-activites.js
new file mode 100644
index 0000000..d0775d7
--- /dev/null
+++ b/gestion_projet/static/gestion_projet/js/suivi-activites.js
@@ -0,0 +1,59 @@
+const $ = (element) => document.getElementById(element)
+
+const url_liste_activite = $("tableau-liste-activite").dataset.urllisteactivite
+const tableau_liste_activite = new Tabulator("#tableau-liste-activite", {
+ columns: [
+ {title: "Activité", field: "titre"},
+ {title: "Date début", field: "date_debut"},
+ {title: "Date fin", field: "date_fin"},
+ {title: "Budget prévu", field: "budget_prevu"},
+ {title: "Budget dépensé", field: "budget_depense"},
+ {title: "Motif de changement de budget", field: "motif_changement_budget"},
+ {title: "Statut", field: "statut"},
+ ],
+ ajaxURL: url_liste_activite,
+})
+tableau_liste_activite.on("rowClick", function (row, rowData) {
+ const data = rowData.getData();
+ $("idDetailActivite").value = data.id;
+ $("titreDetailActivite").value = data.titre;
+ $("descriptionDetailActivite").value = data.description;
+ $("date_debutDetailActivite").value = data.date_debut;
+ $("date_finDetailActivite").value = data.date_fin;
+ $("statutDetailActivite").value = data.statut;
+ $("budget_prevuDetailActivite").value = data.budget_prevu;
+ $("besoin_ressources_materiellesDetailActivite").value = data.besoin_ressource_materielle;
+ const modal = new bootstrap.Modal($("modalDetailActivite"));
+ modal.show();
+
+ fetch(`liste-des-livrables/${data.id}/`)
+ .then(response => response.json())
+ .then(livrables => {
+ tableau_liste_livrable.setData(livrables);
+ })
+})
+
+const tableau_liste_livrable = new Tabulator("#listeLivrables", {
+ columns: [
+ {title: "Livrable", field: "titre"},
+ {title: "Lien du livrable", field: "lien", formatter: "link", formatterParams: {blank: true}},
+ ],
+ placeholder: "Aucun livrable trouvé",
+})
+
+$("btnMiseAJourDepense").addEventListener("click", function() {
+ const modal = new bootstrap.Modal($("modalDepenseActivite"));
+ bootstrap.Modal.getOrCreateInstance($("modalDetailActivite")).hide();
+ const idActivite = $("idDetailActivite").value;
+ const budgetPrevu = $("budget_prevuDetailActivite").value;
+
+ $("id_activite_depense").value = idActivite;
+ $("budget_prevu").value = budgetPrevu;
+ modal.show();
+})
+
+$("btnAnnulerActivite").addEventListener("click", function(event) {
+ new bootstrap.Modal($("modalAnnulerActivite")).show();
+ $("id_activite_annulation").value = $("idDetailActivite").value;
+ bootstrap.Modal.getOrCreateInstance($("modalDetailActivite")).hide();
+})
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/ajout-financement.html b/gestion_projet/templates/gestion_projet/ajout-financement.html
new file mode 100644
index 0000000..642fc29
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/ajout-financement.html
@@ -0,0 +1,37 @@
+{% extends "BASE.html" %}
+{% load static %}
+{% block 'titre_page' %} Gestion des projets {% endblock %}
+{% block 'contenu' %}
+
+
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+
+
+
Ajout du financement au projet (Nom du projet ici)
+
+
+
+
+{% endblock %}
+{% block 'modal' %}
+ {% include "gestion_projet/parts/modalAjoutProjet.html" %}
+{% endblock %}
+{% block 'js' %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/creation_projet.html b/gestion_projet/templates/gestion_projet/creation_projet.html
new file mode 100644
index 0000000..c04c5d4
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/creation_projet.html
@@ -0,0 +1,32 @@
+{% extends "BASE.html" %}
+{% load static %}
+{% block 'titre_page' %} Gestion des projets {% endblock %}
+{% block 'contenu' %}
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+ {% comment %}
Enregistrement d'un nouveau projet
{% endcomment %}
+
+
+
Enregistrement d'un nouveau projet
+
+
+
+
+{% endblock %}
+{% block 'modal' %}
+{% endblock %}
+{% block 'js' %}
+{% endblock %}
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/index.html b/gestion_projet/templates/gestion_projet/index.html
new file mode 100644
index 0000000..70ba584
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/index.html
@@ -0,0 +1,61 @@
+{% extends "BASE.html" %}
+{% load static %}
+{% load tags_personnaliser %}
+{% block 'titre_page' %} Gestion des projets {% endblock %}
+{% block 'contenu' %}
+
Gestion des projets
+{% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+{% endif %}
+
+
+ Projets en cours
+
{{projet_en_cours}}
+
+
+ Budget Total (GNF)
+
{{budget_total}}
+
+
+ Personnel sous projet
+
{{nombre_personnel}}
+
+
+
+
+
La liste des projets
+ {% if user|has_group:"ressource_humaine" %}
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+{% endblock %}
+{% block 'modal' %}
+ {% include "gestion_projet/parts/modalAjoutProjet.html" %}
+ {% include "gestion_projet/parts/modalFinancement.html" %}
+ {% include "gestion_projet/parts/creation_bailleur.html" %}
+ {% include "gestion_projet/parts/modalDetailProjet.html" %}
+{% endblock %}
+{% block 'js' %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/creation_bailleur.html b/gestion_projet/templates/gestion_projet/parts/creation_bailleur.html
new file mode 100644
index 0000000..6fe4194
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/creation_bailleur.html
@@ -0,0 +1,20 @@
+
+
+
+
+
Ajouter un Bailleur
+
+
+
+
+
+
+
+
+
diff --git a/gestion_projet/templates/gestion_projet/parts/liste_document_projet.html b/gestion_projet/templates/gestion_projet/parts/liste_document_projet.html
new file mode 100644
index 0000000..eb63ad2
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/liste_document_projet.html
@@ -0,0 +1,16 @@
+
+
+
+
+
Liste des Documents
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalAjoutActivite.html b/gestion_projet/templates/gestion_projet/parts/modalAjoutActivite.html
new file mode 100644
index 0000000..d253eb3
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalAjoutActivite.html
@@ -0,0 +1,20 @@
+
+
+
+
+
Ajouter une Activité
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalAjoutDocument.html b/gestion_projet/templates/gestion_projet/parts/modalAjoutDocument.html
new file mode 100644
index 0000000..6787fad
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalAjoutDocument.html
@@ -0,0 +1,20 @@
+
+
+
+
+
Ajouter un Document
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalAjoutLivrable.html b/gestion_projet/templates/gestion_projet/parts/modalAjoutLivrable.html
new file mode 100644
index 0000000..6b2bb68
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalAjoutLivrable.html
@@ -0,0 +1,20 @@
+
+
+
+
+
Ajouter un livrable - (Nom du livrable)
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalAjoutProjet.html b/gestion_projet/templates/gestion_projet/parts/modalAjoutProjet.html
new file mode 100644
index 0000000..1be46ce
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalAjoutProjet.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
Ajouter un projet
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalAnnulerActivite.html b/gestion_projet/templates/gestion_projet/parts/modalAnnulerActivite.html
new file mode 100644
index 0000000..7793aaf
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalAnnulerActivite.html
@@ -0,0 +1,26 @@
+
+
+
+
+
Annuler l'activité
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalDetailActivite.html b/gestion_projet/templates/gestion_projet/parts/modalDetailActivite.html
new file mode 100644
index 0000000..fba8cd9
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalDetailActivite.html
@@ -0,0 +1,58 @@
+
+
+
+
+
Détails de l'activité
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Liste des livrables :
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalDetailProjet.html b/gestion_projet/templates/gestion_projet/parts/modalDetailProjet.html
new file mode 100644
index 0000000..a7f6087
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalDetailProjet.html
@@ -0,0 +1,115 @@
+{% load tags_personnaliser %}
+
+
+
+
+
Détails du projet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalFinancement.html b/gestion_projet/templates/gestion_projet/parts/modalFinancement.html
new file mode 100644
index 0000000..a075b41
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalFinancement.html
@@ -0,0 +1,20 @@
+
+
+
+
+
Ajout de financement
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/parts/modalMiseAJourDepense.html b/gestion_projet/templates/gestion_projet/parts/modalMiseAJourDepense.html
new file mode 100644
index 0000000..d796bb4
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/parts/modalMiseAJourDepense.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Mise à jour des dépenses
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_projet/templates/gestion_projet/suivi_activite.html b/gestion_projet/templates/gestion_projet/suivi_activite.html
new file mode 100644
index 0000000..24d44a6
--- /dev/null
+++ b/gestion_projet/templates/gestion_projet/suivi_activite.html
@@ -0,0 +1,64 @@
+{% extends "BASE.html" %}
+{% load static %}
+{% block 'titre_page' %} Gestion des projets {% endblock %}
+{% block 'contenu' %}
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+
+ Suivi des Activités ({{ nom_projet }} )
+
+
+
+ Budget Total (GNF)
+
{{ budget_total }}
+
+
+ Budget RH (GNF)
+
{{ budget_RH }}
+
+
+ Budget Dépensé (GNF)
+
{{budget_depense}}
+
+
+
+
+
La liste des Activités
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block 'modal' %}
+ {% include "gestion_projet/parts/modalAjoutActivite.html" %}
+ {% include "gestion_projet/parts/modalDetailActivite.html" %}
+ {% include "gestion_projet/parts/modalAjoutDocument.html" %}
+ {% include "gestion_projet/parts/modalMiseAJourDepense.html" %}
+ {% include "gestion_projet/parts/modalAjoutLivrable.html" %}
+ {% include "gestion_projet/parts/liste_document_projet.html" %}
+ {% include "gestion_projet/parts/modalAnnulerActivite.html" %}
+{% endblock %}
+{% block 'js' %}
+
+
+{% endblock %}
+
\ No newline at end of file
diff --git a/gestion_projet/tests.py b/gestion_projet/tests.py
new file mode 100644
index 0000000..de8bdc0
--- /dev/null
+++ b/gestion_projet/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/gestion_projet/urls.py b/gestion_projet/urls.py
new file mode 100644
index 0000000..6e3f087
--- /dev/null
+++ b/gestion_projet/urls.py
@@ -0,0 +1,122 @@
+from django.urls import path
+from . import views
+
+app_name = 'gestion_projet'
+
+urlpatterns = [
+ path(
+ '',
+ views.index,
+ name='index'
+ ),
+ path(
+ 'modifier-financement//',
+ views.modifier_financement_projet,
+ name='modifier-financement'
+ ),
+ path(
+ 'projet/creation/',
+ views.creation_projet,
+ name='creation-projet'
+ ),
+ path(
+ 'projet/modifier//',
+ views.modification_projet,
+ name='modifier-projet'
+ ),
+ path(
+ 'liste-projet/',
+ views.liste_projet,
+ name='liste-projet'
+ ),
+ path(
+ 'projet/suppression//',
+ views.suppression_projet,
+ name='projet-suppression'
+ ),
+ path(
+ 'projet/ajouter-financement',
+ views.ajouter_financement_projet,
+ name='ajouter_financement'
+ ),
+ path(
+ 'creation-bailleur',
+ views.creation_bailleur,
+ name='creation-bailleur'
+ ),
+ path(
+ 'projet/affectation/',
+ views.affecter_employe_projet,
+ name='affecter-employe-projet'
+ ),
+ path(
+ 'projet/ajout-de-document/',
+ views.ajouter_document_projet,
+ name='ajouter-document'
+ ),
+ path(
+ 'projet/bailleurs//',
+ views.liste_bailleurs,
+ name='liste-bailleurs'
+ ),
+ path(
+ 'activite/',
+ views.activites_projet,
+ name='activites-projet'
+ ),
+ path(
+ 'activite/ajouter/',
+ views.ajouter_activite_projet,
+ name='ajouter-activite'
+ ),
+ path(
+ 'activite/modifier//',
+ views.modifier_activite_projet,
+ name='modifier-activite'
+ ),
+ path(
+ 'activite/annuler/',
+ views.annuler_activite_projet,
+ name='annuler-activite'
+ ),
+ path(
+ 'activite/liste/',
+ 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,
+ name='liste-documents-projet'
+ ),
+ path(
+ 'activite/liste-des-livrables//',
+ views.liste_livrables_activite,
+ name='liste-livrables-activite'
+ ),
+ path(
+ 'projet/ajout-de-livrable/',
+ views.ajouter_livrables_projet,
+ name='ajouter-livrable'
+ ),
+ path(
+ 'activite/mise-a-jour-depense/',
+ views.mises_a_jour_depense_activite,
+ name='mettre-a-jour-depense'
+ ),
+ path(
+ 'projet/liste-employes-par-projet/',
+ views.liste_employes_affectes,
+ name='liste-employes-affectes'
+ ),
+ path(
+ 'projet/mises-a-jour-projet',
+ views.mises_a_jour_projet,
+ name='mises-a-jour-projet'
+ )
+]
\ No newline at end of file
diff --git a/gestion_projet/views.py b/gestion_projet/views.py
new file mode 100644
index 0000000..69bd619
--- /dev/null
+++ b/gestion_projet/views.py
@@ -0,0 +1,485 @@
+from datetime import date
+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 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,
+ DocumentProjetForm,
+ FinancementProjetFrom,
+ BailleurForm,
+ LivrablesLivresForm
+)
+
+def liste_projet(request):
+ """ Vue pour retourner la liste de tous les projet """
+ projets = Projet.objects.all().order_by('-created_at')
+
+ data = []
+ for p in projets:
+ financement = FinancementProjet.objects.filter(projet=p).select_related('bailleur')
+ data.append({
+ "id_projet": p.id_projet,
+ "nom_projet": p.nom_projet,
+ "date_debut": p.date_debut,
+ "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,
+ "statut": p.statut,
+ "avancement": p.avancement,
+ "domaine_recherche": [d.nom for d in p.domaine_recherche.all()],
+ "source_financement": [f.bailleur.nom_organisme for f in financement],
+ })
+
+ return JsonResponse(data, safe=False)
+
+def liste_employes_affectes(request, projet_id):
+ """ Vue pour retourner la liste des employés affectés à un projet spécifique """
+ employes = Employe.objects.filter(affectation__projet_id=projet_id).distinct()
+ data = []
+ for employe in employes:
+ data.append({
+ "employe": f"{employe.user.first_name} {employe.user.last_name}",
+ "pourcentage_affectation": Affectation.objects.get(employe=employe, projet__id_projet=projet_id).pourcentage_temps_affectation
+ })
+ return JsonResponse(data, safe=False)
+
+def liste_bailleurs(request, projet_id):
+ """ Vue pour retourner la liste des bailleurs associés à un projet spécifique """
+ bailleurs = FinancementProjet.objects.filter(projet_id=projet_id).select_related('bailleur')
+ data = []
+ for b in bailleurs:
+ data.append({
+ "bailleur": b.bailleur.nom_organisme,
+ "pourcentage_financement": b.pourcentage
+ })
+
+ return JsonResponse(data, safe=False)
+
+@login_required
+def index(request):
+ projets = Projet.objects.all().order_by('-created_at')
+ nombre_personnel = Affectation.objects.values('employe_id').distinct().count()
+ budget_total = sum([projet.budget for projet in projets if projet.budget or 0])
+ context = {
+ 'form': AffectationForm(),
+ 'form_ajout_financement': FinancementProjetFrom(),
+ 'form_ajout_bailleur': BailleurForm(),
+ 'bailleurs': Bailleur.objects.all(),
+ 'nombre_personnel': nombre_personnel,
+ 'budget_total': budget_total,
+ 'formulaire_creation_projet': ProjetForm(),
+ 'projet_en_cours': Projet.objects.filter(date_fin__gte=date.today()).count(),
+ }
+
+ return render(request, 'gestion_projet/index.html', context)
+
+@login_required
+def creation_projet(request):
+ """Vue pour créer un nouveau projet via un formulaire"""
+ formulaire_creation_projet = ProjetForm()
+ if request.method == "POST":
+ form = ProjetForm(request.POST)
+
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Projet créé avec succès.")
+ else:
+ messages.error(request, "Le formulaire transmis est invalide.")
+ else:
+ form = ProjetForm()
+ return render(
+ request,
+ "gestion_projet/creation_projet.html",
+ {
+ "formulaire_creation_projet": formulaire_creation_projet
+ }
+ )
+
+@login_required
+def mises_a_jour_projet(request):
+ """ Vue de mises à jour des informations du projet """
+ if request.method == "POST":
+ try:
+ projet = Projet.objects.get(id_projet = request.POST["id_projet"])
+ except Projet.DoesNotExist:
+ messages.error(request, "Ce projet n'existe pas.")
+ else:
+ projet_form = ProjetForm(request.POST, instance=projet)
+ if projet_form.is_valid():
+ projet_form.save()
+ messages.success(request, f"Le projet d'identifiant {request.POST['id_projet']} a été mis à jour avec succès.")
+ else:
+ messages.error(request, f"Les informations de modification transmises pour la modification du projet {request.POST['id_projet']} ne sont pas valides.")
+ else:
+ messages.error(request, "La méthode de transmission des données n'est pas valide.")
+ return redirect('gestion_projet:index')
+
+@login_required
+def creation_bailleur(request):
+ form = BailleurForm(request.POST)
+ if request.method == 'POST':
+ if form.is_valid():
+ form.save()
+ return JsonResponse({'success': True})
+ return JsonResponse({'success': False})
+
+@login_required
+def ajouter_financement_projet(request):
+ """Ajoute un financement à un projet en vérifiant que le total ne dépasse pas 100%"""
+
+ if request.method == 'POST':
+ pourcentage_recuperer = request.POST.get('pourcentage')
+ bailleur_id = request.POST.get('bailleur')
+ projet_id = request.POST.get('projet')
+ try:
+ projet = Projet.objects.get(id_projet=projet_id)
+ except Projet.DoesNotExist:
+ return JsonResponse({'success': False, 'message': "Le projet spécifié n’existe pas."})
+
+ try:
+ pourcentage_nouveau = Decimal(pourcentage_recuperer) if pourcentage_recuperer else Decimal(0)
+ except (InvalidOperation, TypeError):
+ return JsonResponse({'success': False, 'message': "Le pourcentage saisi n’est pas valide."})
+
+ financement_total_actuel = sum(financement.pourcentage for financement in FinancementProjet.objects.filter(projet=projet))
+ if financement_total_actuel + pourcentage_nouveau > 100:
+ return JsonResponse({'success': False, 'message': "Le total des financements dépasse 100%."})
+
+ if bailleur_id:
+ FinancementProjet.objects.create(
+ projet=projet,
+ bailleur_id=bailleur_id,
+ pourcentage=pourcentage_nouveau
+ )
+ return JsonResponse({'success': True, 'message': "Financement ajouté avec succès."})
+ else:
+ return JsonResponse({'success': False, 'message': "Aucun bailleur sélectionné."})
+ return JsonResponse({'success': False, 'message': "Requête invalide."})
+
+@login_required
+def modification_projet(request, projet_id):
+ """Vue pour éditer un projet existant via un formulaire pré-rempli"""
+ try:
+ projet = Projet.objects.get(id=projet_id)
+ except Projet.DoesNotExist:
+ messages.error(request, "Le projet spécifié n’existe pas.")
+ return redirect('projet-index')
+
+ if request.method == "POST":
+ form = ProjetForm(request.POST)
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Le projet a été modifié avec succès.")
+ return redirect('projet-index')
+ messages.error(request, "Erreur lors de la modification du projet.")
+ form = ProjetForm(instance=projet)
+ return render(request, 'gestion_projet/projet-edit.html', {'form': form, 'projets': projet})
+
+@login_required
+def suppression_projet(request, id):
+ """Vue pour supprimer un projet spécifique après confirmation de l'utilisateur"""
+ try:
+ projet = Projet.objects.get(id=id)
+ except Projet.DoesNotExist:
+ messages.error(request, "Le projet spécifié n’existe pas.")
+ return redirect('projet-index')
+
+ if request.method == "POST":
+ projet.delete()
+ messages.success(request, "Le projet a été supprimé avec succès.")
+ return redirect('projet-index')
+
+@login_required
+def affecter_employe_projet(request, projet_id):
+ """Vue pour affecter un employé à un projet avec vérification des contraintes d'affectation"""
+ try:
+ projet = Projet.objects.get(id=projet_id)
+ except Projet.DoesNotExist:
+ messages.error(request, "Le projet spécifié n’existe pas.")
+ return redirect('projet-index')
+
+ if request.method == 'POST':
+ form = AffectationForm(request.POST)
+ if form.is_valid():
+ employe = Employe.objects.get(id=form.cleaned_data['employe'].id)
+ date_fin_affectation = form.cleaned_data['date_fin_daffectation']
+ temps_nouveau = form.cleaned_data['temps_affectation']
+ date_affectation = form.cleaned_data['date_affectation']
+
+ if (date_fin_affectation and date_affectation):
+ total_affectation = (
+ Affectation.objects.filter(employe=employe)
+ .aggregate(total_pourcentage_affectation=Sum('temps_affectation'))
+ ['total_pourcentage_affectation'] or 0
+ )
+ if (date_fin_affectation < date_affectation):
+ messages.warning(request, "La date de fin d'affectation ne peut pas être antérieure à la date de début.")
+ return redirect('projet-index')
+ elif date_fin_affectation > projet.date_fin:
+ messages.warning(request, f"La date de fin de l'affectation ({date_fin_affectation}) ne peut pas dépasser la date de fin du projet ({projet.date_fin}).")
+ return redirect('projet-index')
+ elif total_affectation + temps_nouveau > 100:
+ messages.warning(
+ request,
+ f"Les pourcentages d'affectation de l'employé {employe.first_name} {employe.last_name} dépasse 100% sur les différents projets ({total_affectation + temps_nouveau}%)."
+ )
+ return redirect('projet-index')
+
+ Affectation.objects.update_or_create(
+ projet=projet,
+ employe=employe,
+ defaults={
+ 'date_affectation': form.cleaned_data['date_affectation'],
+ 'date_fin_daffectation': date_fin_affectation,
+ 'role': form.cleaned_data['role'],
+ 'temps_affectation': temps_nouveau
+ }
+ )
+ form = AffectationForm(initial={'projet': projet})
+ messages.error(request, "Erreur : Formulaire non valide.")
+ return redirect('projet-index')
+
+def modifier_financement_projet(request, financement_id):
+ try:
+ financement = FinancementProjet.objects.get(id=financement_id)
+ except FinancementProjet.DoesNotExist:
+ messages.error(request, "Le financement spécifié n’existe pas.")
+ return redirect('projet-index')
+ projet = financement.projet
+ if request.method == 'POST':
+ try:
+ nouveau_pourcentage = Decimal(request.POST.get('pourcentage', '0'))
+ except InvalidOperation:
+ messages.error(request, "Le pourcentage saisi est invalide.")
+ return redirect('projet-index')
+ pourcentage_total_financement = (
+ FinancementProjet.objects.filter(projet=projet)
+ .exclude(id=financement.id)
+ .aggregate(total_financement=Sum('pourcentage'))['pourcentage_total_financement'] or 0
+ )
+ if pourcentage_total_financement + nouveau_pourcentage > 100:
+ messages.error(request, f"Le total des financements dépasse 100% ({pourcentage_total_financement + nouveau_pourcentage}%).")
+ return redirect('projet-index')
+ financement.pourcentage = nouveau_pourcentage
+ financement.save()
+ messages.success(request, "Financement modifié avec succès.")
+ return redirect('projet-index')
+
+@login_required
+def activites_projet(request):
+ try:
+ employe = Employe.objects.get(user=request.user)
+ except Employe.DoesNotExist:
+ messages.error(request, "Impossible d'accéder au menu 'Suivi des activités' car votre profil Utilisateur n'est lié à aucun profil Employe. Veuillez contacter l'administrateur.")
+ return redirect("gestion_conges:conge")
+
+ try:
+ Affectation.objects.get(employe=employe, date_fin_daffectation__gte = timezone.now().date(), role='chef_projet')
+ except Affectation.DoesNotExist :
+ messages.error(request, "Seuls les chefs de projet ont accès à l'onglet 'Suivi des Activités'")
+ return redirect("gestion_conges:conge")
+
+ projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first()
+ if projet :
+ context = {
+ **model_to_dict(projet),
+ "nom_projet": projet.projet.nom_projet,
+ "budget_total": projet.projet.budget,
+ "budget_RH": projet.projet.budget_RH,
+ "form_ajout_activite": ActiviteProjetForm(),
+ "form_ajout_document": DocumentProjetForm(),
+ "form_ajout_livrable": LivrablesLivresForm(),
+ }
+ else :
+ context = {
+ "form_ajout_activite": ActiviteProjetForm(),
+ "form_ajout_document": DocumentProjetForm(),
+ "form_ajout_livrable": LivrablesLivresForm(),
+ }
+ return render(request, 'gestion_projet/suivi_activite.html', context)
+
+@login_required
+def ajouter_activite_projet(request):
+ """Vue pour ajouter une activité à un projet spécifique via un formulaire"""
+ employe = Employe.objects.get(user=request.user)
+ projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first()
+
+ if request.method == "POST":
+ form = ActiviteProjetForm(request.POST)
+ if form.is_valid():
+ activite = form.save(commit=False)
+ activite.projet = projet.projet
+ activite.budget_depense = request.POST["budget_prevu"]
+ activite.save()
+ messages.success(request, "Activité ajoutée avec succès !")
+ else:
+ messages.error(request, "Erreur : vérifiez les informations saisies.")
+ return redirect('gestion_projet:activites-projet')
+
+@login_required
+def liste_activites_projet(request):
+ """Vue pour retourner la liste des activités d'un projet spécifique"""
+ employe = Employe.objects.get(user=request.user)
+ projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first()
+ if projet:
+ activites = ActiviteProjet.objects.filter(projet_id=projet.projet.id_projet).order_by('-date_debut')
+ else:
+ activites = []
+ data = []
+ for a in activites:
+ data.append({
+ "id": a.id,
+ "titre": a.titre,
+ "date_debut": a.date_debut,
+ "date_fin": a.date_fin,
+ "statut": a.statut,
+ "budget_prevu": a.budget_prevu,
+ "budget_depense": a.budget_depense,
+ "motif_changement_budget": a.motif_changement_budget,
+ "besoin_ressource_materielle": a.besoin_ressource_materielle,
+ "description": a.description,
+ })
+ return JsonResponse(data, safe=False)
+
+@login_required
+def liste_livrables_activite(request, activite_id):
+ """Vue pour retourner la liste des livrables attendus d'une activité spécifique"""
+ livrables = LivrablesLivres.objects.filter(activite__id=activite_id)
+ data = []
+ for livrable in livrables:
+ print(livrable.fichier.url)
+ data.append({
+ "titre": livrable.nom,
+ "lien": livrable.fichier.url if livrable.fichier else "",
+ })
+ return JsonResponse(data, safe=False)
+
+@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"""
+ if request.method == "POST":
+ activite_id = request.POST.get("id_activite")
+ budget_depense = request.POST.get("budget_depense")
+ motif = request.POST.get("motif", "").strip()
+ try:
+ activite = ActiviteProjet.objects.get(id=activite_id)
+ activite.budget_depense = Decimal(budget_depense)
+ if Decimal(budget_depense) != activite.budget_prevu:
+ activite.motif_changement_budget = motif
+ else:
+ activite.motif_changement_budget = ""
+ activite.save()
+ messages.success(request, f"Dépenses mises à jour pour l’activité '{activite.titre}'.")
+ except (ActiviteProjet.DoesNotExist, InvalidOperation):
+ messages.error(request, "Erreur lors de la mise à jour des dépenses.")
+ return redirect("gestion_projet:activites-projet")
+
+@login_required
+def ajouter_livrables_projet(request):
+ """Vue pour ajouter un livrable à une activité de projet spécifique via un formulaire"""
+
+ if request.method == "POST":
+ form = LivrablesLivresForm(request.POST, request.FILES)
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Livrable ajouté avec succès !")
+ else:
+ messages.error(request, "Erreur : vérifiez les informations saisies.")
+ return redirect('gestion_projet:activites-projet')
+
+@login_required
+def ajouter_document_projet(request):
+ """Ajoute un document à un projet"""
+ employe = Employe.objects.get(user=request.user)
+ projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first()
+ if request.method == "POST":
+ form = DocumentProjetForm(request.POST, request.FILES)
+ if form.is_valid():
+ document = form.save(commit=False)
+ document.projet = projet.projet
+ document.save()
+ messages.success(request, "Document ajouté avec succès !")
+ else:
+ messages.error(request, "Erreur : le document n’a pas pu être enregistré.")
+ return redirect('gestion_projet:activites-projet')
+
+def liste_documents_projet(request):
+ employe = Employe.objects.get(user=request.user)
+ projet = Affectation.objects.filter(employe=employe, role='chef_projet', date_fin_daffectation__gte=timezone.now().date()).select_related('projet').first()
+ if projet:
+ documents = DocumentProjet.objects.filter(projet__id_projet=projet.projet.id_projet)
+ else:
+ documents = []
+ data = []
+ for d in documents:
+ data.append({
+ "nom_document": d.nom_document,
+ "numero": d.numero,
+ "date_validite": d.date_validite,
+ "lien_document": d.fichier.url if d.fichier else "",
+ })
+ return JsonResponse(data, safe=False)
+
+def modifier_activite_projet(request, id):
+ """Vue pour modifier une activité de projet spécifique via un formulaire pré-rempli"""
+ try:
+ activite = ActiviteProjet.objects.get(id=id)
+ except ActiviteProjet.DoesNotExist:
+ messages.error(request, "L'activité spécifiée n’existe pas.")
+ return redirect('activites-projet')
+ if request.method == 'POST':
+ form = ActiviteProjetForm(request.POST, instance=activite)
+ if form.is_valid():
+ activite.besoin_ressource_materielle = bool(request.POST.get("besoin_ressource_materielle"))
+ form.save()
+ messages.success(request, f"Activité « {activite.titre} » modifiée avec succès.")
+ else:
+ messages.error(request, "Erreur lors de la modification de l'activité.")
+ return redirect('activites-projet')
+
+ form = ActiviteProjetForm(instance=activite)
+ return render(
+ request,
+ 'gestion_projet/activite.html', {
+ 'form': form,
+ 'activite': activite,
+ }
+ )
+
+def annuler_activite_projet(request):
+ """Vue pour annuler une activité de projet spécifique après confirmation de l'utilisateur"""
+ print(request.POST)
+ if request.method != "POST":
+ messages.error(request, "Requête invalide.")
+ return redirect('gestion_projet:activites-projet')
+ try:
+ activite = ActiviteProjet.objects.get(id=request.POST.get('id_activite'))
+ except ActiviteProjet.DoesNotExist:
+ messages.error(request, "L'activité spécifiée n’existe pas.")
+ return redirect('gestion_projet:activites-projet')
+ if request.method == "POST":
+ activite.annuler = True
+ activite.motif_annulation = request.POST.get("motif_annulation", "").strip()
+ activite.save()
+ messages.success(request, f"L'activité '{activite.titre}' a été annulée avec succès.")
+ return redirect('gestion_projet:activites-projet')
\ No newline at end of file
diff --git a/gestion_salle/__init__.py b/gestion_salle/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gestion_salle/admin.py b/gestion_salle/admin.py
new file mode 100644
index 0000000..ea5d68b
--- /dev/null
+++ b/gestion_salle/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/gestion_salle/apps.py b/gestion_salle/apps.py
new file mode 100644
index 0000000..569d365
--- /dev/null
+++ b/gestion_salle/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class GestionSalleConfig(AppConfig):
+ name = 'gestion_salle'
diff --git a/gestion_salle/forms.py b/gestion_salle/forms.py
new file mode 100644
index 0000000..e40caf0
--- /dev/null
+++ b/gestion_salle/forms.py
@@ -0,0 +1,32 @@
+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']
+ widgets = {
+ 'date_debut': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
+ 'date_fin': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
+ 'heure_debut': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
+ 'heure_fin': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}),
+ 'motif_reservation': forms.Textarea(attrs={'rows': 3, 'cols': 40, 'style':'resize:none;', 'class': 'form-control'}),
+ 'salle': forms.Select(attrs={'class': 'form-select'}),
+ }
+ besoin_zoom = forms.BooleanField(
+ required=False,
+ label="Besoin d'un lien Zoom ?",
+ widget=forms.CheckboxInput(attrs={'class': 'form-check-input', 'id': 'id_besoin_zoom'})
+ )
+ besoin_ordi = forms.BooleanField(
+ required=False,
+ label="Besoin d'ordinateur ?",
+ widget=forms.CheckboxInput(attrs={'class': 'form-check-input', 'id': 'id_besoin_ordi'})
+ )
+
+class RefusReservationForm(forms.Form):
+ motif_refus = forms.CharField(
+ label= "Motif du refus",
+ widget=forms.Textarea(attrs={'rows': 3, 'cols': 40, 'style': 'resize:none;'}),
+ required=True
+ )
diff --git a/gestion_salle/migrations/0001_initial.py b/gestion_salle/migrations/0001_initial.py
new file mode 100644
index 0000000..f477d96
--- /dev/null
+++ b/gestion_salle/migrations/0001_initial.py
@@ -0,0 +1,34 @@
+# Generated by Django 5.2.13 on 2026-04-17 12:03
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('gestion_employe', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Reservation',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('salle', models.CharField(choices=[('formation', 'Salle de formation'), ('reunion', 'Salle de réunion'), ('lien_zoom', 'Lien Zoom')], max_length=100)),
+ ('date_demande', models.DateTimeField(auto_now_add=True)),
+ ('date_debut', models.DateField()),
+ ('date_fin', models.DateField(blank=True, null=True)),
+ ('heure_debut', models.TimeField()),
+ ('heure_fin', models.TimeField()),
+ ('besoin_zoom', models.BooleanField(default=False, verbose_name="Besoin d'un lien Zoom ?")),
+ ('besoin_ordi', models.BooleanField(default=False, verbose_name="Besoin d'un ordinateur ?")),
+ ('lien_zoom', models.URLField(blank=True, null=True, verbose_name='Lien Zoom')),
+ ('motif_reservation', models.TextField()),
+ ('statut', models.CharField(choices=[('en_attente', 'En attente'), ('validee', 'Validée'), ('refusee', 'Refusée'), ('annulee', 'Annulée')], default='en_attente', max_length=25)),
+ ('employe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion_employe.employe')),
+ ],
+ ),
+ ]
diff --git a/gestion_salle/migrations/__init__.py b/gestion_salle/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/gestion_salle/models.py b/gestion_salle/models.py
new file mode 100644
index 0000000..5d1d4e2
--- /dev/null
+++ b/gestion_salle/models.py
@@ -0,0 +1,33 @@
+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 = [
+ ('formation', 'Salle de formation'),
+ ('reunion', 'Salle de réunion'),
+ ('lien_zoom', 'Lien Zoom'),
+ ]
+ 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)
+ date_demande = models.DateTimeField(auto_now_add=True)
+ date_debut = models.DateField()
+ date_fin = models.DateField(blank=True,null=True)
+ heure_debut = models.TimeField()
+ heure_fin = models.TimeField()
+ besoin_zoom = models.BooleanField(default=False, verbose_name="Besoin d'un lien Zoom ?")
+ besoin_ordi = models.BooleanField(default=False, verbose_name="Besoin d'un ordinateur ?")
+ lien_zoom = models.URLField(blank=True, null=True, verbose_name="Lien Zoom")
+ motif_reservation = models.TextField()
+ statut = models.CharField(choices=STATUT, default='en_attente', max_length=25)
+
+ def __str__(self):
+ return f"{self.salle} - {self.employe.user.first_name} {self.employe.user.last_name} le {self.date_reservation}"
\ No newline at end of file
diff --git a/gestion_salle/static/gestion_salle/js/index.js b/gestion_salle/static/gestion_salle/js/index.js
new file mode 100644
index 0000000..8b5bff8
--- /dev/null
+++ b/gestion_salle/static/gestion_salle/js/index.js
@@ -0,0 +1,193 @@
+const $ = (element) => document.getElementById(element);
+const { Schedule } = calendarjs;
+
+let dateAUtiliser = new Date().toISOString().split('T')[0];
+let currentReservationId = null;
+
+const calendrier = Schedule(document.getElementById('planning-reservation'), {
+ type: 'weekdays',
+ value: dateAUtiliser,
+ validRange: ['08:00', '18:00'],
+ ondblclick: function(self, event) {
+ 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;
+ console.log(data);
+ $("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.statut !== "annulee"){
+ $("motif_refus_container").className = "d-none";
+ }else{
+ $("motif_refus").value=data.motif_refus;
+ }
+ })
+ }
+});
+
+$("modalReservation").addEventListener('shown.bs.modal', (e) => {
+ $("id_salle").value = $("liste-salle").value;
+})
+
+$('semaineDate').addEventListener('change', () => {
+ calendrier.value = $('semaineDate').value;
+ calendrier.render();
+})
+
+evenement_defini = null
+
+$("liste-salle").addEventListener("change", (e) => {
+ if(evenement_defini === null){
+ evenement_defini = calendrier.getData();
+ }
+ evenements = evenement_defini;
+ evenement_filtrer = evenements.filter((evenement) => {
+ if(evenement.title == $("liste-salle").value){
+ return evenement
+ }
+ })
+ calendrier.setData(evenement_filtrer);
+})
+
+function chargement_evenement(){
+ const url = $("planning-reservation").dataset.url;
+ fetch (url)
+ .then(response => response.json())
+ .then(data => {
+ calendrier.setData(data);
+ })
+}
+
+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;
+
+ 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;
+ const urlvalider = $("formulaire-details").dataset.urlvalider;
+
+ fetch(
+ urlvalider,
+ {
+ 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($("ajoutZoom")){
+ $("ajoutZoom").addEventListener("click", (e) => {
+ e.preventDefault();
+ bootstrap.Modal.getOrCreateInstance($("modalDetailReservation")).hide();
+ new bootstrap.Modal($("modalZoom")).show();
+ })
+}
+
+if($("refuserReservation")){
+ $("refuserReservation").addEventListener("click", (e) => {
+ const csrf = document.querySelector("[name=csrfmiddlewaretoken]").value;
+ const url = e.currentTarget.dataset.lienrefus;
+ const idRes = $("id_reservation_detail").value;
+
+ fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Requested-With": "XMLHttpRequest",
+ "X-CSRFToken": csrf
+ },
+ body: JSON.stringify({ "id_reservation": idRes })
+ })
+ .then(response => response.json())
+ .then(data => alert(data.message))
+ .catch(error => console.error("Erreur:", error));
+ });
+}
+
+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: "Heure de début", field: "heure_debut"},
+ {title: "Heure de fin", field: "heure_fin"},
+ {title: "Motif de reservation", field: "motif_reservation"},
+ ],
+ placeholder: "Aucune reservation en attente.",
+ ajaxURL : $("tableau-reservation-attente").dataset.reservationattentes
+})
+
+tableau_reservation_attente.on("rowClick", (row, rowData) => {
+ const data = rowData.getData();
+
+ console.log(data);
+
+ if(data.besoin_zoom === false){
+ $("lien_zoom_container").className = 'd-none';
+ }
+
+ if(data.statut !== "refusee"){
+ $("motif_refus_container").className = 'd-none';
+ }
+
+ $("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_evenement").value=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_ordi;
+ $("lien_zoom").value=data.lien_zoom;
+ $("motif_refus").value=data.motif_refus;
+
+ const modal = new bootstrap.Modal($("modalDetailReservation"));
+ bootstrap.Modal.getOrCreateInstance($("modalReservationAttente")).hide();
+ modal.show();
+})
\ No newline at end of file
diff --git a/gestion_salle/templates/gestion_salle/index.html b/gestion_salle/templates/gestion_salle/index.html
new file mode 100644
index 0000000..b0fa88e
--- /dev/null
+++ b/gestion_salle/templates/gestion_salle/index.html
@@ -0,0 +1,59 @@
+{% extends "BASE.html" %}
+{% load static %}
+{% block 'titre_page' %} Gestion des projets {% endblock %}
+{% block 'css' %}
+
+
+{% endblock %}
+{% block 'contenu' %}
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+
+
+
Reservation de salle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block 'modal' %}
+ {% include 'gestion_salle/parts/modalCreationReservation.html' %}
+ {% include 'gestion_salle/parts/ModaleAjoutLienZoom.html' %}
+ {% include 'gestion_salle/parts/ModalRefusReservation.html' %}
+ {% include 'gestion_salle/parts/modalDetailResevation.html' %}
+ {% include 'gestion_salle/parts/modalListeValidation.html' %}
+{% endblock %}
+{% block 'js' %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/gestion_salle/templates/gestion_salle/parts/ModalRefusReservation.html b/gestion_salle/templates/gestion_salle/parts/ModalRefusReservation.html
new file mode 100644
index 0000000..96d358d
--- /dev/null
+++ b/gestion_salle/templates/gestion_salle/parts/ModalRefusReservation.html
@@ -0,0 +1,22 @@
+
+
+
+
+
Motif du refus
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_salle/templates/gestion_salle/parts/ModaleAjoutLienZoom.html b/gestion_salle/templates/gestion_salle/parts/ModaleAjoutLienZoom.html
new file mode 100644
index 0000000..0c7c534
--- /dev/null
+++ b/gestion_salle/templates/gestion_salle/parts/ModaleAjoutLienZoom.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
Ajouter/Modifier le lien Zoom
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_salle/templates/gestion_salle/parts/modalCreationReservation.html b/gestion_salle/templates/gestion_salle/parts/modalCreationReservation.html
new file mode 100644
index 0000000..0db15f8
--- /dev/null
+++ b/gestion_salle/templates/gestion_salle/parts/modalCreationReservation.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Nouvelle reservation
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_salle/templates/gestion_salle/parts/modalDetailResevation.html b/gestion_salle/templates/gestion_salle/parts/modalDetailResevation.html
new file mode 100644
index 0000000..fb84ee8
--- /dev/null
+++ b/gestion_salle/templates/gestion_salle/parts/modalDetailResevation.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ Détails de la reservation ()
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gestion_salle/templates/gestion_salle/parts/modalListeValidation.html b/gestion_salle/templates/gestion_salle/parts/modalListeValidation.html
new file mode 100644
index 0000000..0f50e01
--- /dev/null
+++ b/gestion_salle/templates/gestion_salle/parts/modalListeValidation.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Liste des reservation en attente de validation
+
, because it just gets in the way.
+ from_box.parentNode.removeChild(p);
+ } else if (p.classList.contains("help")) {
+ // Move help text up to the top so it isn't below the select
+ // boxes or wrapped off on the side to the right of the add
+ // button:
+ from_box.parentNode.insertBefore(p, from_box.parentNode.firstChild);
+ }
+ }
+
+ //
or
+ const selector_div = quickElement('div', from_box.parentNode);
+ // Make sure the selector div is at the beginning so that the
+ // add link would be displayed to the right of the widget.
+ from_box.parentNode.prepend(selector_div);
+ selector_div.className = is_stacked ? 'selector stacked' : 'selector';
+
+ //
");
+ addButton = $this.filter(":last").next().find("a");
+ }
+ }
+ addButton.on('click', addInlineClickHandler);
+ };
+
+ const addInlineClickHandler = function(e) {
+ e.preventDefault();
+ const template = $("#" + options.prefix + "-empty");
+ const row = template.clone(true);
+ row.removeClass(options.emptyCssClass)
+ .addClass(options.formCssClass)
+ .attr("id", options.prefix + "-" + nextIndex);
+ addInlineDeleteButton(row);
+ row.find("*").each(function() {
+ updateElementIndex(this, options.prefix, totalForms.val());
+ });
+ // Insert the new form when it has been fully edited.
+ row.insertBefore($(template));
+ // Update number of total forms.
+ $(totalForms).val(parseInt(totalForms.val(), 10) + 1);
+ nextIndex += 1;
+ // Hide the add button if there's a limit and it's been reached.
+ if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
+ addButton.parent().hide();
+ }
+ // Show the remove buttons if there are more than min_num.
+ toggleDeleteButtonVisibility(row.closest('.inline-group'));
+
+ // Pass the new form to the post-add callback, if provided.
+ if (options.added) {
+ options.added(row);
+ }
+ row.get(0).dispatchEvent(new CustomEvent("formset:added", {
+ bubbles: true,
+ detail: {
+ formsetName: options.prefix
+ }
+ }));
+ };
+
+ /**
+ * The "X" button that is part of every unsaved inline.
+ * (When saved, it is replaced with a "Delete" checkbox.)
+ */
+ const addInlineDeleteButton = function(row) {
+ if (row.is("tr")) {
+ // If the forms are laid out in table rows, insert
+ // the remove button into the last table cell:
+ row.children(":last").append('