feature: Affichage de la liste des bailleur #14

Closed
fatima wants to merge 6 commits from liste_bailleur into main
276 changed files with 46132 additions and 0 deletions
Showing only changes of commit c28b14fb98 - Show all commits

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# db.sqlite3
# venv/*
# media/*
__pycache__/
*.pyc
db.sqlite3
venv/
.env

0
SIRH/__init__.py Normal file
View File

16
SIRH/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for SIRH project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'SIRH.settings')
application = get_asgi_application()

159
SIRH/settings.py Normal file
View File

@@ -0,0 +1,159 @@
"""
Django settings for SI_RH project.
Generated by 'django-admin startproject' using Django 5.2.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure--wdb9t(77rvyac$_q!n5gw86&0r(0&&j171v9h!-_$jahsza*5'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ["https://support.cerfig.org", "support.cerfig.org"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'gestion_employe',
'gestion_conge',
'gestion_projet',
'gestion_salle',
'simple_sso.sso_server'
]
LOGIN_URL = 'login'
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'SIRH.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "SIRH", "templates", "SIRH")],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'SIRH.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.mysql',
# 'NAME': 'sirh',
# 'USER': 'sirh',
# 'PASSWORD': 'sirh-cerfig',
# 'HOST': 'localhost',
# 'PORT': '3306',
# }
# }
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'ssl0.ovh.net'
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_USE_TLS = False
EMAIL_HOST_USER = 'support.it@cerfig.org'
EMAIL_HOST_PASSWORD = 'Cerfig2025'
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

BIN
SIRH/static/SIRH/PS1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

2078
SIRH/static/SIRH/icons.css Normal file

File diff suppressed because it is too large Load Diff

BIN
SIRH/static/SIRH/per1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
SIRH/static/SIRH/user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -0,0 +1,157 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PRINCICALA</title>
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'styles.css' %}">
<link rel="stylesheet" href="{% static 'icons.css' %}">
</head>
<body>
<style>
.bg-orange-dark {
background: linear-gradient(90deg, #993d00, #b34700); /* oranges plus sombres */
color: white;
}
.sidebar .nav-link {
color: white;
font-weight: 500;
}
.sidebar .nav-link:hover {
background-color: rgba(255,255,255,0.2);
border-radius: 5px;
}
.logo {
max-width: 90px;
}
main {
margin-left: 220px;
}
.stat-card {
background: rgb(255, 255, 255);
padding: 20px;
border-radius: 8px;
box-shadow: 0px 2px 5px rgba(113, 11, 11, 0.1);
}
</style>
<div class="container-fluid">
<div class="row flex-nowrap">
<nav class="col-12 col-md-3 col-lg-2 sidebar p-3">
<div class="text-center mb-4">
<img src="{% static 'CERFIF.jpg' %}" alt="Logo" class="logo mb-5">
<h4 class="text-white">CERFIG</h4>
</div>
<ul class="nav flex-column">
<li class="nav-item mb-2">
<a href="/" class="nav-link"><i class="bi bi-speedometer2"></i> Tableau de bord</a>
</li>
<li class="nav-item mb-2">
<a href="#" class="nav-link"><i class=" bi bi-airplane"></i> Demande de congé </a>
</li>
<li class="nav-item mb-2">
<a href="#" class="nav-link"><i class="bi bi-people"></i> Gestion des Employés</a>
</li>
<li class="nav-item mb-2">
<a href="#" class="nav-link"><i class="bi bi-folder"></i> Gestion des Projets</a>
</li>
<li class="nav-item mb-2">
<a href="#" class="nav-link"><i class="bi bi-airplane"></i> Gestion des Congés</a>
</li>
<li class="nav-item mb-2">
<a href="#" class="nav-link"><i class="bi bi-graph-up"></i>Rapports et Statistiques</a>
</li>
<li class="nav-item mb-2">
<a href="#" class="nav-link"><i class="bi bi-gear"></i> Paramètres</a>
</li>
<li>
<a href="#" class="nav-link"><i class="bi bi-box-arrow-right"></i> Déconnexion</a>
</li>
</ul>
</nav>
<!-- Contenu principal -->
<main class="col px-4 py-4">
<h1 class="mb-4">Bienvenue sur le tableau de bord</h1>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="stat-card">
<h5><i class="bi bi-airplane"></i> Congés en attente</h5>
<p class="display-6 fw-bold"> {{ nombre_conges_attente }}</p>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<h5><i class="bi bi-people"></i> Nombre Employés </h5>
<p class="display-6 fw-bold">{{ nombre_employes }}</p>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<h5><i class="bi bi-kanban"></i> Projets en cours</h5>
</div>
</div>
</div>
<div class="card p-3">
<h3>
<i class="bi bi-people"></i> La liste des employés en congé</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Employé</th>
<th>Type de congé</th>
<th>Date de début</th>
<th>Date de fin</th>
<th>Statut</th>
</tr>
</thead>
<tbody>
{% for employe in personnes_en_conge %}
<tr>
<td>{{ employe.nom }} {{ employe.prenom }}</td>
<td>{{ employe.type }}</td>
<td>{{ employe.date_debut }}</td>
<td>{{ employe.date_fin }}</td>
<td>
<span class="badge bg-success">{{ employe.statut }} </span>
<td>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">Personne en conge </td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</main>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,197 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Connexion SI-RH</title>
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'styles.css' %}">
</head>
<body class="d-flex justify-content-center align-items-center min-vh-100 ">
<style>
.bg-orange-dark {
background: linear-gradient(90deg, #b35400, #cc6600);
color: white;
}
</style>
<!-- Formulaire de connexion -->
<div class="p-4 shadow-sm rounded bg-white" style="max-width: 400px; margin: auto;">
<img src="{% static 'CERFIF.jpg' %}" alt="Logo" class="logo mb-3"
style="display: block; margin-left: auto; margin-right: auto; width: 60%; max-width: 500px; height: auto;">
{% if messages %}
<!-- Modal message -->
<div class="modal fade" id="messageModal" tabindex="-1" aria-labelledby="messageModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark
{% if messages.0.tags == 'error' %}bg-orange-dark text-white
{% elif messages.0.tags == 'success' %}bg-orange-dark text-white
{% else %}bg-orange-dark text-white{% endif %}">
<h5 class="modal-title" id="messageModalLabel">
{% if messages.0.tags == 'error' %}Erreur
{% elif messages.0.tags == 'success' %}Succès
{% else %}Information{% endif %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var messageModal = new bootstrap.Modal(document.getElementById('messageModal'));
messageModal.show();
});
</script>
{% endif %}
<form method="POST" action="{% url 'login' %}">
{% csrf_token %}
<div class="mb-3">
<label for="username">Nom dutilisateur</label>
<input type="text" name="username" class="form-control" placeholder="Entrez votre nom dutilisateur" required>
</div>
<div class="mb-3 position-relative">
<label for="password">Mot de passe</label>
<input type="password" name="password" class="form-control" placeholder="Entrez votre mot de passe" required>
<i class="bi bi-eye toggle-password" onclick="togglePassword()"
style="position:absolute; right:10px; top:38px; cursor:pointer;"></i>
</div>
<button type="submit" class="btn btn-primary w-100">Se connecter</button>
</form>
<p class="mt-3 text-center">
<a href="#" data-bs-toggle="modal" data-bs-target="#resetPasswordModal">Mot de passe oublié ?</a>
</p>
</div>
<!-- Modale de réinitialisation -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content p-4">
<h5 class="modal-title mb-3" id="resetPasswordModalLabel">Réinitialisation du mot de passe</h5>
<form id="resetPasswordForm">
{% csrf_token %}
<div class="mb-3">
<label for="email">Entrez votre email</label>
<input type="email" name="email" class="form-control" placeholder="Email" required>
</div>
<div id="resetMessage" class="mb-3"></div>
<button type="submit" class="btn btn-warning w-100">Envoyer le lien</button>
</form>
</div>
</div>
</div>
{% if show_politique_modal %}
<!-- Modal Politique d'utilisation -->
<div class="modal fade" id="politiqueModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-orange-dark text-white">
<h5 class="modal-title">Politique d'utilisation de SI-RH</h5>
</div>
<div class="modal-body">
<p>Veuillez lire et accepter notre politique d'utilisation avant d'accéder à votre compte.</p>
<h6>Confidentialité des données</h6>
<p>Vos informations personnelles et professionnelles sont protégées. Toute divulgation non autorisée est strictement interdite.</p>
<h6>Utilisation autorisée</h6>
<p>Lapplication est réservée à un usage professionnel. Toute utilisation à des fins personnelles ou non autorisées est prohibée.</p>
<h6>Sécurité des comptes</h6>
<p>Ne partagez jamais vos identifiants. Changez votre mot de passe régulièrement et en cas de suspicion dintrusion.</p>
<h6>Responsabilités de lutilisateur</h6>
<p>Vous êtes responsable des actions effectuées via votre compte. Signalez toute anomalie ou problème à léquipe RH ou à ladministrateur.</p>
<h6>Acceptation et conformité</h6>
<p>En cliquant sur <strong>“Jaccepte”</strong>, vous confirmez avoir lu et accepté cette politique. Le non-respect peut entraîner une suspension ou une révocation de laccès.</p>
</div>
<div class="modal-footer">
<form method="POST" action="{% url 'accepter-politique' %}" style="display:inline;">
{% csrf_token %}
<button type="submit" class="btn btn-success">J'accepte</button>
</form>
<form method="POST" action="{% url 'refuser-politique' %}" style="display:inline;">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Refuser</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var politiqueModal = new bootstrap.Modal(document.getElementById('politiqueModal'));
politiqueModal.show();
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var politiqueModal = new bootstrap.Modal(document.getElementById('politiqueModal'));
politiqueModal.show();
});
</script>
>
<script>
document.addEventListener("DOMContentLoaded", function() {
var modal = new bootstrap.Modal(document.getElementById('politiqueModal'));
modal.show();
});
</script>
{% endif %}
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script>
// Afficher / masquer le mot de passe
function togglePassword() {
const input = document.querySelector('input[name="password"]');
input.type = input.type === 'password' ? 'text' : 'password';
}
// AJAX pour réinitialisation
$('#resetPasswordForm').on('submit', function(e) {
e.preventDefault();
const email = $('input[name="email"]').val();
const csrfToken = $('[name=csrfmiddlewaretoken]').val();
$.ajax({
url: "{% url 'password_reset' %}",
type: 'POST',
data: {
'email': email,
'csrfmiddlewaretoken': csrfToken
},
success: function(response) {
$('#resetMessage').html('<div class="alert alert-success">Email envoyé ! Vérifiez votre boîte de réception.</div>');
},
error: function() {
$('#resetMessage').html('<div class="alert alert-danger">Erreur lors de lenvoi. Vérifiez lemail.</div>');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,150 @@
{% load static %}
{% load roles %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PRINCICALA</title>
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'styles.css' %}">
</head>
<body>
<!-- ===================== -->
<!-- SIDEBAR RESPONSIVE -->
<!-- ===================== -->
<nav class="sidebar bg-orange-dark text-white d-none d-md-block col-md-3 col-lg-2 p-3"
style="background: linear-gradient(90deg, #993d00, #b34700); position: fixed; height: 100vh; overflow-y: auto;">
<!-- Profil utilisateur -->
<div class="text-center mb-4 d-flex flex-column align-items-center" style="margin-top:20px;">
<h6 class="fw-bold text-white mb-2">{{ user.first_name }} {{ user.last_name }}</h6>
<a href="{% url 'mon_profil' %}">
{% if user.photo %}
<img src="{{ user.photo.url }}" alt="Profil"
class="rounded-circle img-profil mx-auto"
style="width: 80px; height: 80px; object-fit: cover;">
{% else %}
<div class="rounded-circle bg-secondary d-flex justify-content-center align-items-center"
style="width: 80px; height: 80px;">
<i class="bi bi-person-circle text-white" style="font-size: 2rem;"></i>
</div>
{% endif %}
</a>
</div>
<!-- Liens du menu -->
<ul class="nav flex-column">
{% if user|has_role:"Employe" or user|has_role:"Administrateur" %}
<li class="nav-item mb-2">
<a href="{% url 'mon_profil' %}" class="nav-link text-white"><i class="bi bi-person-circle"></i> Mon profil</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'activites-projet' %}" class="nav-link text-white"><i class="bi bi-list-task"></i> Suivi des Activités</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'employe-conge' %}" class="nav-link text-white"><i class="bi bi-airplane"></i> Mes Congés</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'mes_formations' %}" class="nav-link text-white"><i class="bi bi-journal-text"></i> Mes certificats</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'salle' %}" class="nav-link text-white"><i class="bi bi-calendar-check"></i> Réserver une salle</a>
</li>
{% endif %}
{% if user|has_role:"RH" or user|has_role:"Directeur" or user|has_role:"Assistante" %}
<li class="nav-item mb-2">
<a href="{% url 'employe-index' %}" class="nav-link text-white"><i class="bi bi-speedometer2"></i> Tableau de bord</a>
</li>
{% if user|has_role:"Assistante" or user|has_role:"RH" %}
<li class="nav-item mb-2">
<a href="{% url 'activites-projet' %}" class="nav-link text-white"><i class="bi bi-list-task"></i> Suivi des Activités</a>
</li>
{% endif %}
{% if user|has_role:"Directeur" %}
<li class="nav-item mb-2">
<a href="{% url 'directeur' %}" class="nav-link text-white"><i class="bi bi-graph-up"></i> Suivi des Projets</a>
</li>
{% endif %}
<li class="nav-item mb-2">
<a href="{% url 'projet-index' %}" class="nav-link text-white"><i class="bi bi-folder"></i> Gestion des Projets</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'conge' %}" class="nav-link text-white"><i class="bi bi-airplane"></i> Congés</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'mes_formations' %}" class="nav-link text-white"><i class="bi bi-journal-text"></i> Mes certificats</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'salle' %}" class="nav-link text-white"><i class="bi bi-calendar-check"></i> Réserver une salle</a>
</li>
{% if user|has_role:"RH" or user|has_role:"Directeur" %}
<li class="nav-item mb-2">
<a href="{% url 'rapport-rh' %}" class="nav-link text-white"><i class="bi bi-graph-up"></i> Rapports et Statistiques</a>
</li>
{% endif %}
<li class="nav-item mb-2">
<a href="{% url 'mon_profil' %}" class="nav-link text-white"><i class="bi bi-person-circle"></i> Mon profil</a>
</li>
{% endif %}
{% if user|has_role:"Administrateur" %}
<li class="nav-item mb-2">
<a href="{% url 'gestion-utilisateurs' %}" class="nav-link text-white"><i class="bi bi-person-gear"></i> Gestion des Utilisateurs</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'parametres-rh' %}" class="nav-link text-white"><i class="bi bi-gear"></i> Paramètres</a>
</li>
{% endif %}
<li class="nav-item mb-2">
<a href="{% url 'deconnexion' %}" class="nav-link text-white"><i class="bi bi-box-arrow-right"></i> Déconnexion</a>
</li>
</ul>
</nav>
<!-- ===================== -->
<!-- MENU MOBILE (offcanvas) -->
<!-- ===================== -->
<!-- Bouton burger (visible uniquement sur téléphone) -->
<nav class="navbar navbar-dark bg-orange-dark d-md-none p-2" style="background: linear-gradient(90deg, #993d00, #b34700);">
<button class="btn btn-light" type="button" data-bs-toggle="offcanvas" data-bs-target="#menuMobile">
<i class="bi bi-list"></i> Menu
</button>
</nav>
<!-- Menu Offcanvas mobile -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="menuMobile" style="background: linear-gradient(90deg, #993d00, #b34700); color: white;">
<div class="offcanvas-header">
<h5 class="offcanvas-title">{{ user.first_name }} {{ user.last_name }}</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body">
<!-- Tu peux copier ici le même <ul> du menu principal -->
<ul class="nav flex-column">
<li class="nav-item mb-2">
<a href="{% url 'mon_profil' %}" class="nav-link text-white"><i class="bi bi-person-circle"></i> Mon profil</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'activites-projet' %}" class="nav-link text-white"><i class="bi bi-list-task"></i> Activités</a>
</li>
<li class="nav-item mb-2">
<a href="{% url 'deconnexion' %}" class="nav-link text-white"><i class="bi bi-box-arrow-right"></i> Déconnexion</a>
</li>
</ul>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<div class="alert alert-{{ message.tags }} d-flex align-items-center" role="alert">
{% if message.tags == 'success' %}
<i class="bi bi-check-circle-fill me-2"></i>
{% elif message.tags == 'warning' %}
<i class="bi bi-exclamation-triangle-fill me-2"></i>
{% elif message.tags == 'error' %}
<i class="bi bi-x-circle-fill me-2"></i>
{% endif %}
{{ message }}
</div>

View File

@@ -0,0 +1,384 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profil utilisateur</title>
<!-- CSS -->
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'styles.css' %}">
<style>
.bg-orange-dark {
background: linear-gradient(90deg, #993d00, #b34700); /* oranges plus sombres */
color: white;
}
.card-body p { margin-bottom: 0.5rem; }
.main {
margin-left: 0px;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row flex-nowrap">
<!-- Sidebar -->
{% include 'menu_principal.html' %}
<!-- Main -->
<main class="col px-2 py-4" style="margin-left: 280px;">
{% if messages %}
<!-- Modal Message Django -->
<div class="modal fade" id="messageModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark text-white">
<h5 class="modal-title">
{% if messages.0.tags == 'error' %}
Erreur
{% elif messages.0.tags == 'success' %}
Succès
{% else %}
Information
{% endif %}
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Icône Notification -->
<div class="notification-bell position-fixed top-0 end-0 m-3" data-bs-toggle="modal" data-bs-target="#modalNotifications" style="z-index:1050; cursor:pointer;">
<i class="bi bi-bell-fill fs-4"></i>
<span class="badge bg-danger">{{ notifications_contrats|length }}</span>
</div>
<!-- Modal Notifications Contrats -->
<div class="modal fade" id="modalNotifications" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark text-white">
<h5 class="modal-title"><i class="bi bi-bell me-2"></i>Contrats proches de la fin</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{% if notifications_contrats %}
<ul class="list-group">
{% for notif in notifications_contrats %}
<h5>Votre Contrat <strong>{{ notif.numero }}</strong> signé le <strong>{{ notif.date }}</strong>
se termine dans
<span class="badge bg-danger rounded-pill">{{ notif.jours_restants }} jours</span>
</h5>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">Aucun contrat proche de la fin.</p>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
<!-- Profil -->
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h4 class="mb-0 fw-bold">
<i class="bi bi-person-circle me-2"></i> Mon Profil
</h4>
</div>
<div class="card-body p-3">
<!-- Infos personnelles -->
<div class="row mb-3">
<div class="col-md-6">
<p><strong>Nom :</strong> {{ employe.last_name|default:"Non renseigné" }}</p>
<p><strong>Prénom :</strong> {{ employe.first_name|default:"Non renseigné" }}</p>
<p><strong>Matricule :</strong> {{ employe.matricule|default:"Non renseigné" }}</p>
</div>
<div class="col-md-6">
<p><strong>Département :</strong> {{ employe.departement|default:"Non renseigné" }}</p>
<p><strong>Email :</strong> {{ employe.email|default:"Non renseigné" }}</p>
<p><strong>Téléphone :</strong> {{ employe.telephone|default:"Non renseigné" }}</p>
</div>
</div>
<!-- Contrats -->
<h5 class="mt-3">Contrats</h5>
{% for contrat in contrats %}
<div class="mb-3 p-3 border rounded">
<div class="row">
<div class="col-md-6">
<p><strong>Numéro :</strong> {{ contrat.numero_contrat }}</p>
<p><strong>Type :</strong> {{ contrat.type_contrat }}</p>
<p><strong>Date début :</strong> {{ contrat.date_debut|date:"d/m/Y" }}</p>
<p><strong>Date fin :</strong> {{ contrat.date_fin|date:"d/m/Y"|default:"Non précisée" }}</p>
</div>
<div class="col-md-6">
<p><strong>Salaire mensuel :</strong>
{% if contrat.salaire_mensuel %}
{{ contrat.salaire_mensuel }} GNF
{% else %}
Non précisé
{% endif %}
</p>
<p><strong>Statut :</strong>
<span class="badge
{% if contrat.statut_auto == 'Actif' %}bg-success
{% elif contrat.statut_auto == 'Terminé' %}bg-danger
{% elif contrat.statut_auto == 'Suspendu' %}bg-warning text-dark
{% else %}bg-secondary{% endif %}">
{{ contrat.statut_auto }}
</span>
</p>
<p><strong>Solde congé :</strong> {{ contrat.solde_conge|default:"Non précisé" }} jours</p>
<p><strong>Fichier :</strong>
{% if contrat.fichier_contrat %}
<a href="{{ contrat.fichier_contrat.url }}" target="_blank">
<i class="bi bi-file-earmark-pdf me-1"></i> Télécharger
</a>
{% else %}
<span class="text-muted">Aucun fichier</span>
{% endif %}
</p>
</div>
</div>
</div>
{% empty %}
<p class="text-danger">Aucun contrat trouvé.</p>
{% endfor %}
<!-- Projets -->
<h5 class="mt-3">Projets</h5>
<ul class="list-group mb-3">
{% for a in affectations %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
{{ a.projet.nom_projet }} - {{ a.temps_affectation }}%
{% if a.projet.statut %}
- <span class="badge bg-info text-dark">{{ a.projet.statut }}</span>
{% endif %}
</div>
<div>
<button type="button" class="btn bg-orange-dark btn-sm text-white"
data-bs-toggle="modal" data-bs-target="#modalFicheProjet{{ a.projet.id }}">
<i class="bi bi-info-circle"></i> Détails
</button>
</div>
</li>
{% empty %}
<li class="list-group-item text-muted text-center">Aucun projet assigné.</li>
{% endfor %}
</ul>
</div>
<div class="card-footer bg-white d-flex justify-content-end">
<button type="button" class="btn btn-outline-secondary me-2" onclick="window.history.back()">Fermer</button>
<a href="{% url 'profil' %}" class="btn bg-orange-dark text-white">
<i class="bi bi-pencil-square me-1"></i> Renseigner les autres informations
</a>
</div>
</div>
</div>
</div>
</div>
{% for a in affectations %}
<!-- Modal Détails Projet -->
<div class="modal fade" id="modalFicheProjet{{ a.projet.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-orange-dark text-white">
<h5 class="modal-title">
<i class="bi bi-info-circle me-2"></i>Détails du projet {{ a.projet.nom_projet }}
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p><strong>Nom :</strong> {{ a.projet.nom_projet }}</p>
<p><strong>Numéro convention :</strong> {{ a.projet.numero_convention }}</p>
<p><strong>Dates :</strong> {{ a.projet.date_debut|date:"d/m/Y" }} → {{ a.projet.date_fin|date:"d/m/Y" }}</p>
<p><strong>Type :</strong> {{ a.projet.get_type_projet_display }}</p>
<p><strong>Domaine :</strong> {{ a.projet.get_domaine_recherche_display }}</p>
<p><strong>Budget :</strong> {{ a.projet.budget }} GNF</p>
<p><strong>Budget RH :</strong> {{ a.projet.budget_RH }} GNF</p>
<p><strong>Statut :</strong> <span class="badge bg-info">{{ a.projet.statut }}</span></p>
<p><strong>Description :</strong> {{ a.projet.description }}</p>
<h6 class="mt-4">Employés affectés :</h6>
<ul class="list-group mb-3">
{% for aff in a.projet.affectations.all %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ aff.employe }} <span class="badge bg-orange-dark">{{ aff.temps_affectation }}%</span>
</li>
{% empty %}
<li class="list-group-item text-muted">Aucun employé affecté</li>
{% endfor %}
</ul>
<h6>Bailleurs :</h6>
<ul class="list-group mb-3">
{% for f in a.projet.financements.all %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ f.bailleur.nom }} <span class="badge bg-success">{{ f.pourcentage }}%</span>
</li>
{% empty %}
<li class="list-group-item text-muted">Aucun bailleur enregistré</li>
{% endfor %}
</ul>
<h6 class="mt-4">Documents du projet :</h6>
<ul class="list-group">
{% for doc in a.projet.documents.all %}
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<strong>{{ doc.get_nom_document_display }}</strong><br>
pour le projet {{ a.projet.nom_projet }}<br>
{% if doc.description %}
<small class="text-muted">{{ doc.description }}</small><br>
{% endif %}
<small class="text-muted">
{% if doc.numero %}
{{ doc.numero }}
{% endif %}
</small>
<br> <small></small>
document ajouter le {{ doc.date_ajout|date:"d/m/Y" }}</small>
{% if doc.date_validite %}
<br><small class="text-muted"> et Valide jusqu'au {{ doc.date_validite|date:"d/m/Y" }}</small>
{% endif %}
</div>
</li>
{% empty %}
<li class="list-group-item text-muted">Aucun document disponible</li>
{% endfor %}
</ul>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-1"></i> Fermer
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</main>
</div>
</div>
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
// Ferme les modales ouvertes avant d'en ouvrir une autre
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
modal.addEventListener('show.bs.modal', function () {
document.querySelectorAll('.modal.show').forEach(openModal => {
if (openModal !== modal) {
const instance = bootstrap.Modal.getInstance(openModal);
if (instance) instance.hide();
}
});
});
});
// Répare le bug d'écran figé après fermeture
document.addEventListener('hidden.bs.modal', function () {
if (!document.querySelector('.modal.show')) {
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
}
});
const forms = document.querySelectorAll('form[action*="ajouter-document"]');
forms.forEach(form => {
form.addEventListener('submit', function (e) {
const btn = form.querySelector('button[type="submit"]');
const modalEl = form.closest('.modal');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Enregistrement...';
}
if (modalEl) {
const modalInstance = bootstrap.Modal.getInstance(modalEl);
if (modalInstance) modalInstance.hide();
}
});
});
const messageModalEl = document.getElementById('messageModal');
if (messageModalEl) {
const messageModal = new bootstrap.Modal(messageModalEl);
messageModal.show();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,223 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Paramètres RH</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'styles.css' %}">
</head>
<body>
<style>
.bg-orange-dark {
background: linear-gradient(90deg, #993d00, #b34700);
color: white;
}
</style>
<div class="container-fluid">
<div class="row flex-nowrap">
<!-- Sidebar -->
{% include 'menu_principal.html' %}
<!-- Main -->
<main class="col px-2 py-4" style="margin-left: 280px;">
{% if messages %}
<!-- Modal message -->
<div class="modal fade" id="messageModal" tabindex="-1" aria-labelledby="messageModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark
{% if messages.0.tags == 'error' %}bg-orange-dark text-white
{% elif messages.0.tags == 'success' %}bg-orange-dark text-white
{% else %}bg-orange-dark text-white{% endif %}">
<h5 class="modal-title" id="messageModalLabel">
{% if messages.0.tags == 'error' %}Erreur
{% elif messages.0.tags == 'success' %}Succès
{% else %}Information{% endif %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var messageModal = new bootstrap.Modal(document.getElementById('messageModal'));
messageModal.show();
});
</script>
{% endif %}
<h2 class="mb-4"><i class="bi bi-gear-fill"></i> Paramètres Administration</h2>
<div class="row">
<!-- Colonne Départements -->
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5><i class="bi bi-building"></i> Départements</h5>
<button class="btn bg-orange-dark btn-sm" data-bs-toggle="modal" data-bs-target="#modalAjouterDepartement">
<i class="bi bi-plus-circle"></i> Ajouter
</button>
</div>
<!-- Modal Ajout -->
<div class="modal fade" id="modalAjouterDepartement" tabindex="-1">
<div class="modal-dialog">
<form method="POST">
{% csrf_token %}
<input type="hidden" name="ajouter_departement" value="1">
<div class="modal-content">
<div class="modal-header bg-orange-dark text-white">
<h5 class="modal-title">Ajouter un département</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{{ form_departement.as_p }}
</div>
<div class="modal-footer">
<button type="submit" class="btn bg-orange-dark">Enregistrer</button>
</div>
</div>
</form>
</div>
</div>
<!-- Liste des départements -->
<ul class="list-group">
{% for d in departements %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ d.nom }}
<div>
<!-- Bouton détails -->
<button class="btn btn-sm btn-outline-info" data-bs-toggle="modal" data-bs-target="#modalDetailDepartement{{ d.id }}">
<i class="bi bi-eye"></i> Détails
</button>
<!-- Bouton modification -->
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#modalModifierDepartement{{ d.id }}">
<i class="bi bi-pencil-square"></i>
</button>
<form method="POST" style="display:inline;">
{% csrf_token %}
<input type="hidden" name="supprimer_departement" value="1">
<input type="hidden" name="departement_id" value="{{ d.id }}">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Voulez-vous vraiment supprimer ce département ?');">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</li><!-- Modal détails -->
<div class="modal fade" id="modalDetailDepartement{{ d.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark text-white">
<h5 class="modal-title">Détails du département</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p><strong>Nom :</strong> {{ d.nom }}</p>
<p><strong>Chef :</strong> {% if d.chef %}{{ d.chef.first_name }} {{ d.chef.last_name }}{% else %}Non défini{% endif %}</p>
<!-- Liste des employés du département -->
{% for emp in d.employe_set.all %}
<p>- {{ emp.username }}</p>
{% empty %}
<p>Aucun employé associé</p>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="modalModifierDepartement{{ d.id }}" tabindex="-1">
<div class="modal-dialog">
<form method="POST">
{% csrf_token %}
<input type="hidden" name="modifier_departement" value="1">
<input type="hidden" name="departement_id" value="{{ d.id }}">
<div class="modal-content">
<div class="modal-header bg-orange-dark text-white">
<h5 class="modal-title">Modifier le département</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<!-- Nom du département -->
<div class="mb-3">
<label for="nom_{{ d.id }}" class="form-label">Nom du département</label>
<input type="text" class="form-control" id="nom_{{ d.id }}" name="nom" value="{{ d.nom }}" required>
</div>
<!-- Sélection du chef -->
<div class="mb-3">
<label for="chef_{{ d.id }}" class="form-label">Chef du département</label>
<select class="form-select" id="chef_{{ d.id }}" name="chef">
<option value="">-- Aucun chef --</option>
{% for emp in employes %}
<option value="{{ emp.id }}" {% if d.chef and emp.id == d.chef.id %}selected{% endif %}>
{{ emp.first_name }} {{ emp.last_name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn bg-orange-dark">Modifier</button>
</div>
</div>
</form>
</div>
</div>
{% endfor %}
</ul>
</div>
<!-- Colonne Groupes -->
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5><i class="bi bi-person-badge-fill"></i> Groupes et rôles</h5>
<button class="btn bg-orange-dark btn-sm">Ajouter</button>
</div>
<ul class="list-group">
{% for g in groupes %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ g.name }}
<a href="{% url 'admin:auth_group_change' g.id %}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,205 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Tableau de Bord RH</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>
<style>
h1 { font-weight: 700; }
.card { border-radius: 15px; }
.chart-container { position: relative; height: 350px; }
table { min-width: 100%; }
/* Couleurs oranges */
.bg-orange-dark { background: linear-gradient(90deg, #b35400, #cc6600); color: white; }
.bg-orange { background: #ff9f1c; color: white; }
.bg-orange-light { background: #ffc107; color: #212529; }
/* Badges */
.badge-warning { background-color: #ffc107; color: #212529; }
.badge-success { background-color: #28a745; color: #fff; }
.badge-primary { background-color: #007bff; color: #fff; }
@media print {
body * { visibility: hidden; }
.printable-table, .printable-table * { visibility: visible; }
.printable-table { position: absolute; top: 0; left: 0; width: 100%; }
}
</style>
</head>
<body>
<div class="container-fluid py-4">
<div class="row flex-nowrap">
{% include 'menu_principal.html' %}
<main class="col px-2 py-4" style="margin-left: 280px;">
<h1 class="mb-5 text-center"><i class="bi bi-bar-chart-line-fill me-2"></i>Tableau de Bord RH</h1>
<!-- Filtres -->
<form method="GET" class="row g-3 mb-5">
<div class="col-md-4">
<select name="mois" class="form-select">
<option value="">Tous les mois</option>
{% for num, nom in mois_liste %}
<option value="{{ num }}" {% if mois_selectionne == num %}selected{% endif %}>{{ nom }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<select name="departement" class="form-select">
<option value="">Tous les départements</option>
{% for d in departements %}
<option value="{{ d.id }}" {% if departement_selectionne == d.id %}selected{% endif %}>{{ d.nom }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-outline-primary w-100"><i class="bi bi-search"></i> Filtrer</button>
</div>
</form>
<!-- Cartes statistiques -->
<div class="row g-4 mb-5">
<div class="col-md-3"><div class="card text-center"><div class="card-body"><h6 class="fw-bold"><i class="bi bi-people me-2"></i>Total Employés</h6><h3 class="fw-bold">{{ total_employes }}</h3></div></div></div>
<div class="col-md-3"><div class="card text-center"><div class="card-body"><h6 class="fw-bold"><i class="bi bi-gender-male me-2"></i>Hommes</h6><h3 class="fw-bold">{{ hommes }}</h3></div></div></div>
<div class="col-md-3"><div class="card text-center"><div class="card-body"><h6 class="fw-bold"><i class="bi bi-gender-female me-2"></i>Femmes</h6><h3 class="fw-bold">{{ femmes }}</h3></div></div></div>
<div class="col-md-3"><div class="card text-center"><div class="card-body"><h6 class="fw-bold"><i class="bi bi-clock-history me-2"></i>Contrats expirants(2mois)</h6><h3 class="fw-bold">{{ expirants }}</h3></div></div></div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3"><div class="card text-center"><div class="card-header fw-bold"><i class="bi bi-airplane me-2"></i>Congés en attente</div><div class="card-body"><h5 class="display-6 fw-bold">{{ conges_attente }}</h5></div></div></div>
<div class="col-md-3"><div class="card text-center"><div class="card-body"><h6 class="fw-bold"><i class="bi bi-kanban me-2"></i>Projets</h6><h6>(actifs / total)</h6><h3 class="fw-bold">{{ nb_projets_actifs }} / {{ nb_projets }}</h3></div></div></div>
<div class="col-md-3"><div class="card text-center"><div class="card-header fw-bold"><i class="bi bi-clock-history me-2"></i>Âge moyen</div><div class="card-body"><h5 class="display-6 fw-bold">{{ age_moyen }} ans</h5></div></div></div>
</div>
<!-- Graphiques -->
<div class="row g-4 mb-5">
<div class="col-lg-6">
<div class="card shadow"><div class="card-header bg-orange-dark text-white"><i class="bi bi-pie-chart-fill me-2"></i> Répartition par Département</div>
<div class="card-body chart-container"><canvas id="chartDepartement"></canvas></div></div>
</div>
<div class="col-lg-6">
<div class="card shadow"><div class="card-header bg-orange-dark text-white"><i class="bi bi-pie-chart-fill me-2"></i> Répartition par genre</div>
<div class="card-body chart-container"><canvas id="chartSexe"></canvas></div></div>
</div>
</div>
<div class="row g-4 mb-6">
<div class="col-lg-6">
<div class="card shadow"><div class="card-header bg-orange-dark text-white"><i class="bi bi-bar-chart-fill me-2"></i> Projets (En cours / Terminés)</div>
<div class="card-body chart-container"><canvas id="chartProjets"></canvas></div></div>
</div>
<div class="col-lg-6">
<div class="card shadow"><div class="card-header bg-orange-dark text-white"><i class="bi bi-bar-chart-fill me-2"></i> Projets par domaine de recherche</div>
<div class="card-body chart-container"><canvas id="chartDomaine"></canvas></div></div>
</div>
</div>
</main>
</div>
</div>
<script>
const couleursOrange = ['#b35400', '#ff9f1c', '#ffc107', '#ffb84d', '#ffcc80'];
const departementLabels = {{ departement_labels|safe }};
const departementCounts = {{ departement_counts|safe }};
const sexeLabels = {{ sexe_labels|safe }};
const sexeCounts = {{ sexe_counts|safe }};
const projetLabels = {{ projet_labels|safe }};
const projetCounts = {{ projet_counts|safe }};
const ticketsLabels = ["Traités", "Non traités"];
const ticketsCounts = [{{ tickets_traite }}, {{ tickets_non_traite }}];
const domaineLabels = {{ domaine_labels|safe }};
const domaineCounts = {{ domaine_counts|safe }};
new Chart(document.getElementById("chartDepartement"), {
type: 'pie',
plugins: [ChartDataLabels],
data: {
labels: departementLabels,
datasets: [{ data: departementCounts, backgroundColor: couleursOrange }]
},
options: {
plugins: {
legend: { position: 'bottom' },
datalabels: { color: '#fff', font: { weight: 'bold', size: 14 }, formatter: v => v }
}
}
});
new Chart(document.getElementById("chartSexe"), {
type: 'doughnut',
plugins: [ChartDataLabels],
data: {
labels: sexeLabels,
datasets: [{ data: sexeCounts, backgroundColor: couleursOrange }]
},
options: {
plugins: {
legend: { position: 'bottom' },
datalabels: { color: '#fff', font: { weight: 'bold', size: 14 }, formatter: v => v }
}
}
});
new Chart(document.getElementById("chartProjets"), {
type: 'bar',
plugins: [ChartDataLabels],
data: {
labels: projetLabels,
datasets: [{ label:'Nombre de projets', data: projetCounts, backgroundColor: couleursOrange }]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
datalabels: { anchor:'end', align:'top', color:'#000', font:{ weight:'bold', size:16 }, formatter:v => v }
},
scales: {
y: { beginAtZero:true, ticks:{ stepSize:1, precision:0 }, title:{ display:true, text:'Nombre de projets' } },
x: { title:{ display:true, text:'' } }
}
}
});
new Chart(document.getElementById("chartDomaine"), {
type: 'bar',
plugins: [ChartDataLabels],
data: {
labels: domaineLabels,
datasets: [{ label:'Nombre de projets', data: domaineCounts, backgroundColor: couleursOrange }]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
datalabels: { anchor:'end', align:'top', color:'#000', font:{ weight:'bold', size:14 }, formatter:v => v }
},
scales: {
y: { beginAtZero:true, title:{ display:true, text:'Nombre de projets' }, ticks:{ precision:0, stepSize:1 } },
x: {
ticks:{ autoSkip:false, maxRotation:100, minRotation:30, callback: function(value){ return this.getLabelForValue(value); } }
}
}
}
});
document.getElementById('btnVoirRapport').addEventListener('click', function() {
const projetId = document.getElementById('projetId').value;
if(!projetId) return alert("Veuillez sélectionner un projet.");
fetch(`/rapports/projet/${projetId}/`)
.then(resp => { if(!resp.ok) throw new Error("Projet non trouvé"); return resp.text(); })
.then(html => { document.getElementById('contenuRapportProjet').innerHTML = html; })
.catch(err => { document.getElementById('contenuRapportProjet').innerHTML = "<p class='text-danger'>Erreur lors du chargement.</p>"; console.error(err); });
});
</script>
</body>
</html>

View File

View File

@@ -0,0 +1,33 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Mot de passe oublié</title>
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'styles.css' %}">
</head>
<body class="container mt-5">
<div class="p-4 shadow-sm rounded bg-white" style="max-width: 400px; margin:auto;">
<h2 class="mb-4 text-center" style="color:rgba(255, 157, 0, 0.895);">🔑 Réinitialiser le mot de passe</h2>
{% if messages %}
{% for message in messages %}
<div class="alert alert-info">{{ message }}</div>
{% endfor %}
{% endif %}
<form method="POST">
{% csrf_token %}
<div class="mb-3">
<label for="email">Adresse e-mail</label>
<input type="email" name="email" class="form-control" placeholder="Entrez votre email" required>
</div>
<button type="submit" class="btn btn-primary w-100">Envoyer le lien</button>
</form>
<p class="mt-3 text-center">
<a href="{% url 'login' %}">Retour à la connexion</a>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,620 @@
{% load static %}
{% load roles %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mes demandes</title>
<link rel="stylesheet" href="{% static 'bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'styles.css' %}">
<style>
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.bg-orange-dark {
background: linear-gradient(90deg, #b35400, #cc6600);
color: white;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row flex-nowrap">
<nav class="col-12 col-md-3 col-lg-2 sidebar p-3">
{% include 'menu_principal.html' %}
</nav>
<main class="col px-4 py-4">
<!-- Boutons Notifications -->
{% if notifications %}
<div class="d-flex justify-content-end mb-3">
<button type="button" class="btn position-relative" data-bs-toggle="modal" data-bs-target="#modalNotifications">
<i class="bi bi-bell-fill"></i> Notifications
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ notifications|length }}
</span>
</button>
</div>
{% endif %}
<!-- Bouton Notifications -->
{% if notifications_employe %}
<div class="d-flex justify-content-end mb-3">
<button type="button" class="btn position-relative" data-bs-toggle="modal" data-bs-target="#modalNotifications">
<i class="bi bi-bell-fill"></i> Notifications
<span id="badgeNotifications" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ notifications_employe|length }}
</span>
</button>
</div>
{% endif %}
<!-- Modal Notifications -->
<div class="modal fade" id="modalNotifications" tabindex="-1" aria-labelledby="modalNotificationsLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalNotificationsLabel">Mes Notifications</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
{% if notifications_employe %}
<ul class="list-group">
{% for notif in notifications_employe %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ notif.message|default:"Nouvelle notification" }}
<small class="text-muted">{{ notif.date_created|date:"d/m/Y H:i" }}</small>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">Aucune notification pour le moment.</p>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function () {
var modal = document.getElementById('modalNotifications');
modal.addEventListener('show.bs.modal', function () {
fetch("{% url 'notifications-lues' %}", {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}',
'Content-Type': 'application/json'
},
body: JSON.stringify({})
})
.then(res => res.json())
.then(data => {
if(data.status === "ok"){
document.getElementById('badgeNotifications').style.display = 'none';
}
});
});
});
</script>
<h1 class="mb-4">La liste des demandes de congés</h1>
<!-- Cartes Statistiques -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-white ">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-list-check me-2"></i> Total Demandes</h6>
<p class="card-text fs-4">{{ total }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white ">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-check-circle me-2"></i> Validés</h6>
<p class="card-text fs-4">{{ valides }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white ">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-x-circle me-2"></i> Refusés</h6>
<p class="card-text fs-4">{{ refuses }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-dark ">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-hourglass-split me-2"></i> En attente</h6>
<p class="card-text fs-4">{{ attentes }}</p>
</div>
</div>
</div>
</div>
{% if messages %}
<!-- Modal message -->
<div class="modal fade" id="messageModal" tabindex="-1" aria-labelledby="messageModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark
{% if messages.0.tags == 'error' %}bg-danger text-white
{% elif messages.0.tags == 'success' %}bg-success text-white
{% else %}bg-info text-white{% endif %}">
<h5 class="modal-title" id="messageModalLabel">
{% if messages.0.tags == 'error' %}Erreur
{% elif messages.0.tags == 'success' %}Succès
{% else %}Information{% endif %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
var messageModal = new bootstrap.Modal(document.getElementById('messageModal'));
messageModal.show();
});
</script>
{% endif %}
<!-- Bouton Nouvelle Demande -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-people"></i> Liste</h2>
<button type="button" class="btn btn-primary mb-3" data-bs-toggle="modal" data-bs-target="#modalDemandeConge">
<i class="bi bi-calendar-plus"></i> Nouvelle demande de congé
</button>
</div>
<!-- Modal Nouvelle Demande -->
<div class="modal fade" id="modalDemandeConge" tabindex="-1" aria-labelledby="modalDemandeCongeLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-header bg-orange-dark">
<h5 class="modal-title" id="modalDemandeCongeLabel">
<i class="bi bi-calendar-plus me-2"></i> Nouvelle demande de congé
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<fieldset class="border p-3 rounded">
<legend class="fw-bold">Informations du congé
Il vous reste {{ employe.solde_conge }} jours de congé Annuel
</legend>
<div class="row">
<div class="col-md-6 mb-3">
{{ form.type.label_tag }} {{ form.type }}
</div>
<div class="col-md-6 mb-3">
{{ form.date_debut.label_tag }} {{ form.date_debut }}
</div>
<div class="col-md-6 mb-3">
{{ form.date_fin.label_tag }} {{ form.date_fin }}
</div>
<div class="col-md-6 mb-3">
{{ form.nombre_jours.label_tag }} {{ form.nombre_jours }}
</div>
</div>
</fieldset>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success">
<i class="bi bi-send"></i> Envoyer
</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Annuler
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Tableau des demandes -->
<table class="table table-striped mt-2">
<thead>
<tr>
<th>Employe</th>
<th>Date demande</th>
<th>Type</th>
<th>Date début</th>
<th>Date fin</th>
<th>Nombre jours</th>
<th>Jours restant</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for conge in conges %}
<tr>
<td>{{ conge.employe.first_name }} {{ conge.employe.last_name }}</td>
<td>{{ conge.date_demande|date:"d/m/Y" }}</td>
<td>{{ conge.type }}</td>
<td>{{ conge.date_debut|date:"d/m/Y"}}</td>
<td>{{ conge.date_fin|date:"d/m/Y" }}</td>
<td>{{ conge.nombre_jours }}</td>
<td>{{ conge.employe.solde_conge }} </td>
<td>
{% if conge.statut == "Refusé" %}
<span class="badge bg-danger">Refusé</span>
{% elif conge.statut == "Validé directeur" %}
<span class="badge bg-success">Valide </span>
{% elif conge.statut == "Validé chef" %}
<span class="badge bg-warning">Approuvé </span>
{% elif conge.statut == "Refusé par chef" %}
<span class="badge bg-danger">Refusé </span>
{% elif conge.statut == "En attente" %}
<span class="badge bg-warning">En attente </span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-info"
data-bs-toggle="modal"
data-bs-target="#detailsCongeModal{{ conge.id }}">
<i class="bi bi-eye"></i> Voir
</button>
{% if conge.statut == "En attente" and conge.employe == request.user %}
<!-- Bouton modifier -->
<button class="btn btn-sm btn-primary"
data-bs-toggle="modal"
data-bs-target="#modifierCongeModal{{ conge.id }}">
Modifier
</button>
<!-- Bouton supprimer -->
<button class="btn btn-sm btn-danger"
data-bs-toggle="modal"
data-bs-target="#supprimerCongeModal{{ conge.id }}">
Supprimer
</button>
{% endif %}
</td>
{% for conge in conges %}
<div class="modal fade" id="modifierCongeModal{{ conge.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
{% csrf_token %}
<input type="hidden" name="conge_id" value="{{ conge.id }}">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Modifier la demande de congé</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<label>Type de congé</label>
<select name="type" class="form-select">
<option value="Conge Annuel" {% if conge.type == "Conge Annuel" %}selected{% endif %}>Conge Annuel</option>
<option value="Maladie" {% if conge.type == "Maladie" %}selected{% endif %}>Maladie</option>
<option value="Vacances" {% if conge.type == "Vacances" %}selected{% endif %}>Vacances</option>
<option value="Maternité" {% if conge.type == "Maternité" %}selected{% endif %}>Maternité</option>
</select>
</div>
<div class="mb-2">
<label>Nombre de jours</label>
<input type="number" name="nombre_jours" class="form-control" value="{{ conge.nombre_jours }}">
</div>
<div class="mb-2">
<label>Date début</label>
<input type="date" name="date_debut" class="form-control" value="{{ conge.date_debut|date:'Y-m-d' }}">
</div>
<div class="mb-2">
<label>Date fin</label>
<input type="date" name="date_fin" class="form-control" value="{{ conge.date_fin|date:'Y-m-d' }}">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
{% for conge in conges %}
<div class="modal fade" id="detailsCongeModal{{ conge.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Détails de la demande</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<ul class="list-group list-group-flush">
<li class="list-group-item"><strong>Employé :</strong> {{ conge.employe.first_name }} {{ conge.employe.last_name }}</li>
<li class="list-group-item"><strong>Type de congé :</strong> {{ conge.type }}</li>
<li class="list-group-item"><strong>Date début :</strong> {{ conge.date_debut|date:"d/m/Y" }}</li>
<li class="list-group-item"><strong>Date fin :</strong> {{ conge.date_fin|date:"d/m/Y" }}</li>
<li class="list-group-item"><strong>Nombre de jours :</strong> {{ conge.nombre_jours }}</li>
<li class="list-group-item"><strong>Solde restant :</strong> {{ conge.employe.solde_conge }}</li>
<li class="list-group-item">
<strong>Statut :</strong>
{% if conge.statut == "En attente" %}
<span class="badge bg-warning text-dark">{{ conge.statut }}</span>
{% elif conge.statut == "Validé directeur" %}
<span class="badge bg-success">Validé</span>
{% elif conge.statut == "Refusé" %}
<span class="badge bg-danger">Refusé</span>
{% else %}
<span class="badge bg-secondary">{{ conge.statut }}</span>
{% endif %}
</li>
<li class="list-group-item"><strong>Motif :</strong> {{ conge.motif_refus|default:"—" }}</li>
<li class="list-group-item"><strong>Date de la demande :</strong> {{ conge.date_demande|date:"d/m/Y H:i" }}</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
{% endfor %}
{% for conge in conges %}
<!-- Modal supprimer -->
<div class="modal fade" id="supprimerCongeModal{{ conge.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
{% csrf_token %}
<input type="hidden" name="delete_id" value="{{ conge.id }}">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Supprimer la demande</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Voulez-vous vraiment supprimer la demande de congé du
<strong>{{ conge.date_debut|date:"d/m/Y" }}</strong> au
<strong>{{ conge.date_fin|date:"d/m/Y" }}</strong> ?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-danger">Supprimer</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
</tr>
{% empty %}
<tr>
<td colspan="10" class="text-center text-muted">Aucune demande trouvée.</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Pagination -->
<nav>
<ul class="pagination">
{% for page_num in conges.paginator.page_range %}
<li class="page-item {% if conges.number == page_num %}active{% endif %}">
<a class="page-link" href="?page={{ page_num }}&search={{ search }}">{{ page_num }}</a>
</li>
{% endfor %}
</ul>
</nav>
<!-- Modal Notifications Chef -->
<div class="modal fade" id="modalNotifications" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title"><i class="bi bi-bell-fill me-2"></i> Demandes de congé en attente</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{% if notifications %}
<table class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>Employé</th>
<th>Période</th>
<th>Jours</th>
<th>Type</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for conge in notifications %}
<tr>
<td>{{ conge.employe.first_name }} {{ conge.employe.last_name }}</td>
<td>{{ conge.date_debut|date:"d/m/Y" }} - {{ conge.date_fin|date:"d/m/Y" }}</td>
<td>{{ conge.nombre_jours }}</td>
<td>{{ conge.type }}</td>
<td class="d-flex gap-1">
<!-- Valider -->
<form method="POST" action="{% url 'valider-par-chef' conge.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-success btn-sm" title="Valider">
<i class="bi bi-check-circle"></i>
</button>
</form>
<!-- Refuser -->
<button class="btn btn-danger btn-sm btn-refuser"
data-id="{{ conge.id }}"
data-nom="{{ conge.employe.first_name }} {{ conge.employe.last_name }}"
data-bs-toggle="modal"
data-bs-target="#modalRefuserGlobal">
<i class="bi bi-x-circle"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info">Aucune demande en attente.</div>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
<!-- Modal Refuser -->
<div class="modal fade" id="modalRefuserGlobal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" id="formRefuser">
{% csrf_token %}
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Refuser la demande</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="texteConge"></p>
<div class="mb-3">
<label class="form-label">Motif du refus</label>
{{ form.motif_refus }}
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger">Refuser</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const modal = new bootstrap.Modal(document.getElementById('modalRefuserGlobal'));
const texteConge = document.getElementById('texteConge');
const formRefuser = document.getElementById('formRefuser');
document.querySelectorAll('.btn-refuser').forEach(button => {
button.addEventListener('click', function() {
const congeId = this.getAttribute('data-id');
const nomEmploye = this.getAttribute('data-nom');
texteConge.innerHTML = `Refuser le congé de <strong>${nomEmploye}</strong> ?`;
formRefuser.action = `/refuser-par-chef/${congeId}/`; // URL dynamique vers Django
formRefuser.reset();
modal.show();
});
});
});
</script>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('{% static "sw.js" %}').then(function(reg) {
console.log('Service worker registered.', reg);
}).catch(function(err) {
console.warn('Service worker registration failed:', err);
});
});
}
</script>

View File

@@ -0,0 +1,31 @@
{% load static %}
{% load tags_personnaliser %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<link href="https://unpkg.com/tabulator-tables@6.4.0/dist/css/tabulator_bootstrap5.min.css" rel="stylesheet">
{% block 'css' %}{% endblock %}
<title> {% block 'titre_page'%}{% endblock%}</title>
</head>
<body>
<div class="container-fluid">
<div class="row flex-nowrap">
{% include 'parts/menu_principal.html' %}
<div class="col-9 p-4">
{% block 'contenu' %} {% endblock %}
</div>
</div>
{% block 'modal' %} {% endblock %}
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/luxon/3.7.2/luxon.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/tabulator-tables/dist/js/tabulator.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/js/bootstrap.min.js"></script>
{% block 'js' %} {% endblock%}
</body>
</html>

View File

@@ -0,0 +1,43 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<title>Login - SIRH</title>
</head>
<body>
<div class="container-fluid vh-100">
<div class="row">
<div class="col-6 vh-100 d-flex flex-column justify-content-center align-items-center">
<img src="{% static 'img/cerfig.jpg' %}" class="w-50">
<h5 class="text-center">Bienvenue sur les systèmes de gestion du CERFIG</h5>
</div>
<div class="col-6 vh-100 d-flex justify-content-center align-items-center">
<form method="POST" action="{% url 'login' %}" class="w-100 shadow rounded px-3 py-5">
<h2 class="text-center">Connexion</h2>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %}">{{message}}</div>
{% endfor %}
{% endif %}
{% csrf_token %}
<div class="mb-3">
<label for="mail">Votre adresse email :</label>
<input type="text" name="mail" class="form-control" placeholder="Entrez votre e-mail" required>
</div>
<div class="mb-3">
<label for="mot_de_passe">Mot de passe</label>
<input type="password" name="mot_de_passe" class="form-control" placeholder="Entrez votre mot de passe" required>
<i class="bi bi-eye toggle-password" onclick="togglePassword()"
style="position:absolute; right:10px; top:38px; cursor:pointer;"></i>
</div>
<button type="submit" class="btn btn-primary w-100">Se connecter</button>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,56 @@
{% load static %}
{% load tags_personnaliser %}
<div class="col-3 bg-danger d-flex flex-column vh-100 pt-5 sticky-top">
<div class="text-center mb-4">
{% if user.employe.photo %}
<img src="{{ user.employe.photo.url }}"
class="rounded-circle"
width="80"
height="80"
style="object-fit:cover;">
{% else %}
<i class="bi bi-person-circle text-white" style="font-size:60px;"></i>
{% endif %}
<div class="text-white mt-2">
{{ user.username }}
</div>
</div>
<a href="{% url 'gestion_employe:mon-profil' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-person-circle"></i> Mon profil
</a>
{% if user|has_group:"ressource_humaine" or user|has_group:"direction" %}
<a href="{% url 'gestion_employe:index' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-speedometer2"></i> Tableau de bord
</a>
{% endif %}
{% if user|has_group:"ressource_humaine" or user|has_group:"direction" %}
<a href="{% url 'gestion_projet:index' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-folder"></i> Gestion des Projets
</a>
{% endif %}
<a href="{% url 'gestion_conges:conge' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-airplane"></i> Gestion des congés
</a>
{% if user|is_chef_projet %}
<a href="{% url 'gestion_projet:activites-projet' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-list-task"></i> Suivi des Activités
</a>
{% endif %}
<a href="{% url 'gestion_salle:reservation-salle' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-calendar-check"></i> Réservation
</a>
{% comment %} <a href="{% url 'rapport-rh' %}" class="text-white fw-bold text-decoration-none mb-2" style="font-size:1.2em">
<i class="bi bi-graph-up"></i> Rapports et Statistiques
</a> {% endcomment %}
{% comment %} <a href="" class="text-white fw-bold text-decoration-none mb-2" style="font-size:1.2em">
<i class="bi bi-person-gear"></i> Gestion des Utilisateurs
</a> {% endcomment %}
{% comment %} <a href="{% url 'gestion_employe:departement' %}" class="text-white fw-bold text-decoration-none mb-2" style="font-size:1.2em">
<i class="bi bi-gear"></i> Paramètres
</a> {% endcomment %}
<a href="{% url 'deconnexion' %}" class="text-white fw-bold text-decoration-none mb-4" style="font-size:1.4em">
<i class="bi bi-box-arrow-right"></i> Déconnexion
</a>
</div>

67
SIRH/urls.py Normal file
View File

@@ -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)

37
SIRH/views.py Normal file
View File

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

16
SIRH/wsgi.py Normal file
View File

@@ -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()

View File

@@ -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
}

View File

3
gestion_conge/admin.py Normal file
View File

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

6
gestion_conge/apps.py Normal file
View File

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

17
gestion_conge/forms.py Normal file
View File

@@ -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',
}

View File

@@ -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')),
],
),
]

View File

36
gestion_conge/models.py Normal file
View File

@@ -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)

View File

@@ -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";
}
})
}

View File

@@ -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)
}
})

View File

@@ -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 %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %}">{{message}}</div>
{% endfor %}
{% endif %}
<div class="row d-flex justify-content-center mb-4">
<div class="col text-white bg-danger d-flex flex-column justify-content-center align-items-center border rounded p-4">
<div class="card-header fw-bold">
<i class="bi bi-x-circle me-2"></i> Congés refusés
</div>
<div class="card-body text-center">
<h5 class="fw-bold">{{ nombre_conges_refuse }}</h5>
</div>
</div>
<div class="col text-white bg-warning d-flex flex-column justify-content-center align-items-center border rounded p-4 mx-2">
<div class="card-header fw-bold">
<i class="bi bi-hourglass-split me-2"></i> Congés en attente
</div>
<div class="card-body text-center">
<h5 class="fw-bold">{{ nombre_conges_en_attente }}</h5>
</div>
</div>
<div class="col text-white bg-success d-flex flex-column justify-content-center align-items-center border rounded p-4">
<div class="card-header fw-bold">
<i class="bi bi-check-circle me-2"></i> Congés Validé
</div>
<div class="card-body text-center">
<h5 class="fw-bold">{{ nombre_conges_valide }}</h5>
</div>
</div>
<div class="d-flex justify-content-between my-4">
<h3><i class="bi bi-list-ul"></i> Liste des demandes de congé</h3>
<button class='btn btn-primary' id="bouton-demande-conge">Demande de congé</button>
</div>
<div class="table-responsive">
<div id="liste-demande-conges" data-url="{% url 'gestion_conges:liste-des-conges' %}"></div>
</div>
</div>
{% endblock %}
{% block 'modal' %}
{% include 'gestion_conge/parts/modalDemandeConge.html' %}
{% include 'gestion_conge/parts/modalDetailConge.html' %}
{% endblock %}
{% block 'js' %}
<script type="text/javascript" src="{% static 'gestion_conge/js/index.js' %}"></script>
<script type="text/javascript" src="{% static 'gestion_conge/js/detail_conges.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,26 @@
<div class="modal fade" id="modalDemandeConge" tabindex="-1" aria-labelledby="modalDemandeCongeLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title" id="modalDemandeCongeLabel">
<i class="bi bi-calendar-plus me-2"></i> Nouvelle demande de congé
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'gestion_conges:demande-conge' %}" enctype="multipart/form-data">
{% csrf_token %}
{{ formulaire_demande_conge.as_p }}
<div class="modal-footer">
<button type="submit" class="btn btn-success">
<i class="bi bi-send"></i> Soumettre
</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Annuler
</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,92 @@
<div class="modal fade" id="detailsCongeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Détails de la demande de congés</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form method="post" id="form-detail-conge" action="{% url 'gestion_conges:validation-des-conges' %}">
{% csrf_token %}
<input type="hidden" id="id_conge" value="">
<div class="form-group mb-2">
<label for="motif_refus">Employé :</label>
<input type="text" class="form-control" id="employe" value="" readonly>
</div>
<div class="form-group mb-2">
<label for="type_conge">Type de congé :</label>
<select class="form-select" id="type_conge" readonly>
{% comment %} <option value="maladie">Maladie</option> {% endcomment %}
<option value="Conge Annuel">Conge Annuel</option>
{% comment %} <option value="conge_maternite">Conge Maternité</option> {% endcomment %}
{% comment %} <option value="conge_mariage">Conge Mariage</option>
<option value="conge_naissance">Conge Naissance</option>
<option value="conge_deces_proche">Conge de décès d'un proche</option>
<option value="conge_mariage_proche">Conge de mariage d'un proche</option>
<option value="autre">Autre</option> {% endcomment %}
</select>
</div>
<div class="form-group mb-2">
<label for="date_debut">Date début :</label>
<input type="date" class="form-control" id="date_debut" value="" readonly>
</div>
<div class="form-group mb-2">
<label for="date_fin">Date fin :</label>
<input type="date" class="form-control" id="date_fin" value="" readonly>
</div>
<div class="form-group mb-2">
<label for="nombre_jours">Nombre de jours :</label>
<input type="number" class="form-control" id="nombre_jours" value="" readonly>
</div>
<div class="form-group mb-2">
<label for="solde_restant">Solde restant :</label>
<input type="number" class="form-control" id="solde_restant" value="" readonly>
</div>
<div class="form-group mb-2">
<label for="date_demande">Date de la demande :</label>
<input type="text" class="form-control" id="date_demande" value="" readonly>
</div>
{% if employe_est_il_chef or est_chef_projet %}
<hr>
<h5 class="text-center">Validation par le supérieur hiérarchique</h5>
<div class="d-flex align-items-center justify-content-center mb-2">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="validation_hierarchique" id="validation_hierarchique_valide" value="valide">
<label class="form-check-label" for="validation_hierarchique_valide">Valide</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="validation_hierarchique" id="validation_hierarchique_refuse" value="refuse" {% if conge.validation_hierarchique == False %}checked{% endif %}>
<label class="form-check-label" for="validation_hierarchique_refuse">Refusé</label>
</div>
</div>
{% endif %}
{% if membre_de_la_direction %}
<hr>
<h5 class="text-center">Validation par le directeur</h5>
<div class="d-flex align-items-center justify-content-center mb-2">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="validation_direction" id="validation_direction_valide" value="valide" {% if conge.validation_direction == True %}checked{% endif %}>
<label class="form-check-label" for="validation_direction_valide">Valide</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="validation_direction" id="validation_direction_refuse" value="refuse" {% if conge.validation_direction == False %}checked{% endif %}>
<label class="form-check-label" for="validation_direction_refuse">Refusé</label>
</div>
</div>
{% endif %}
<hr>
<div class="d-none form-group mt-3" id="motif_refus_container">
<label for="motif_refus">Motif de refus (si applicable) :</label>
<textarea class="form-control" id="motif_refus" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
{% if employe_est_il_chef or membre_de_la_direction or est_chef_projet %}
<button type="button" class="btn btn-success" id="bouton-enregistrer-detail-conge">Enregistrer</button>
{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,48 @@
{% for conge in conges %}
<div class="modal fade" id="modifierCongeModal{{ conge.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
{% csrf_token %}
<input type="hidden" name="conge_id" value="{{ conge.id }}">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Modifier la demande de congé</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<label>Type de congé</label>
<select name="type" class="form-select">
<option value="Conge Annuel" {% if conge.type == "Conge Annuel" %}selected{% endif %}>Conge Annuel</option>
<option value="Maladie" {% if conge.type == "Maladie" %}selected{% endif %}>Maladie</option>
<option value="Conge maternité" {% if conge.type == "Conge maternité" %}selected{% endif %}>Conge Maternité</option>
<option value="Conge de Mariage" {% if conge.type == "Conge de Mariage" %}selected{% endif %}>Conge Mariage</option>
<option value="conge de naissance" {% if conge.type == "conge de naissance" %}selected{% endif %}>Conge de Naissance</option>
<option value="conge de deces dun proche" {% if conge.type == "conge de deces dun proche" %}selected{% endif %}>Conge de décès d'un proche</option>
<option value="conge de mariage dun proche" {% if conge.type == "conge de mariage dun proche" %}selected{% endif %}>Conge de mariage d'un proche</option>
<option value="conge pour evenements familiaux" {% if conge.type == "conge pour evenements familiaux" %}selected{% endif %}>Conge pour événements familiaux</option>
<option value="Autre" {% if conge.type == "Autre" %}selected{% endif %}>Autre</option>
</select>
</div>
<div class="mb-2">
<label>Nombre de jours</label>
<input type="number" name="nombre_jours" class="form-control" value="{{ conge.nombre_jours }}">
</div>
<div class="mb-2">
<label>Date début</label>
<input type="date" name="date_debut" class="form-control" value="{{ conge.date_debut|date:'Y-m-d' }}">
</div>
<div class="mb-2">
<label>Date fin</label>
<input type="date" name="date_fin" class="form-control" value="{{ conge.date_fin|date:'Y-m-d' }}">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn bg-orange-dark">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}

View File

@@ -0,0 +1,24 @@
<div class="modal fade" id="modalRefuserGlobal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" id="formRefuser">
{% csrf_token %}
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Refuser la demande</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="texteConge"></p>
<div class="mb-3">
<label class="form-label">Motif du refus</label>
{{ form.motif_refus }}
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger">Refuser</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
</div>
</form>
</div>
</div>
</div>

3
gestion_conge/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

27
gestion_conge/urls.py Normal file
View File

@@ -0,0 +1,27 @@
from django.urls import path
from . import views
app_name = "gestion_conges"
urlpatterns = [
path(
'',
views.index,
name='conge'
),
path(
'demande_conge/',
views.demander_conge,
name='demande-conge'
),
path(
'liste-des-conges/',
views.liste_demande_conges,
name='liste-des-conges'
),
path(
'validation-des-conges/',
views.validation_de_conge,
name='validation-des-conges'
),
]

225
gestion_conge/views.py Normal file
View File

@@ -0,0 +1,225 @@
import json
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from gestion_conge.forms import CongeForm
from gestion_employe.models import Affectation, Employe
from django.forms.models import model_to_dict
from django.utils import timezone
from django.db.models import Q
from fonction_utilitaire import fonctions_utilitaire
from .models import Conge
@login_required
def index(request):
"""Vue de gestion de l'index"""
employe = Employe.objects.get(user__username = request.user)
membre_direction = 'direction' in employe.user.groups.values_list('name', flat=True)
try:
affectation = Affectation.objects.get(employe = employe, date_fin_daffectation__gte = timezone.now().date())
except Affectation.DoesNotExist:
affectation = None
try:
projet = Affectation.objects.get(employe=employe, date_fin_daffectation__gte = timezone.now().date())
except Affectation.DoesNotExist:
pass
if employe.chef:
nombre_conges_valide = Conge.objects.filter(validation_hierarchique = True, employe__departement = employe.departement).count()
nombre_conges_refuse = Conge.objects.filter(validation_hierarchique = False, employe__departement = employe.departement).count()
conges_en_attente = Conge.objects.filter(validation_hierarchique = None, employe__departement = employe.departement).order_by('-date_demande')
elif membre_direction:
nombre_conges_valide = Conge.objects.filter(validation_direction = True).count()
nombre_conges_refuse = Conge.objects.filter(validation_direction = False).count()
conges_en_attente = Conge.objects.filter(validation_hierarchique = True, validation_direction = None).order_by('-date_demande')
elif affectation and affectation.role == "chef_projet":
employes_du_projet = Affectation.objects.filter(
projet = projet.projet,
date_fin_daffectation__gte = timezone.now().date()
).values('employe')
nombre_conges_valide = Conge.objects.filter(
employe__in = employes_du_projet,
validation_hierarchique = True
).count()
nombre_conges_refuse = Conge.objects.filter(
Q(employe__in = employes_du_projet) &
(Q(validation_hierarchique = False) | Q(validation_direction = False))
).count()
conges_en_attente = Conge.objects.filter(
Q(employe__in = employes_du_projet) &
(
Q(validation_hierarchique__isnull = True) | Q(validation_direction__isnull = True)
)
).exclude(
Q(validation_hierarchique = True) | Q(validation_hierarchique = False) |
Q(validation_direction = True) | Q(validation_direction = False)
).order_by('-date_demande')
else:
nombre_conges_valide = Conge.objects.filter(
employe=employe,
validation_direction = True
).count()
nombre_conges_refuse = Conge.objects.filter(Q(employe=employe) & (
Q(validation_direction = False) | Q(validation_hierarchique = False)
)).count()
conges_en_attente = Conge.objects.filter(
Q(employe = employe) &
(
Q(validation_direction__isnull = True) | Q(validation_hierarchique__isnull = True)
)
).exclude(
Q(validation_hierarchique = True) | Q(validation_hierarchique = False)
).order_by('-date_demande')
return render(request, 'gestion_conge/index.html', {
"nombre_conges_valide": nombre_conges_valide,
"nombre_conges_refuse": nombre_conges_refuse,
"nombre_conges_en_attente": conges_en_attente.count(),
"formulaire_demande_conge": CongeForm,
"employe_est_il_chef": employe.chef,
"membre_de_la_direction": membre_direction,
"est_chef_projet": affectation.role == "chef_projet" if affectation else False,
})
@login_required
def demander_conge(request):
"""Vue de gestion des demandes de congés"""
try:
employe = Employe.objects.get(user__username = request.user)
except Employe.DoesNotExist:
messages.error(request, "Votre demande de congé a échoué car votre profil Utilisateur n'est lié à aucun profil Employé. Veuillez contacter l'administrateur.")
return redirect("gestion_conges:conge")
retour_quota = fonctions_utilitaire.solde_conge(employe)
if retour_quota["success"]:
quota_annuel = retour_quota['quota_annuel']
else:
messages.error(request, retour_quota['message'])
return redirect("gestion_conges:conge")
if request.method == "POST":
form = CongeForm(request.POST, request.FILES)
if form.is_valid():
conge_obj = form.save(commit=False)
conge_obj.employe = employe
if conge_obj.type == "conge_annuel":
if retour_quota["nombre_jours_valide"] + conge_obj.nombre_jours > quota_annuel:
messages.error(request, "Quota annuel dépassé (30 jours max).")
return redirect("gestion_conges:conge")
conge_obj.save()
messages.success(request, "Votre demande de congé a été enregistrée.")
return redirect("gestion_conges:conge")
return redirect("gestion_conges:conge")
@login_required
def liste_demande_conges(request):
"""Vue de liste des demandes de congés en attente de validation selon le statut de l'utilisateur actuel"""
try:
employe = Employe.objects.get(user__username = request.user)
except Employe.DoesNotExist:
return JsonResponse({
"success": False,
"message": "Votre profil Utilisateur n'est lié à aucun profil Employé. Veuillez contacter l'administrateur."
})
try:
affectation = Affectation.objects.get(
employe=employe,
date_fin_daffectation__gte=timezone.now().date()
)
except Affectation.DoesNotExist:
affectation = None
if employe.chef:
print("chef")
conges_en_attente = Conge.objects.filter(
employe__departement = employe.departement,
validation_hierarchique = None
).order_by('-date_demande')
elif affectation and affectation.role == "chef_projet":
employes_du_projet = Affectation.objects.filter(
projet = affectation.projet,
date_fin_daffectation__gte = timezone.now().date()
).values('employe')
conges_en_attente = Conge.objects.filter(
employe__in = employes_du_projet,
validation_hierarchique = None
).order_by('-date_demande')
elif 'direction' in employe.user.groups.values_list('name', flat=True):
conges_en_attente = Conge.objects.filter(
validation_hierarchique = True,
validation_direction = None
).order_by('-date_demande')
else:
conges_en_attente = Conge.objects.filter(
employe__user__username = request.user
).order_by('-date_demande')
return JsonResponse({
"success": True,
"data":[
{
**model_to_dict(conge),
"prenom_nom": f"{conge.employe.user.first_name} {conge.employe.user.last_name}",
"date_demande": conge.date_demande,
"nombre_jours": conge.nombre_jours,
"type": dict(conge.TYPE_CHOICES).get(conge.type),
"solde_conge": fonctions_utilitaire.solde_conge(conge.employe)["quota_annuel"]
}
for conge in conges_en_attente]},
safe=False
)
@login_required
def validation_de_conge(request):
"""
Vue de validation de conges par le superieur hierarchique.
1- Si l'employe appartient à un département, le congé est validé par le chef de département.
2- Si l'employé n'appartient pas à un département, le congé est validé par le chef de projet.
"""
request_data = json.loads(request.body)
conge_id = request_data.get("id_conge", None)
try:
conge = Conge.objects.get(id=conge_id)
except conge.DoesNotExist:
return JsonResponse({"message": "Le congé selectionné n'existe pas."})
if request.method == "POST":
validation_hierarchique = request_data.get("validation_hierarchique", None)
validation_direction = request_data.get("validation_direction", None)
motif_refus = request_data.get("motif_refus", "")
if validation_hierarchique is not None:
conge.validation_hierarchique = True if validation_hierarchique == "valide" else False
if validation_hierarchique == "refuse" and not motif_refus:
return JsonResponse({"message": "Veuillez fournir un motif de refus."})
conge.motif_refus = motif_refus if validation_hierarchique == "refuse" else ""
if validation_direction is not None:
conge.validation_direction = True if validation_direction == "valide" else False
if validation_direction == "refuse" and not motif_refus:
return JsonResponse({"message": "Veuillez fournir un motif de refus."})
conge.motif_refus = motif_refus if validation_direction == "refuse" else ""
conge.save()
return JsonResponse({"message": "La décision a été enregistrée avec succès."})

View File

14
gestion_employe/admin.py Normal file
View File

@@ -0,0 +1,14 @@
from django.contrib import admin
from .models import Departement, Employe
@admin.register(Departement)
class DepartementAdmin(admin.ModelAdmin):
list_display = ("nom", )
@admin.register(Employe)
class EmployeAdmin(admin.ModelAdmin):
list_display = (
"user",
"matricule",
"fonction",
)

84
gestion_employe/forms.py Normal file
View File

@@ -0,0 +1,84 @@
from django import forms
from .models import Contrat, Departement, Employe,Affectation,Formation
class EmployeForm(forms.ModelForm):
"""Formulaire pour modifier les informations de profil d'un employé"""
class Meta:
model = Employe
fields = (
'adresse',
'telephone',
'CV',
'diplome',
'rib',
'photo',
'casier_judiciaire'
)
class AffectationForm(forms.ModelForm):
class Meta:
model = Affectation
fields = (
'projet',
'date_affectation',
'date_fin_daffectation',
'role',
'pourcentage_temps_affectation'
)
widgets = {
'projet': forms.Select(attrs={'class': 'form-select'}),
'date_affectation': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'date_fin_daffectation': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'role': forms.Select(attrs={'class': 'form-select'}),
'pourcentage_temps_affectation': forms.NumberInput(attrs={'class': 'form-control'}),
}
class ContratForm(forms.ModelForm):
class Meta:
model = Contrat
fields = [
"numero_contrat",
"type_contrat",
"date_debut",
"date_fin",
"salaire_mensuel",
"statut",
"fichier_contrat"
]
widgets = {
'numero_contrat': forms.TextInput(attrs={'class': 'form-control'}),
'type_contrat': forms.Select(attrs={'class': 'form-select'}),
'date_debut': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'date_fin': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'salaire_mensuel': forms.TextInput(attrs={'class': 'form-control'}),
'statut': forms.Select(attrs={'class': 'form-select'}),
'fichier_contrat': forms.FileInput(attrs={'class': 'form-control'}),
}
class DepartementForm(forms.ModelForm):
class Meta:
model = Departement
fields = ['nom']
class FormationForm(forms.ModelForm):
class Meta:
model = Formation
fields = [
'titre',
'organisme',
'date_obtention',
'date_fin',
'description',
'certificat'
]
widgets = {
'titre': forms.TextInput(attrs={"class": "form-control"}),
'organisme': forms.TextInput(attrs={"class": "form-control"}),
'date_obtention': forms.DateInput(attrs={"class": "form-control", "type": "date"}),
'date_fin': forms.DateInput(attrs={"class": "form-control", "type": "date"}),
'description': forms.Textarea(attrs={"class": "form-control"}),
'certificat': forms.FileInput(attrs={"class": "form-control"}),
}

View File

@@ -0,0 +1,88 @@
# Generated by Django 5.2.13 on 2026-04-17 12:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('gestion_projet', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Departement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nom', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Employe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('matricule', models.CharField(blank=True, max_length=10, null=True, unique=True)),
('fonction', models.CharField(blank=True, choices=[('directeur', 'Directeur'), ('assistant_direction', 'Assistante de direction'), ('comptable', 'Comptable'), ('raf', 'RAF'), ('data_manager', 'Data Manager'), ('logisticien', 'Logisticien'), ('post_doctorant', 'Post-Doctorant'), ('qualiticien', 'Qualiticien'), ('technicien_surface', 'Technicien de surface'), ('chauffeur', 'Chauffeur')], max_length=50, null=True)),
('date_embauche', models.DateField(blank=True, null=True)),
('adresse', models.CharField(blank=True, max_length=100, null=True)),
('telephone', models.CharField(blank=True, max_length=15, null=True)),
('sexe', models.CharField(blank=True, choices=[('m', 'Masculin'), ('f', 'Féminin')], max_length=1, null=True)),
('date_naissance', models.DateField(blank=True, null=True)),
('CV', models.FileField(blank=True, null=True, upload_to='cv/')),
('diplome', models.FileField(blank=True, null=True, upload_to='diplomes/')),
('rib', models.FileField(blank=True, null=True, upload_to='rib/')),
('photo', models.ImageField(blank=True, null=True, upload_to='photos/')),
('casier_judiciaire', models.FileField(blank=True, null=True, upload_to='casier/')),
('chef', models.BooleanField(default=False, verbose_name='Cet utilisateur est-il chef de ce département ?')),
('departement', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='gestion_employe.departement')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Contrat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('numero_contrat', models.CharField(max_length=100, unique=True)),
('type_contrat', models.CharField(choices=[('contrat_duree_determinee', 'Contrat à Durée Déterminée'), ('contrat_duree_indeterminee', 'Contrat à Durée Indéterminée'), ('contrat_prestation', 'Contrat de Prestation de Service'), ('contrat_stage', 'Contrat de Stage')], max_length=50)),
('date_debut', models.DateField()),
('date_fin', models.DateField(blank=True, null=True)),
('salaire_mensuel', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('statut', models.CharField(choices=[('actif', 'Actif'), ('termine', 'Terminé'), ('suspendu', 'Suspendu'), ('rupture_contrat', 'Rupture de Contrat')], max_length=50)),
('fichier_contrat', models.FileField(blank=True, null=True, upload_to='contrats/')),
('employe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion_employe.employe')),
],
),
migrations.CreateModel(
name='Formation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titre', models.CharField(max_length=255, verbose_name='Nom du certificat')),
('organisme', models.CharField(max_length=255, verbose_name="Nom de l'organisme")),
('description', models.TextField(blank=True, null=True, verbose_name='Description de la formation')),
('date_obtention', models.DateField(blank=True, null=True, verbose_name="Date d'obtention")),
('date_fin', models.DateField(blank=True, null=True, verbose_name='Date de fin de validité')),
('certificat', models.FileField(blank=True, null=True, upload_to='documents/formations/', verbose_name='Certificat (PDF/Image)')),
('employe', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='employe_formation', to='gestion_employe.employe')),
],
),
migrations.CreateModel(
name='Affectation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date_affectation', models.DateField()),
('date_fin_daffectation', models.DateField(blank=True, null=True)),
('role', models.CharField(choices=[('chef_projet', 'Chef de projet'), ('doctorant', 'Doctorant'), ('mastorant', 'Mastorant'), ('consultant', 'Consultant'), ('stagiaire', 'Stagiaire'), ('laborantin', 'Laborantin'), ('medecin', 'Médecin'), ('autre', 'Autre')], default='Membre', max_length=20)),
('pourcentage_temps_affectation', models.DecimalField(decimal_places=2, max_digits=5)),
('projet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion_projet.projet')),
('employe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion_employe.employe')),
],
options={
'unique_together': {('projet', 'employe')},
},
),
]

View File

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

View File

122
gestion_employe/models.py Normal file
View File

@@ -0,0 +1,122 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from gestion_projet.models import Projet
class Departement(models.Model):
"""Modèle représentant un département de l'entreprise."""
nom = models.CharField(max_length=100)
def __str__(self):
return self.nom
class Employe(models.Model):
"""Modèle représentant un employé de l'entreprise."""
FONCTION_LISTE = [
('directeur', 'Directeur'),
('assistant_direction', 'Assistante de direction'),
('comptable', 'Comptable'),
('raf', 'RAF'),
('data_manager', 'Data Manager'),
('logisticien', 'Logisticien'),
('post_doctorant', 'Post-Doctorant'),
('qualiticien', 'Qualiticien'),
('technicien_surface', 'Technicien de surface'),
('chauffeur', 'Chauffeur'),
]
user = models.OneToOneField(User, on_delete=models.CASCADE)
matricule = models.CharField(max_length=10, unique=True, null=True, blank=True)
departement = models.ForeignKey(Departement, on_delete=models.SET_NULL, null=True, blank=True)
fonction = models.CharField(max_length=50, blank=True, null=True, choices=FONCTION_LISTE)
date_embauche = models.DateField(blank=True, null=True)
adresse = models.CharField(max_length=100, null=True, blank=True)
telephone = models.CharField(max_length=15, null=True, blank=True)
sexe = models.CharField(max_length=1, null=True, blank=True, choices=[('m', 'Masculin'), ('f', 'Féminin')])
date_naissance = models.DateField(blank=True, null=True)
CV = models.FileField(upload_to='cv/', blank=True, null=True)
diplome = models.FileField(upload_to='diplomes/', blank=True, null=True)
rib = models.FileField(upload_to='rib/', blank=True, null=True)
photo = models.ImageField(upload_to='photos/', blank=True, null=True)
casier_judiciaire = models.FileField(upload_to='casier/', blank=True, null=True)
chef = models.BooleanField(
default=False,
verbose_name="Cet utilisateur est-il chef de ce département ?"
)
def __str__(self):
return f"{self.user.first_name or 'N/A'} {self.user.last_name or ' '} ({self.matricule or ' '})"
class Contrat(models.Model):
"""Modèle représentant un contrat de travail d'un employé."""
TYPE_CONTRAT = [
('contrat_duree_determinee', 'Contrat à Durée Déterminée'),
('contrat_duree_indeterminee', 'Contrat à Durée Indéterminée'),
('contrat_prestation', 'Contrat de Prestation de Service'),
('contrat_stage', 'Contrat de Stage'),
('convention_bourse_entretien', 'Convention de bourse d\'entretien'),
]
STATUT_CONTRAT = [
('actif', 'Actif'),
('termine', 'Terminé'),
('suspendu', 'Suspendu'),
('rupture_contrat', 'Rupture de Contrat'),
]
employe = models.ForeignKey(Employe, on_delete=models.CASCADE)
numero_contrat = models.CharField(max_length=100, unique=True)
type_contrat = models.CharField(
max_length=50,
choices=TYPE_CONTRAT,
)
date_debut = models.DateField()
date_fin = models.DateField(null=True, blank=True)
salaire_mensuel = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
statut = models.CharField(max_length=50, choices=STATUT_CONTRAT)
fichier_contrat = models.FileField(upload_to='contrats/', null=True, blank=True)
@property
def nombre_jours_restant(self):
if self.date_fin:
return (self.date_fin - timezone.now().date()).days
return 100
def __str__(self):
return f"{self.numero_contrat} - {self.type_contrat}"
class Affectation(models.Model):
"""Modèle représentant l'affectation d'un employé à un projet avec un rôle spécifique."""
ROLE_CHOICES = [
('chef_projet', 'Chef de projet'),
('doctorant','Doctorant'),
('mastorant','Mastorant'),
('consultant','Consultant'),
('stagiaire','Stagiaire'),
('laborantin','Laborantin'),
('medecin','Médecin'),
('autre','Autre'),
]
employe = models.ForeignKey(Employe, on_delete=models.CASCADE)
projet = models.ForeignKey(Projet, on_delete=models.CASCADE)
date_affectation = models.DateField()
date_fin_daffectation = models.DateField(null=True, blank=True)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='Membre')
pourcentage_temps_affectation = models.DecimalField(max_digits=5, decimal_places=2)
class Meta:
unique_together = ('projet', 'employe')
class Formation(models.Model):
"""Modèle représentant une formation suivie par un employé."""
employe = models.ForeignKey(Employe, on_delete=models.CASCADE, null=True, related_name='employe_formation')
titre = models.CharField(max_length=255, verbose_name="Nom du certificat")
organisme = models.CharField(max_length=255, verbose_name="Nom de l'organisme")
description = models.TextField(blank=True, null=True, verbose_name="Description de la formation")
date_obtention = models.DateField(blank=True, null=True, verbose_name="Date d'obtention")
date_fin = models.DateField(blank=True, null=True, verbose_name="Date de fin de validité")
certificat = models.FileField(upload_to='documents/formations/', blank=True, null=True, verbose_name="Certificat (PDF/Image)")
def __str__(self):
return self.titre

View File

@@ -0,0 +1,263 @@
const $ = (element) => document.getElementById(element);
const url_liste_employe = $("tableau_liste_employe").dataset.url;
const tableau_liste_employe = new Tabulator("#tableau_liste_employe", {
columns: [
{"title": "Matricule", "field": "matricule"},
{"title": "Nom & Prénom", "field": "employe"},
{"title": "Fonction", "field": "fonction"},
{"title": "Projet", "field": "projet"},
{"title": "Téléphone", "field": "telephone"},
],
// ajaxURL: url_liste_employe,
pagination: true,
paginationSize: 10,
})
fetch(url_liste_employe)
.then(response => response.json())
.then(data => {
if (data.success){
console.log(data.data)
tableau_liste_employe.setData(data.data);
}else{
alert(data.message);
}
}
)
tableau_liste_employe.on("rowClick", function (row, rowData) {
const data = rowData.getData();
document.getElementById('detail-id').value = data.id;
document.getElementById('detail-matricule').value = data.matricule;
document.getElementById('detail-employe').value = data.employe;
document.getElementById('detail-fonction').value = data.fonction;
document.getElementById('detail-departement').value = data.departement;
document.getElementById('detail-sexe').value = "";
document.getElementById('detail-dateNaissance').value = "";
document.getElementById('detail-email').value = data.email;
document.getElementById('detail-telephone').value = data.telephone;
document.getElementById('detail-adresse').value = data.adresse;
document.getElementById('detail-dateEmbauche').value = data.date_embauche;
document.getElementById('detail-dateNaissance').value = data.date_naissance;
document.getElementById('detail-sexe').value = data.sexe;
document.getElementById('document-diplome').href = data.diplome;
document.getElementById('document-diplome').textContent = data.diplome || "Aucun diplôme";
document.getElementById('document-cv').href = data.CV;
document.getElementById('document-cv').textContent = data.CV || "Aucun CV";
document.getElementById('document-rib').href = data.rib;
document.getElementById('document-rib').textContent = data.rib || "Aucun RIB";
document.getElementById('document-casier-judiciaire').href = data.casier_judiciaire;
document.getElementById('document-casier-judiciaire').textContent = data.casier_judiciaire || "Aucun casier judiciaire";
document.getElementById("document-employe").innerHTML = data.employe || "Employé inconnu";
document.getElementById("formations-list").innerHTML = "";
for (const formation of data.formations) {
document.getElementById("formations-list").innerHTML += `
<div class="col-6">
<div class="form-group mb-2">
<label>Titre :</label>
<input type="text" class="form-control" value="${formation.titre}">
</div>
<div class="form-group mb-2">
<label>Organisme :</label>
<input type="text" class="form-control" value="${formation.organisme}">
</div>
<div class="form-group mb-2">
<label>Date d'Optention :</label>
<input type="text" class="form-control" value="${formation.date_obtention}">
</div>
<div class="form-group mb-2">
<label>Date de validitée :</label>
<input type="text" class="form-control" value="${formation.date_fin}">
</div>
</div>
`
}
document.getElementById("contrat-employe").innerHTML = data.employe || "Employé inconnu";
document.getElementById("employeIdInput").value = data.id;
document.getElementById("contrats-list").innerHTML = "";
for (const contrat of data.contrats) {
document.getElementById("contrats-list").innerHTML += `
<div class="col-6">
<div class="form-group mb-2">
<label>Numéro de contrat :</label>
<input type="text" class="form-control" value="${contrat.numero_contrat}">
</div>
<div class="form-group mb-2">
<label>Type de contrat :</label>
<select class="form-select">
<option value="">Sélectionnez un projet</option>
<option value="contrat_duree_determinee" ${contrat.type_contrat == 'contrat_duree_determinee' ? 'selected' : ''}>Contrat à Durée Déterminée</option>
<option value="contrat_duree_indeterminee" ${contrat.type_contrat == 'contrat_duree_indeterminee' ? 'selected' : ''}>Contrat à Durée Indéterminée</option>
<option value="contrat_prestation" ${contrat.type_contrat == 'contrat_prestation' ? 'selected' : ''}>Contrat de Prestation de Service</option>
<option value="contrat_stage" ${contrat.type_contrat == 'contrat_stage' ? 'selected' : ''}>Contrat de Stage</option>
</select>
</div>
<div class="form-group mb-2">
<label>Date de début :</label>
<input type="text" class="form-control" value="${contrat.date_debut}">
</div>
<div class="form-group mb-2">
<label>Date de fin :</label>
<input type="text" class="form-control" value="${contrat.date_fin}">
</div>
<div class="form-group mb-2">
<label>Salaire mensuel :</label>
<input type="text" class="form-control" value="${contrat.salaire_mensuel}">
</div>
<div class="form-group mb-2">
<label>Statut :</label>
<input type="text" class="form-control" value="${contrat.statut}">
</div>
<div class="form-group mb-2">
<label>Fichier de contrat :</label>
<a href="${contrat.fichier_contrat}" target="_blank">Voir le document</a>
</div>
<button class='d-block m-auto btn btn-danger btn-supprimer-contrat' data-contratid="${contrat.numero_contrat}">
<i class='bi bi-trash'></i> Supprimer le contrat
</button>
</div>
`
};
const supprimerButtons = document.getElementsByClassName("btn-supprimer-contrat");
Array.from(supprimerButtons).forEach(button => {
button.addEventListener("click", function() {
const contratId = this.dataset.contratid;
fetch(`contrat/supprimer/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
"id": contratId
})
})
.then(response => response.json())
.then(data => {
alert(data.message);
location.reload();
})
});
})
document.getElementById("affectation-nom-employe").textContent = data.employe || "Employé inconnu";
document.getElementById("affecter_employe_id").value = data.id;
document.getElementById("affectations-list").innerHTML = "";
for (const affectation of data.affectations) {
document.getElementById("affectations-list").innerHTML += `
<div class="col-6">
<div class="form-group mb-2">
<label>Nom du projet :</label>
<input type="text" class="form-control" value="${affectation.projet}">
</div>
<div class="form-group mb-2">
<label>Rôle :</label>
<input type="text" class="form-control" value="${affectation.role}">
</div>
<div class="form-group mb-2">
<label>Pourcentation d'affectation :</label>
<input type="text" class="form-control" value="${affectation.pourcentage_temps_affectation}">
</div>
<div class="form-group mb-2">
<label>Date d'affectation :</label>
<input type="date" class="form-control" value="${affectation.date_affectation}">
</div>
<div class="form-group mb-2">
<label>Date de fin d'affectation :</label>
<input type="date" class="form-control" value="${affectation.date_fin_daffectation}">
</div>
<button class='d-block m-auto btn btn-danger btn-supprimer-affectation' data-contratid="${affectation.id}">
<i class='bi bi-trash'></i> Supprimer l'affectation
</button>
</div>
`
}
Array.from(document.getElementsByClassName("btn-supprimer-affectation")).forEach(button => {
button.addEventListener("click", function() {
const affectationId = this.dataset.contratid;
fetch(`affectation/supprimer/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
"id": affectationId
})
})
.then(response => response.json())
.then(data => {
alert(data.message);
location.reload();
})
});
})
const modal = new bootstrap.Modal($("modalDetailEmploye"));
modal.show();
})
$("enregistrerDetail").addEventListener("click", function() {
const id_ = document.getElementById('detail-id').value;
const fonction = document.getElementById('detail-fonction').value;
const dateEmbauche = document.getElementById('detail-dateEmbauche').value;
const matricule = document.getElementById('detail-matricule').value;
const url_enregistrer_detail = $("modalDetailEmployeBody").dataset.url;
fetch(url_enregistrer_detail, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
"id": id_,
"fonction": fonction,
"date_embauche": dateEmbauche,
"matricule": matricule
})
})
.then(response => response.json())
.then(data => {
alert(data.message);
location.reload();
})
});
$("input-recherche").addEventListener("input", function() {
const recherche = this.value;
if (recherche){
tableau_liste_employe.setFilter("employe", "like", recherche);
}else{
tableau_liste_employe.clearFilter();
}
})
const listeContratExpirant = new Tabulator("#listeContratExpirant", {
columns: [
{"title": "Employé", "field": "employe"},
{"title": "Type de contrat", "field": "type_contrat"},
{"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": "Statut", "field": "statut"},
{"title": "Lien du fichier", "field": "fichier_contrat", formatter:"link", formatterParams:{
target:"_blank",
}
},
],
ajaxURL: $("boutonContratExpirants").dataset.urlexpirants,
})

View File

@@ -0,0 +1,46 @@
const $ = (element) => document.getElementById(element);
url_certificat = $("tableau-certificat").dataset.url;
const tableau_certificat = new Tabulator("#tableau-certificat", {
columns: [
{"title": "Nom du certificat", "field": "titre"},
{"title": "Nom de l'organisme", "field": "organisme"},
{"title": "Date d'obtention", "field": "date_obtention", formatter:"datetime", formatterParams:{
inputFormat:"yyyy-MM-dd",
outputFormat:"dd/MM/yy",
}},
{"title": "Date de fin de validité", "field": "date_fin", formatter:"datetime", formatterParams:{
inputFormat:"yyyy-MM-dd",
outputFormat:"dd/MM/yy",
}},
],
ajaxURL: url_certificat,
})
const enregistrerProfil = $("enregistrerProfil");
enregistrerProfil.addEventListener("click", (e) => {
const url = $("information-personnelles").dataset.url;
const csrftoken = document.querySelector("[name='csrfmiddlewaretoken']").value;
const formData = new FormData();
formData.append("nom", $("nom").value);
formData.append("prenom", $("prenom").value);
formData.append("email", $("email").value);
formData.append("telephone", $("telephone").value);
formData.append("adresse", $("adresse").value);
formData.append("sexe", $("sexe").value);
formData.append("date_naissance", $("date_naissance").value);
fetch(url, {
method: "POST",
headers: {
"X-CSRFToken": csrftoken
},
body: formData
})
.then(response => response.json())
.then(data => alert(data.message))
.catch(error => console.error("Erreur:", error));
});

View File

@@ -0,0 +1,67 @@
{% extends "BASE.html" %}
{% load static %}
{% block 'titre_page' %} Gestion des projets {% endblock %}
{% block 'css' %}
{% endblock %}
{% block 'contenu' %}
<div class="col px-2">
<h3>Gestion des employés</h3>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<div class="row">
<div class="col text-white bg-success d-flex flex-column justify-content-center align-items-center border rounded p-4">
<span class="fs-5">Nombre d'employés</span>
<h3>{{ nombre_employes }}</h3>
</div>
<div class="col text-white bg-info d-flex flex-column justify-content-center align-items-center border rounded p-4 mx-3">
<span class="fs-5">Nombre de prestataires</span>
<h3>{{ nombre_cps }}</h3>
</div>
<div class="col text-white bg-warning d-flex flex-column justify-content-center align-items-center border rounded p-4">
<span class="fs-5">Nombre de stagiaires</span>
<h3>{{ nombre_stage }}</h3>
</div>
</div>
<div class="row mt-4">
<div class="col d-flex justify-content-between mb-3">
<h3>
<i class="bi bi-people"></i>
La liste des employés
</h3>
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modalListeDesContratExpirants" id="boutonContratExpirants" data-urlexpirants="{% url 'gestion_employe:liste-contrat-expirants' %}">
Contrats proches de la fin <span class="badge badge-light">{{ nombre_expirants }}</span>
</button>
</div>
<div class="row mb-2 d-flex justify-content-end">
<div class="col">
<div class="row">
<div class="col d-flex justify-content-center">
<input class="form-control d-block" type="search" placeholder="Recherche par nom et prénom..." id="input-recherche">
</div>
</div>
</div>
</div>
<div class="table-responsive">
<div id="tableau_liste_employe" data-url={% url 'gestion_employe:liste-employes' %}></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block 'modal' %}
{% include "gestion_employe/parts/modalDetailEmploye.html" %}
{% include "gestion_employe/parts/modalCreationContrat.html" %}
{% include "gestion_employe/parts/modalAffectationProjet.html" %}
{% include "gestion_employe/parts/modalDocument.html" %}
{% include "gestion_employe/parts/modalListeContratExpirants.html" %}
{% endblock %}
{% block 'js' %}
<script type="text/javascript" src="{% static 'gestion_employe/js/index.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,105 @@
{% extends "BASE.html" %}
{% load static %}
{% block 'titre_page' %} Gestion des employés - Mon profil {% endblock %}
{% block 'contenu' %}
<div class="card shadow-sm border-0 w-100" style="max-width: 850px;">
<div class="card-header bg-orange-dark">
<h5 class="mb-0 fw-bold">
<i class="bi bi-pencil-square me-2"></i> Modifier mon profil
</h5>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div class="accordion" id="accordionProfil">
<div class="accordion-item">
<h2 class="accordion-header" id="headingInfo">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseInfo" aria-expanded="false" aria-controls="collapseInfo">
<i class="bi bi-person-lines-fill me-2"></i> Informations personnelles
</button>
</h2>
<div id="collapseInfo" class="accordion-collapse collapse" aria-labelledby="headingInfo" data-bs-parent="#accordionProfil">
<div class="accordion-body">
<div class="row">
<div class="col">
<div class="form-group mb-2">
<label>Photo de profil :</label>
<input type="file" class="form-control">
</div>
<div class="form-group mb-2">
<label>Prénom :</label>
<input type="text" class="form-control">
</div>
<div class="form-group mb-2">
<label>Nom :</label>
<input type="text" class="form-control">
</div>
</div>
<div class="col">
<div class="form-group mb-2">
<label>Email :</label>
<input type="email" class="form-control">
</div>
<div class="form-group mb-2">
<label>Téléphone :</label>
<input type="text" class="form-control">
</div>
<div class="form-group mb-2">
<label>Adresse :</label>
<input type="text" class="form-control">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingDocs">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDocs" aria-expanded="false" aria-controls="collapseDocs">
<i class="bi bi-file-earmark-text me-2"></i> Documents
</button>
</h2>
<div id="collapseDocs" class="accordion-collapse collapse" aria-labelledby="headingDocs" data-bs-parent="#accordionProfil">
<div class="accordion-body">
<div class="row">
<div class="col">
<div class="form-group mb-2">
<label>CV :</label>
<input type="file" class="form-control">
</div>
<div class="form-group mb-2">
<label>Diplome :</label>
<input type="file" class="form-control">
</div>
</div>
<div class="col">
<div class="form-group mb-2">
<label>RIB :</label>
<input type="file" class="form-control">
</div>
<div class="form-group mb-2">
<label>Casier judiciaire :</label>
<input type="file" class="form-control">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-4 gap-2 flex-wrap">
<a href="{% url 'gestion_employe:mon-profil' %}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i> Annuler
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i> Enregistrer
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block 'modal' %}
{% endblock %}
{% block 'js' %}
{% endblock %}

View File

@@ -0,0 +1,241 @@
{% extends "BASE.html" %}
{% load static %}
{% block 'titre_page' %} Gestion des employés - Mon profil {% endblock %}
{% block 'contenu' %}
<div class="d-flex justify-content-between">
<h5 class="fw-bold">
<i class="bi bi-pencil-square me-2"></i> Les informations de mon profil
</h5>
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#modalModifierProfil">
<i class="bi bi-pencil-square me-1"></i> Modifier le mot de passe
</button>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}success{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% if not expiration_contrat %}
<div class="alert alert-danger fade show alert-dismissible mt-2">
<strong>Important :</strong> Les informations sur votre contrat n'ont pas été renseignées, veuillez contacter les ressources humaines.
</div>
{% elif contrat_nb_jours_restant %}
<div class="alert alert-danger fade show alert-dismissible mt-2">
<strong>Important :</strong> Votre contrat de travail expire dans {{ contrat_nb_jours_restant }} jours, veuillez contacter les ressources humaines.
</div>
{% endif %}
<div class="accordion mt-2" id="accordionInformationEmploye">
<div class="accordion-item">
<h2 class="accordion-header" id="headingContrats">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseProfil" aria-expanded="false" aria-controls="collapseProfil">
<i class="bi bi-file-earmark-text me-2"></i> Mon identité
</button>
</h2>
<div id="collapseProfil" class="accordion-collapse" aria-labelledby="headingContrats" data-bs-parent="#accordionInformationEmploye">
<div class="accordion-body">
<div class="row" id="information-personnelles" data-url="{% url 'gestion_employe:modifier-employe' %}">
{% csrf_token %}
<div class="col">
<div class="form-group mb-2">
<label>photo</label>
{% if employe.photo %}
<span>Fichier actuel : <a href="{{ employe.photo.url }}">{{employe.photo}}</a></span>
{% endif %}
<input type="file" id="photo" name="photo" class="form-control">
</div>
<div class="form-group mb-2">
<label>Matricule :</label>
<input type="text" class="form-control" id="matricule" value="{{ employe.matricule|default:'' }}" readonly>
</div>
<div class="form-group mb-2">
<label>Nom :</label>
<input type="text" class="form-control" id="nom" value="{{ employe.user.last_name }}">
</div>
<div class="form-group mb-2">
<label>Prénom :</label>
<input type="text" class="form-control" id="prenom" value="{{ employe.user.first_name }}">
</div>
<div class="form-group mb-2">
<label>Sexe :</label>
<select id='sexe' class="form-select">
<option value='h' {% if employe.sexe == 'h' %}selected{% endif %}>Homme</option>
<option value='f' {% if employe.sexe == 'f' %}selected{% endif %}>Femme</option>
</select>
</div>
</div>
<div class="col">
<div class="form-group mb-2">
<label>Date de naissance :</label>
<input type="date" class="form-control" id="date_naissance" value="{{ employe.date_naissance|date:'Y-m-d' }}">
</div>
<div class="form-group mb-2">
<label>Département :</label>
<input type="text" class="form-control" id="departement" value="{{ employe.departement.nom|default:'' }}" readonly>
</div>
<div class="form-group mb-2">
<label>Email :</label>
<input type="email" class="form-control" id="email" value="{{ employe.user.email|default:'' }}">
</div>
<div class="form-group mb-2">
<label>Téléphone :</label>
<input type="text" class="form-control" id="telephone" value="{{ employe.telephone|default:'' }}">
</div>
<div class="form-group mb-2">
<label>Adresse :</label>
<input type="text" class="form-control" id="adresse" value="{{ employe.adresse|default:'' }}">
</div>
</div>
</div>
<button type="submit" class="btn btn-success d-block m-auto" id="enregistrerProfil">Enregistrer</button>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingContrats">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseContrats" aria-expanded="false" aria-controls="collapseContrats">
<i class="bi bi-file-earmark-text me-2"></i> Contrats
</button>
</h2>
<div id="collapseContrats" class="accordion-collapse collapse" aria-labelledby="headingContrats" data-bs-parent="#accordionInformationEmploye">
<div class="accordion-body">
{% for contrat in contrats %}
<div class="mb-3 p-3 border rounded">
<div class="form-group mb-2">
<label>Numéro :</label>
<input type="text" class="form-control" value="{{ contrat.numero_contrat }}" id="numero_contrat" readonly>
</div><div class="form-group mb-2">
<label>Type de Contrat :</label>
<input type="text" class="form-control" value="{{ contrat.type_contrat }}" id="type_contrat" readonly>
</div><div class="form-group mb-2">
<label>Date début :</label>
<input type="date" class="form-control" value="{{ contrat.date_debut|date:'Y-m-d' }}" id="date_debut" readonly>
</div>
<div class="form-group mb-2">
<label>Date fin :</label>
<input type="date" class="form-control" value="{{ contrat.date_fin|date:'Y-m-d' }}" id="date_fin" readonly>
</div>
<div class="form-group mb-2">
<label>Salaire :</label>
<input type="number" class="form-control" value="{{ contrat.salaire_mensuel }}" id="salaire" readonly>
</div>
<div class="form-group mb-2">
<label>Statut :</label>
<input type="text" class="form-control" value="{{ contrat.statut }}" id="satut" readonly>
</div>
<div class="form-group mb-2">
<label>fichier :</label>
<a href="{{ contrat.fichier_contrat }}" target="_blank" class="btn btn-outline-primary">Voir le contrat</a>
</div>
</div>
{% empty %}
<p class="text-danger">Aucun contrat trouvé.</p>
{% endfor %}
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingProjets">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseProjets" aria-expanded="false" aria-controls="collapseProjets">
<i class="bi bi-briefcase me-2"></i> Projets
</button>
</h2>
<div id="collapseProjets" class="accordion-collapse collapse" aria-labelledby="headingProjets" data-bs-parent="#accordionInformationEmploye">
<div class="accordion-body">
<ul class="list-group">
{% for projet in projets %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ projet.nom_projet }} ({{ projet.pourcentage_temps_affectation }}%) du <strong>{{ projet.date_debut|date:"d/m/Y" }}</strong> au
<strong>{{ projet.date_fin|date:"d/m/Y" }}</strong> en tant que {{ projet.role }}.
</li>
{% empty %}
<li class="list-group-item text-muted text-center">Vous n'êtes affecté à aucun projet.</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingDocs">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDocs" aria-expanded="false" aria-controls="collapseDocs">
<i class="bi bi-file-earmark-text me-2"></i> Documents
</button>
</h2>
<div id="collapseDocs" class="accordion-collapse collapse" aria-labelledby="headingDocs" data-bs-parent="#accordionInformationEmploye">
<div class="accordion-body">
<form method="POST" action="{% url 'gestion_employe:enregistrement-document-rh' %}" enctype="multipart/form-data">
{% csrf_token %}
<div class="row">
<div class="col">
<div class="form-group mb-2">
<label>photo</label>
{% if employe.photo %}
<span>Fichier actuel : <a href="{{ employe.photo.url }}">{{employe.photo}}</a></span>
{% endif %}
<input type="file" class="form-control" name="photo">
</div>
<div class="form-group mb-2">
<label>CV</label>
{% if employe.CV %}
<span>Fichier actuel : <a href="{{ employe.CV.url }}">{{employe.CV}}</a></span>
{% endif %}
<input type="file" class="form-control" name="cv">
</div>
<div class="form-group mb-2">
<label>Diplôme</label>
{% if employe.diplome %}
<span>Fichier actuel : <a href="{{ employe.diplome.url }}">{{employe.diplome}}</a></span>
{% endif %}
<input type="file" class="form-control" name="diplome">
</div>
</div>
<div class="col">
<div class="form-group mb-2">
<label>Casier judiciaire</label>
{% if employe.casier_judiciaire %}
<span>Fichier actuel : <a href="{{ employe.casier_judiciaire.url }}">{{employe.casier_judiciaire}}</a></span>
{% endif %}
<input type="file" class="form-control" name="casier_judiciaire">
</div>
<div class="form-group mb-2">
<label>RIB</label>
{% if employe.rib %}
<span>Fichier actuel : <a href="{{ employe.rib.url }}">{{employe.rib}}</a></span>
{% endif %}
<input type="file" class="form-control" name="rib">
</div>
</div>
</div>
<button type="submit" class="btn btn-success d-block m-auto">Enregistrer</button>
</form>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingCertificat">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseCertificat" aria-expanded="false" aria-controls="collapseCertificat">
<i class="bi bi-file-earmark-text me-2"></i> Certificats
</button>
</h2>
<div id="collapseCertificat" class="accordion-collapse collapse" aria-labelledby="headingCertificat" data-bs-parent="#accordionInformationEmploye">
<div class="accordion-body">
<div id="tableau-certificat" data-url="{% url 'gestion_employe:liste-formation' %}"></div>
<button class="btn btn-success d-block m-auto" data-bs-toggle="modal" data-bs-target="#modalAjouterFormation">Ajouter un certificat</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block 'modal' %}
{% include "gestion_employe/parts/modificationMotPasse.html" %}
{% include "gestion_employe/parts/modalAjoutFormation.html" %}
{% endblock %}
{% block 'js' %}
<script src="{% static 'gestion_employe/js/mon_profil.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,28 @@
<div class="modal fade" id="modalAffectation" tabindex="-1" aria-labelledby="modalAffectationLabel" aria-hidden="true">
<div class="modal-dialog ">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title" id="modalAffectationLabel" >
Affectation de projet (<span id="affectation-nom-employe"></span>)
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal">
</button>
</div>
<div class="modal-body">
<form action="{% url 'gestion_employe:affecter_employe_projet' %}" method="POST">
{% csrf_token %}
<input type="hidden" name="affecter_employe_id" id="affecter_employe_id">
{{ affectation_form.as_p }}
<div class="modal-footer bg-light">
<button type="submit" class="btn btn-success">
<i class="bi bi-check-circle"></i> affecter
</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-1"></i> Annuler
</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<div class="modal fade" id="modalAjouterFormation" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Ajouter un certificat</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'gestion_employe:ajouter_formation' %}" enctype="multipart/form-data">
{% csrf_token %}
{{ formation_form.as_p }}
<div class="modal-footer">
<button type="submit" class="btn btn-success">Enregistrer</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,57 @@
<div class="modal fade" id="modalContrat" tabindex="-1" aria-labelledby="modalContratLabel" aria-hidden="true">
<div class="modal-dialog ">
<div class="modal-content bg-white">
<div class="modal-header ">
<h5 class="modal-title" id="modalContratLabel">
<i class="bi bi-file-earmark-text me-2"></i>
Contrat de <span id="contrat-employe"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form action="{% url 'gestion_employe:creation-contrat' %}" method="POST" id="contrat_form" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="employe_id" value="" id="employeIdInput">
{{ contrat_form.as_p }}
<div class="modal-footer bg-light">
<button type="submit" class="btn btn-success">
<i class="bi bi-check-circle me-1"></i> Enregistrer
</button>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-1"></i> Annuler
</button>
</div>
</form>
{% comment %} <fieldset class="border p-3 mb-4">
<legend class="fw-bold mb-3 text-muted">Informations du contrat</legend>
<div class="form-group mb-2">
<label>Numero de contrat :</label>
<input type="text" class="form-control">
</div>
<div class="form-group mb-2">
<label>Type contrat :</label>
<input type="text" class="form-control">
</div>
<div class="form-group mb-2">
<label>date de début :</label>
<input type="date" class="form-control">
</div>
<div class="form-group mb-2">
<label>date de fin :</label>
<input type="date" class="form-control">
</div>
<div class="form-group mb-2">
<label>Salaire mensuel :</label>
<input type="text" class="form-control">
</div>
<div class="form-group mb-2">
<label>Fichier du contrat :</label>
<input type="file" class="form-control">
</div>
</div>
</fieldset> {% endcomment %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,136 @@
{% load tags_personnaliser %}
<div class="modal fade" id="modalDetailEmploye" tabindex="-1" aria-labelledby="modalDetailEmployeLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header ">
<h5 class="modal-title bi-pencil-square" id="modalDetailEmployeLabel">
Détails de l'employé <span id="detail-employe"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="modalDetailEmployeBody" data-url="{% url 'gestion_employe:enregistrer-detail-employe' %}">
<div class="row g-3">
<input type="hidden" id="detail-id">
<div class="col-md-6">
{% csrf_token %}
<div class="form-group mb-2">
<label>Matricule :</label>
<input type="text" class="form-control" id="detail-matricule">
</div>
<div class="form-group mb-2">
<label>Date d'embauche :</label>
<input type="date" class="form-control" id="detail-dateEmbauche">
</div>
<div class="form-group mb-2">
<label>Département :</label>
<input type="text" class="form-control" id="detail-departement">
</div>
<div class="form-group mb-2">
<label>Sexe :</label>
<select id="detail-sexe" class="form-select">
<option value="">-- Sélectionner --</option>
<option value="h">Masculin</option>
<option value="f">Féminin</option>
</select>
</div>
<div class="form-group mb-2">
<label>Date de naissance :</label>
<input type="date" class="form-control" id="detail-dateNaissance">
</div>
</div>
<div class="col-md-6">
<div class="form-group mb-2">
<label>Fonction :</label>
<select class="form-select" id='detail-fonction'>
<option value='chauffeur'>Chauffeur</option>
<option value='technicien_surface'>Technicien de surface</option>
<option value='qualiticien'>Qualiticien</option>
<option value='post_doctorant'>Post-Doctorant</option>
<option value='logisticien'>Logisticien</option>
<option value='data_manager'>Data Manager</option>
<option value='raf'>RAF</option>
<option value='comptable'>Comptable</option>
<option value='assistant_direction'>Assistante de direction</option>
<option value='directeur'>Directeur</option>
</select>
</div>
<div class="form-group mb-2">
<label>Email :</label>
<input type="email" class="form-control" id="detail-email">
</div>
<div class="form-group mb-2">
<label>Téléphone :</label>
<input type="text" class="form-control" id="detail-telephone">
</div>
<div class="form-group mb-2">
<label>Adresse :</label>
<input type="text" class="form-control" id="detail-adresse">
</div>
</div>
</div>
<div class="row">
{% if user|has_group:"ressource_humaine" %}
<div class="col">
<button type="button" class="btn btn-success d-block m-auto" id="enregistrerDetail">
<i class="bi bi-save"></i> Enregistrer
</button>
</div>
{% endif %}
</div>
<div class="row">
<div class="col text-end">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalDocuments">
<i class="bi bi-folder2-open"></i> Voir les documents RH
</button>
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<div class="d-flex justify-content-between">
<h5>
<i class="bi bi-file-earmark-text me-2"></i> Liste des Contrats
</h5>
{% if user|has_group:"ressource_humaine" %}
<div>
<button class="btn btn-secondary" disabled>
<i class="bi bi-file-earmark-lock"></i> Contrat actif
</button>
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#modalContrat{{ item.employe.id }}">
<i class="bi bi-file-earmark-text"></i> Créer contrat
</button>
</div>
{% endif %}
</div>
{% csrf_token %}
<div class="row" id="contrats-list"></div>
</div>
</div>
<hr class="my-4">
<div class="row d-flex g-3">
<div class="d-flex justify-content-between">
<h5><i class="bi bi-pin-angle me-2"></i> Affectations</h5>
{% if user|has_group:"ressource_humaine" %}
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#modalAffectation" onclick="setEmployeId()">
<i class="bi bi-file-earmark-text"></i>Affecter
</button>
{% endif %}
</div>
<div class="row">
<div id="affectations-list"></div>
</div>
</div>
<hr class="my-4">
<div class="row g-3">
<h5 class="mb-3"><i class="bi bi-award-fill me-2"></i> Formations</h5>
<div class="row" id="formations-list"></div>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Fermer
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,41 @@
<div class="modal fade" id="modalDocuments" tabindex="-1" aria-labelledby="modalDocumentsLabel" aria-hidden="true">
<div class="modal-dialog ">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header ">
<h5 class="modal-title" id="modalDocumentsLabel">
<i class="bi bi-folder2-open me-2"></i> Documents RH de <span id="document-employe"></span>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col">
<ul class="list-group">
<li class="list-group-item">
<strong>Diplôme :</strong>
<a id="document-diplome" target="_blank"></a>
</li>
<li class="list-group-item">
<strong>CV :</strong>
<a id="document-cv" target="_blank"></a>
</li>
<li class="list-group-item">
<strong>RIB :</strong>
<a id="document-rib" target="_blank"></a>
</li>
<li class="list-group-item">
<strong>Casier judiciaire :</strong>
<a id="document-casier-judiciaire" target="_blank"></a>
</li>
</ul>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Fermer
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
<div class="modal fade" id="modalListeDesContratExpirants" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Contrats expirants dans un maximum de 60 jours</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id='listeContratExpirant'></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<div class="modal fade" id="modalModifierProfil" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-person-lines-fill me-2"></i> Modifier le mot de passe</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form method="POST" action="{% url 'gestion_employe:modifier-mot-passe' %}">
{% csrf_token %}
<div class="form-group mb-2">
<label>Votre mot de passe actuel :</label>
<input class="form-control" type="password" name="ancien-mdp">
</div>
<div class="form-group mb-2">
<label>Votre nouveau mot de passe :</label>
<input class="form-control" type="password" name="nouveau-mdp">
</div>
<div class="form-group mb-2">
<label>Confirmez votre nouveau mot de passe :</label>
<input class="form-control" type="password" name="confirmation-mdp">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Fermer
</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-check-circle me-1"></i> Enregistrer
</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -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"

3
gestion_employe/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

102
gestion_employe/urls.py Normal file
View File

@@ -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/<int:pk>/",
views.modifier_formation,
name="modifier_formation"
),
path(
"mes-formations/supprimer/<int:pk>/",
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'
)
]

447
gestion_employe/views.py Normal file
View File

@@ -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')

View File

6
gestion_projet/admin.py Normal file
View File

@@ -0,0 +1,6 @@
from django.contrib import admin
from .models import DomaineDeRecherche
@admin.register(DomaineDeRecherche)
class DomaineDeRecherche(admin.ModelAdmin):
list_display = ('nom',)

176
gestion_projet/forms.py Normal file
View File

@@ -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 lactivité'
}),
'description':forms.Textarea(attrs={
'class':'form-control',
'rows':3,
'placeholder':'Description de lactivité'
}),
'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',
}),
}

View File

@@ -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')},
},
),
]

View File

309
gestion_projet/models.py Normal file
View File

@@ -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

View File

@@ -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 !");
}
});
});

View File

@@ -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.');
}
});
});

View File

@@ -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);
}
});
});

View File

@@ -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"));
// })

View File

@@ -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,
});

View File

@@ -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();
})

View File

@@ -0,0 +1,37 @@
{% extends "BASE.html" %}
{% load static %}
{% block 'titre_page' %} Gestion des projets {% endblock %}
{% block 'contenu' %}
<button class="btn btn-primary mb-3">
<i class="bi bi-caret-left-fill"></i> Retour
</button>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == "error" %}danger{% else %}success{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<div class="row d-flex justify-content-center">
<div class="col-8 card bordered rounded py-4 px-2 ">
<h5>Ajout du financement au projet (Nom du projet ici)</h5>
<hr>
<form method="POST" action="{% url 'gestion_projet:creation-projet' %}">
{% csrf_token %}
{{ formulaire_creation_projet.as_p }}
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-danger">Annuler</button>
<a href="{% url 'gestion_projet:ajouter_financement' %}" class="btn btn-primary ms-2">Enregistrer et ajouter un financement</a>
</div>
</form>
</div>
</div>
{% endblock %}
{% block 'modal' %}
{% include "gestion_projet/parts/modalAjoutProjet.html" %}
{% endblock %}
{% block 'js' %}
<script type="text/javascript" src="{% static 'gestion_projet/js/index.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends "BASE.html" %}
{% load static %}
{% block 'titre_page' %} Gestion des projets {% endblock %}
{% block 'contenu' %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% comment %} <h3>Enregistrement d'un nouveau projet</h3> {% endcomment %}
<div class="row d-flex justify-content-center">
<div class="col-8 card bordered rounded py-4 px-2 ">
<h5>Enregistrement d'un nouveau projet</h5>
<hr>
<form method="POST" action="{% url 'gestion_projet:creation-projet' %}">
{% csrf_token %}
{{ formulaire_creation_projet.as_p }}
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-danger">Annuler</button>
<button class="btn btn-primary ms-2">Enregistrer</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block 'modal' %}
{% endblock %}
{% block 'js' %}
{% endblock %}

View File

@@ -0,0 +1,61 @@
{% extends "BASE.html" %}
{% load static %}
{% load tags_personnaliser %}
{% block 'titre_page' %} Gestion des projets {% endblock %}
{% block 'contenu' %}
<h3>Gestion des projets</h3>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == "error" %}danger{% else %}success{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<div class="row">
<div class="col text-white bg-success d-flex flex-column justify-content-center align-items-center border rounded p-4">
<span class="fs-5">Projets en cours</span>
<h3>{{projet_en_cours}}</h3>
</div>
<div class="col text-white bg-success d-flex flex-column justify-content-center align-items-center border rounded p-4 mx-2">
<span class="fs-5">Budget Total (GNF)</span>
<h3>{{budget_total}}</h3>
</div>
<div class="col text-white bg-success d-flex flex-column justify-content-center align-items-center border rounded p-4">
<span class="fs-5">Personnel sous projet</span>
<h3>{{nombre_personnel}}</h3>
</div>
</div>
<div class="row mt-4">
<div class="col d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-4" >La liste des projets </h5>
{% if user|has_group:"ressource_humaine" %}
<div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalProjet">
<i class="bi bi-plus-circle"></i> Ajouter un projet
</button>
<button class="btn btn-info" data-bs-toggle="modal" data-bs-target="#modalBailleur">
<i class="bi bi-person"></i> Ajouter un bailleur
</button>
</div>
{% endif %}
</div>
<div class="row">
<div class="col">
<div id="tableau-liste-projet" data-url="{% url 'gestion_projet:liste-projet' %}"></div>
</div>
</div>
</div>
{% 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' %}
<script type="text/javascript" src="{% static 'gestion_projet/js/index.js' %}"></script>
<script type="text/javascript" src="{% static 'gestion_projet/js/creation_projet.js' %}"></script>
<script type="text/javascript" src="{% static 'gestion_projet/js/enregistrement_bailleur.js' %}"></script>
<script type="text/javascript" src="{% static 'gestion_projet/js/enregistrement_financement.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,20 @@
<div class="modal fade" id="modalBailleur" tabindex="-1" aria-labelledby="modalBailleurLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalBailleurLabel">Ajouter un Bailleur</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<form id="formBailleur" method="POST" action="{% url 'gestion_projet:creation-bailleur' %}">
{% csrf_token %}
{{ form_ajout_bailleur.as_p }}
</form>
</div>
<div class="modal-footer">
<button type="submit" id="btnEnregistrerBailleur" class="btn btn-success"><i class="bi bi-save me-1"></i> Enregistrer</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="modal fade" id="modalListeDocument" tabindex="-1" aria-labelledby="modalListeDocumentLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="modalListeDocumentLabel">Liste des Documents</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div id="listeDocuments" data-urllistedocument="{% url 'gestion_projet:liste-documents-projet' %}"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class="modal fade" id="modalAjoutActivite" tabindex="-1" aria-labelledby="modalAjoutActiviteLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalAjoutActiviteLabel">Ajouter une Activité</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" action="{% url 'gestion_projet:ajouter-activite' %}">
{% csrf_token %}
<div class="modal-body">
{{ form_ajout_activite.as_p }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Ajouter</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class="modal fade" id="modalAjoutDocument" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ajouter un Document</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{% url 'gestion_projet:ajouter-document' %}" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
{{ form_ajout_document.as_p }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Ajouter</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class="modal fade" id="modalAjouterLivrable" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ajouter un livrable - (Nom du livrable)</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'gestion_projet:ajouter-livrable' %}" enctype="multipart/form-data">
{% csrf_token %}
{{ form_ajout_livrable.as_p }}
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-success">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<!-- Modale Projet -->
<div class="modal fade" id="modalProjet" tabindex="-1" aria-labelledby="modalProjetLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title" id="modalProjetLabel">Ajouter un projet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body p-3">
<form method="POST" id="formCreationProjet" action="{% url 'gestion_projet:creation-projet' %}">
{% csrf_token %}
{{ formulaire_creation_projet.as_p }}
</form>
</div>
<div class="modal-footer">
<button type="submit" id="btnEnregistrerProjet" class="btn btn-success">Enregistrer</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,26 @@
<div class="modal fade" id="modalAnnulerActivite" tabindex="-1" aria-labelledby="modalAnnulerActiviteLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="modalAnnulerActiviteLabel">Annuler l'activité</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<form method="POST" action="{% url 'gestion_projet:annuler-activite' %}">
{% csrf_token %}
<input type="hidden" name="id_activite" id="id_activite_annulation">
<div class="mb-3">
<label for="motif_annulation" class="form-label">Motif d'annulation</label>
<textarea class="form-control" name="motif_annulation" id="motif_annulation" rows="4" required></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-danger">
<i class="bi bi-x-circle"></i> Confirmer l'annulation
</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,58 @@
<div class="modal fade" id="modalDetailActivite" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-orange-dark">
<h5 class="modal-title">Détails de l'activité</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="">
<input type="hidden" id="idDetailActivite">
<div class="form-group mb-2">
<label>Titre :</label>
<input type="text" class="form-control" id="titreDetailActivite">
</div>
<div class="form-group mb-2">
<label>Description :</label>
<textarea class="form-control" id="descriptionDetailActivite"></textarea>
</div>
<div class="form-group mb-2">
<label>Date de début :</label>
<input type="date" class="form-control" id="date_debutDetailActivite">
</div>
<div class="form-group mb-2">
<label>Date de fin :</label>
<input type="date" class="form-control" id="date_finDetailActivite">
</div>
<div class="form-group mb-2">
<label>Statut:</label>
<input type="text" class="form-control" id="statutDetailActivite">
</div>
<div class="form-group mb-2">
<label>Budget prévu :</label>
<input type="number" class="form-control" id="budget_prevuDetailActivite">
</div>
<div class="form-group mb-2">
<label>Besoin de ressources matérielles:</label>
<textarea class="form-control" id="besoin_ressources_materiellesDetailActivite"></textarea>
</div>
<p><strong>Liste des livrables :</strong></p>
<div id="listeLivrables"></div>
<button class="btn btn-danger d-block mx-auto" id="btnAnnulerActivite">
Annuler cette activité
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-success" id="btnMiseAJourDepense">
<i class="bi bi-plus-circle"></i> Mise à jour du budget
</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAjouterLivrable">
<i class="bi bi-plus-circle"></i> Ajouter un livrable
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,115 @@
{% load tags_personnaliser %}
<div class="modal fade" id="modalDetailProjet{{ activite.id }}" tabindex="-1">
<div class="modal-dialog ">
<div class="modal-content">
<div class="modal-header ">
<h5 class="modal-title">Détails du projet </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col" id="detail-projet-container">
<form method='post' action="{% url 'gestion_projet:mises-a-jour-projet' %}" id="detail-projet-form">
{% csrf_token %}
<div class="form-group mb-2">
<label>Id projet:</label>
<input type="text" class="form-control" id="detail_id_projet" name='id_projet' readonly>
</div>
<div class="form-group mb-2">
<label> Nom projet :</label>
<input type="text" class="form-control" id="detail_nom_projet" name='nom_projet'>
</div>
<div class="form-group mb-2">
<label>Numero de convention:</label>
<input type="text" class="form-control" id="detail_numero_convention" name="numero_convention">
</div>
<div class="form-group mb-2">
<label>Date de début :</label>
<input type="date" class="form-control" id="detail_date_debut" name='date_debut'>
</div>
<div class="form-group mb-2">
<label>Date de fin :</label>
<input type="date" class="form-control" id="detail_date_fin" name='date_fin'>
</div>
<div class="form-group mb-2">
<label> Type de projet </label>
<select class="form-select" id="detail_type_projet" name='type_projet'>
<option value="laboratoire">Laboratoire</option>
<option value="épidémiologie">Épidémiologie</option>
<option value="sciences sociales">Sciences sociales</option>
<option value="cliniques">Cliniques</option>
<option value="autre">Autre</option>
</select>
</div>
<div class="form-group mb-2">
<label> Domaine de recherche </label>
<select class="form-select" multiple id="detail_domaine_recherche" name="domaine_recherche">
<option value="sciences_sociales">Sciences sociales</option>
<option value="naturelles">Naturelles</option>
<option value="humaines">Humaines</option>
<option value="veterinaires">Vétérinaires</option>
</select>
</div>
<div class="form-group mb-2">
<label>Budget :</label>
<input type="number" class="form-control" id="detail_budget" name='budget'>
</div>
<div class="form-group mb-2">
<label>Budget RH:</label>
<input type="number" class="form-control" id="detail_budget_rh" name='budget_RH'>
</div>
<div class="form-group mb-2">
<label>Description :</label>
<textarea class="form-control" id="detail_description" name='description'></textarea>
</div>
<div class="form-check-group mb-2">
<label class="form-check-label">Statut :</label>
<input type="text" class="form-control" id="detail_statut" readonly>
</div>
<button type='submit' class="btn btn-warning">
<i class="bi bi-pencil"></i> Modifier Projet
</button>
</form>
<hr class="my-4">
<div class="accordion" id="accordionDetailProjet">
<div class="accordion-item">
<h2 class="accordion-header" id="employesAffectes">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Employés Affectés
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="employesAffectes" data-bs-parent="#accordionDetailProjet">
<div class="accordion-body">
<div id="employes_affectes_projet"></div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="bailleursProjet">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
Liste des bailleurs
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="bailleursProjet" data-bs-parent="#accordionDetailProjet">
<div class="accordion-body">
<div id="bailleurs_projet"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
{% if user|has_group:"ressource_humaine" %}
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#modal_ajout_financement">
<i class="bi bi-plus-circle"></i> Ajouter un financement
</button>
{% endif %}
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle"></i> Fermer
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class="modal fade" id="modal_ajout_financement" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ajout de financement</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form method="POST" id="form_financement" action="{% url 'gestion_projet:ajouter_financement' %}">
{% csrf_token %}
{{ form_ajout_financement.as_p }}
</form>
</div>
<div class="modal-footer">
<button type="submit" id="btn_enregistrer_financement" class="btn btn-success">Enregistrer</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<div class="modal fade" id="modalDepenseActivite" tabindex="-1" aria-labelledby="modalDepenseActiviteLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalDepenseActiviteLabel">
<i class="bi bi-cash-stack me-2"></i> Mise à jour des dépenses
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form method="POST" action="{% url 'gestion_projet:mettre-a-jour-depense' %}">
{% csrf_token %}
<input type="hidden" name="id_activite" id="id_activite_depense">
<div class="mb-2">
<label class="form-label">Budget prévu (GNF)</label>
<input type="number" class="form-control" name="budget_prevu" id="budget_prevu" value="" disabled>
</div>
<div class="mb-2">
<label for="budget_depense" class="form-label">Montant dépensé (GNF)</label>
<input type="number" name="budget_depense" id="budget_depense" class="form-control" required>
</div>
<div class="mb-2">
<label class="form-label">Motif de différence</label>
<textarea class="form-control" name="motif" rows="3"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-success">
<i class="bi bi-save"></i> Enregistrer
</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,64 @@
{% extends "BASE.html" %}
{% load static %}
{% block 'titre_page' %} Gestion des projets {% endblock %}
{% block 'contenu' %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{% if message.tags == "error" %}danger{% else %}success{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<h5 class="mb-4 fw-bold text-uppercase text-orange-dark">
<i class="bi bi-kanban-fill me-2"></i> Suivi des Activités ({{ nom_projet }} )
</h5>
<div class="row d-flex justify-content-center mb-4">
<div class="col text-white bg-success d-flex flex-column justify-content-center align-items-center border rounded p-4">
<span class="fs-5">Budget Total (GNF)</span>
<h3>{{ budget_total }}</h3>
</div>
<div class="col text-white bg-info d-flex flex-column justify-content-center align-items-center border rounded mx-3 p-4">
<span class="fs-5">Budget RH (GNF)</span>
<h3>{{ budget_RH }}</h3>
</div>
<div class="col text-white bg-warning d-flex flex-column justify-content-center align-items-center border rounded p-4">
<span class="fs-5">Budget Dépensé (GNF)</span>
<h3>{{budget_depense}}</h3>
</div>
</div>
<div class="row">
<div class="mt-4 d-flex justify-content-between mb-3">
<h5><i class="bi bi-people"></i> La liste des Activités</h5>
<div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAjoutActivite">
<i class="bi bi-plus-circle me-2"></i> Ajouter une Activité
</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAjoutDocument">
<i class="bi bi-plus-circle me-2"></i> Ajouter un document
</button>
<button class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modalListeDocument">
Documents du projet
</button>
</div>
</div>
</div>
<div class="table-responsive">
<div id="tableau-liste-activite" data-urllisteactivite="{% url 'gestion_projet:liste-activites-projet' %}"></div>
</div>
{% 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' %}
<script type="text/javascript" src="{% static 'gestion_projet/js/suivi-activites.js' %}"></script>
<script type="text/javascript" src="{% static 'gestion_projet/js/liste_documents_projet.js' %}"></script>
{% endblock %}

3
gestion_projet/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

122
gestion_projet/urls.py Normal file
View File

@@ -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/<int:financement_id>/',
views.modifier_financement_projet,
name='modifier-financement'
),
path(
'projet/creation/',
views.creation_projet,
name='creation-projet'
),
path(
'projet/modifier/<int:projet_id>/',
views.modification_projet,
name='modifier-projet'
),
path(
'liste-projet/',
views.liste_projet,
name='liste-projet'
),
path(
'projet/suppression/<int:id>/',
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/<int:projet_id>',
views.affecter_employe_projet,
name='affecter-employe-projet'
),
path(
'projet/ajout-de-document/',
views.ajouter_document_projet,
name='ajouter-document'
),
path(
'projet/bailleurs/<str:projet_id>/',
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/<int:id>/',
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/<int:activite_id>/',
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/<str:projet_id>',
views.liste_employes_affectes,
name='liste-employes-affectes'
),
path(
'projet/mises-a-jour-projet',
views.mises_a_jour_projet,
name='mises-a-jour-projet'
)
]

485
gestion_projet/views.py Normal file
View File

@@ -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é nexiste pas."})
try:
pourcentage_nouveau = Decimal(pourcentage_recuperer) if pourcentage_recuperer else Decimal(0)
except (InvalidOperation, TypeError):
return JsonResponse({'success': False, 'message': "Le pourcentage saisi nest 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é nexiste 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é nexiste 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é nexiste 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é nexiste 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 lactivité '{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 na 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 nexiste 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 nexiste 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')

View File

3
gestion_salle/admin.py Normal file
View File

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

5
gestion_salle/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class GestionSalleConfig(AppConfig):
name = 'gestion_salle'

Some files were not shown because too many files have changed in this diff Show More