Début du projet
This commit is contained in:
commit
43b1f166d7
28 files changed
+4284
No files matched your search
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.vscode
|
||||
*.md
|
||||
!README.md
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Dépendances
|
||||
node_modules/
|
||||
|
||||
# Photos uploadées (conservées via volume Docker)
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
|
||||
# Fichiers système
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"projectColors.mainColor": "#2db81e",
|
||||
"window.title": "d:\\Users\\glainey\\developpement\\perso\\poupoule",
|
||||
"workbench.colorCustomizations": {
|
||||
"editor.background": "#F0F0F0",
|
||||
"minimap.background": "#21212180",
|
||||
"statusBar.background": "#2db81e",
|
||||
"tab.unfocusedActiveBorder": "#fff0",
|
||||
"statusBarItem.warningBackground": "#2db81e",
|
||||
"statusBarItem.warningForeground": "#ffffff",
|
||||
"statusBarItem.warningHoverBackground": "#2db81e",
|
||||
"statusBarItem.warningHoverForeground": "#ffffff90",
|
||||
"statusBarItem.remoteBackground": "#3ac52b",
|
||||
"statusBarItem.remoteForeground": "#ffffff",
|
||||
"statusBarItem.remoteHoverBackground": "#47d238",
|
||||
"statusBarItem.remoteHoverForeground": "#ffffff90",
|
||||
"statusBar.foreground": "#ffffff",
|
||||
"statusBar.border": "#2db81e",
|
||||
"statusBar.debuggingBackground": "#2db81e",
|
||||
"statusBar.debuggingForeground": "#ffffff",
|
||||
"statusBar.debuggingBorder": "#2db81e",
|
||||
"statusBar.noFolderBackground": "#2db81e",
|
||||
"statusBar.noFolderForeground": "#ffffff",
|
||||
"statusBar.noFolderBorder": "#2db81e",
|
||||
"statusBar.prominentBackground": "#2db81e",
|
||||
"statusBar.prominentForeground": "#ffffff",
|
||||
"statusBar.prominentHoverBackground": "#2db81e",
|
||||
"statusBar.prominentHoverForeground": "#ffffff90",
|
||||
"focusBorder": "#2db81e99",
|
||||
"progressBar.background": "#2db81e",
|
||||
"textLink.foreground": "#6df85e",
|
||||
"textLink.activeForeground": "#7aff6b",
|
||||
"selection.background": "#20ab11",
|
||||
"list.highlightForeground": "#2db81e",
|
||||
"list.focusAndSelectionOutline": "#2db81e99",
|
||||
"button.background": "#2db81e",
|
||||
"button.foreground": "#ffffff",
|
||||
"button.hoverBackground": "#3ac52b",
|
||||
"tab.activeBorderTop": "#3ac52b",
|
||||
"pickerGroup.foreground": "#3ac52b",
|
||||
"list.activeSelectionBackground": "#2db81e4d",
|
||||
"panelTitle.activeBorder": "#3ac52b",
|
||||
"activityBar.activeBorder": "#2db81e",
|
||||
"activityBarBadge.foreground": "#ffffff",
|
||||
"activityBarBadge.background": "#2db81e"
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p uploads data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,95 @@
|
||||
# Mon Poulailler
|
||||
|
||||
Application web simple pour gérer un poulailler : animaux, ponte, couvage, archives.
|
||||
|
||||
Conçue pour être **lisible par un débutant** : peu de fichiers, JavaScript classique, données en JSON.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- [Node.js](https://nodejs.org/) (version 18 ou plus), **ou**
|
||||
- [Docker](https://www.docker.com/) + Docker Compose
|
||||
|
||||
## Démarrage rapide (sans Docker)
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Ouvre ensuite : [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## Démarrage avec Docker
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Puis : [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
Pour arrêter :
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Compte par défaut
|
||||
|
||||
| Login | Mot de passe | Profil |
|
||||
| --- | --- | --- |
|
||||
| `admin` | `admin123` | administrateur |
|
||||
|
||||
Change ce mot de passe en créant un nouveau compte admin, puis en n'utilisant plus celui-ci (ou en modifiant le fichier JSON).
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Se connecter avec le compte admin
|
||||
2. Créer des comptes utilisateurs (menu **Comptes**, réservé à l'admin)
|
||||
3. Ajouter des poules / coqs depuis la page d'accueil
|
||||
4. Ouvrir une fiche animal pour modifier, enregistrer ponte / couvage, ou archiver
|
||||
5. Consulter les **Archives** pour les animaux décédés
|
||||
|
||||
## Structure du projet
|
||||
|
||||
```
|
||||
poupoule/
|
||||
├── server.js # Serveur Node.js (routes API)
|
||||
├── lib/donnees.js # Lecture / écriture des fichiers JSON
|
||||
├── data/ # Données persistantes (JSON)
|
||||
│ ├── utilisateurs.json
|
||||
│ ├── animaux.json
|
||||
│ ├── periodesPonte.json
|
||||
│ └── periodesCouvage.json
|
||||
├── uploads/ # Photos des animaux
|
||||
├── public/ # Interface (HTML, CSS, JS)
|
||||
│ ├── index.html # Liste + statistiques
|
||||
│ ├── animal.html # Fiche détaillée
|
||||
│ ├── archives.html
|
||||
│ ├── comptes.html
|
||||
│ ├── connexion.html
|
||||
│ ├── css/style.css
|
||||
│ └── js/ # Scripts par page
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── cahierDesCharges.md
|
||||
```
|
||||
|
||||
## Sauvegarde des données
|
||||
|
||||
Les informations sont stockées dans le dossier `data/` (fichiers JSON) et les photos dans `uploads/`.
|
||||
|
||||
Pour sauvegarder : **copie ces deux dossiers**.
|
||||
|
||||
Avec Docker, ils sont montés en volumes : ils restent sur ta machine même si le conteneur est reconstruit.
|
||||
|
||||
## Technologies
|
||||
|
||||
| Couche | Choix |
|
||||
| --- | --- |
|
||||
| Front | HTML, CSS, JavaScript (sans framework) |
|
||||
| Back | Node.js + Express |
|
||||
| Données | Fichiers JSON |
|
||||
| Auth | Sessions + mots de passe hachés (bcrypt) |
|
||||
|
||||
## Documentation métier
|
||||
|
||||
Voir [cahierDesCharges.md](cahierDesCharges.md).
|
||||
@@ -0,0 +1,191 @@
|
||||
# Cahier des charges — Mon Poulailler
|
||||
|
||||
## 1. Contexte
|
||||
|
||||
Le projet **Mon Poulailler** vise à créer une application web de gestion d’un poulailler.
|
||||
|
||||
Dans la gestion quotidienne d’un poulailler, il est important de connaître :
|
||||
|
||||
* le nombre de poules et de coqs ;
|
||||
* l’identité et les caractéristiques de chaque animal ;
|
||||
* l’état de santé ;
|
||||
* les périodes de ponte (début, durée, fin) ;
|
||||
* les périodes de couvage ;
|
||||
* l’historique des animaux décédés.
|
||||
|
||||
L’application doit permettre de suivre ces informations de façon centralisée et à jour.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Disposer d’une vue claire et à jour de l’état du poulailler : effectifs, animaux actifs, périodes de ponte et de couvage, et archives.
|
||||
|
||||
## 3. Périmètre
|
||||
|
||||
### 3.1 Inclus
|
||||
|
||||
* Authentification (création de comptes, connexion) ;
|
||||
* gestion des poules et des coqs ;
|
||||
* suivi des périodes de ponte et de couvage ;
|
||||
* statistiques d’effectifs en temps réel ;
|
||||
* archivage des animaux décédés ;
|
||||
* déploiement local via Docker.
|
||||
|
||||
### 3.2 Hors périmètre (version initiale)
|
||||
|
||||
* Application mobile native ;
|
||||
* notifications push / alertes automatiques ;
|
||||
* suivi vétérinaire détaillé (ordonnances, traitements) ;
|
||||
* gestion multi-poulaillers ;
|
||||
* synchronisation cloud / multi-appareils hors réseau local ;
|
||||
* inventaire des œufs au quotidien (sauf si lié explicitement à une période de ponte).
|
||||
|
||||
## 4. Utilisateurs
|
||||
|
||||
| Profil | Description | Droits principaux |
|
||||
| --- | --- | --- |
|
||||
| Administrateur | Responsable du poulailler / de l’application | Créer des comptes, gérer tous les animaux |
|
||||
| Utilisateur | Personne qui s’occupe des poules | Gérer les poules et les coqs (sans création de comptes) |
|
||||
|
||||
## 5. Fonctionnalités
|
||||
|
||||
### 5.1 Authentification et comptes
|
||||
|
||||
* Création de comptes (réservée à l’administrateur) ;
|
||||
* connexion par identifiant et mot de passe ;
|
||||
* déconnexion ;
|
||||
* deux profils : **Admin** et **Utilisateur**.
|
||||
|
||||
| Action | Admin | Utilisateur |
|
||||
| --- | --- | --- |
|
||||
| Se connecter / se déconnecter | Oui | Oui |
|
||||
| Créer un compte | Oui | Non |
|
||||
| Gérer poules et coqs | Oui | Oui |
|
||||
| Consulter les statistiques | Oui | Oui |
|
||||
| Consulter les archives | Oui | Oui |
|
||||
|
||||
### 5.2 Gestion des animaux (poules et coqs)
|
||||
|
||||
Pour chaque animal, l’application permet de renseigner et consulter :
|
||||
|
||||
| Donnée | Description |
|
||||
| --- | --- |
|
||||
| Identifiant unique | Identifiant technique unique dans l’application |
|
||||
| Photo | Image de l’animal |
|
||||
| Couleur de bague | Couleur de la bague d’identification |
|
||||
| Numéro de bague | Numéro associé à la bague |
|
||||
| Date de naissance | Date de naissance (ou date d’arrivée si inconnue) |
|
||||
| Type | Poule ou coq |
|
||||
| État de santé | Indication simple de l’état de santé |
|
||||
| Date de fin de vie | Renseignée lorsque l’animal décède |
|
||||
|
||||
Actions attendues :
|
||||
|
||||
* créer un animal ;
|
||||
* modifier ses informations ;
|
||||
* consulter sa fiche détaillée ;
|
||||
* marquer un animal comme décédé (passe en archive) ;
|
||||
* lister les animaux actifs ;
|
||||
* lister les animaux archivés (décédés).
|
||||
|
||||
### 5.3 Périodes de ponte (poules uniquement)
|
||||
|
||||
* Enregistrer une ou plusieurs périodes de ponte ;
|
||||
* pour chaque période : date de début, date de fin (optionnelle si en cours), éventuellement une remarque ;
|
||||
* indiquer si la poule pond actuellement.
|
||||
|
||||
### 5.4 Périodes de couvage (poules uniquement)
|
||||
|
||||
* Enregistrer une ou plusieurs périodes de couvage ;
|
||||
* pour chaque période : date de début, date de fin (optionnelle si en cours), éventuellement une remarque ;
|
||||
* indiquer si la poule est actuellement en couvage.
|
||||
|
||||
### 5.5 Statistiques
|
||||
|
||||
Affichage en temps réel (après chaque action ou au chargement) :
|
||||
|
||||
* nombre de poules actives ;
|
||||
* nombre de coqs actifs ;
|
||||
* nombre total d’animaux actifs ;
|
||||
* nombre de poules actuellement en couvage ;
|
||||
* nombre de poules actuellement en ponte (souhaitable).
|
||||
|
||||
### 5.6 Archives
|
||||
|
||||
* Vue dédiée aux animaux décédés ;
|
||||
* conservation des informations (identité, bagues, dates, historique de ponte / couvage) ;
|
||||
* pas de suppression définitive obligatoire en v1 (possibilité de la prévoir plus tard).
|
||||
|
||||
## 6. Exigences non fonctionnelles
|
||||
|
||||
| Exigence | Attendu |
|
||||
| --- | --- |
|
||||
| Accessibilité | Interface utilisable sur ordinateur et tablette (responsive de base) |
|
||||
| Performance | Réponses rapides sur un usage local (peu d’utilisateurs simultanés) |
|
||||
| Sécurité | Mots de passe stockés de façon sécurisée (hachage) ; accès réservé aux utilisateurs authentifiés |
|
||||
| Disponibilité | Application disponible en local via Docker |
|
||||
| Ergonomie | Navigation simple : liste, fiche animal, statistiques, archives |
|
||||
| Données | Persistance via fichiers JSON ; sauvegarde manuelle possible en copiant les fichiers |
|
||||
|
||||
## 7. Spécifications techniques
|
||||
|
||||
| Élément | Choix |
|
||||
| --- | --- |
|
||||
| Type d’application | Application web |
|
||||
| Hébergement | Local, via Docker |
|
||||
| Front-end | HTML, CSS, JavaScript |
|
||||
| Back-end | JavaScript (Node.js recommandé) |
|
||||
| Base de données | Fichiers JSON (stockage documentaire) |
|
||||
|
||||
### 7.1 Architecture cible (indicatif)
|
||||
|
||||
* Interface web servie par le back-end ;
|
||||
* API métier pour authentification et CRUD animaux / périodes ;
|
||||
* persistance dans un ou plusieurs fichiers JSON montés en volume Docker.
|
||||
|
||||
### 7.2 Contraintes techniques
|
||||
|
||||
* Fonctionnement hors cloud, en environnement local ;
|
||||
* pas de dépendance à une base relationnelle en v1 ;
|
||||
* les données doivent survivre à un redémarrage du conteneur (volume Docker).
|
||||
|
||||
## 8. Modèle de données (indicatif)
|
||||
|
||||
### Utilisateur
|
||||
|
||||
* `id`, `login`, `motDePasseHash`, `profil` (`admin` | `utilisateur`)
|
||||
|
||||
### Animal
|
||||
|
||||
* `id`, `type` (`poule` | `coq`), `photo`, `couleurBague`, `numeroBague`, `dateNaissance`, `etatSante`, `dateFinDeVie` (nullable), `actif` (booléen)
|
||||
|
||||
### PeriodePonte
|
||||
|
||||
* `id`, `idAnimal`, `dateDebut`, `dateFin` (nullable), `remarque`
|
||||
|
||||
### PeriodeCouvage
|
||||
|
||||
* `id`, `idAnimal`, `dateDebut`, `dateFin` (nullable), `remarque`
|
||||
|
||||
## 9. Critères d’acceptation (v1)
|
||||
|
||||
* Un administrateur peut créer un compte utilisateur et se connecter.
|
||||
* Un utilisateur connecté peut ajouter une poule avec photo, bague, date de naissance.
|
||||
* Un utilisateur peut enregistrer une période de ponte et une période de couvage.
|
||||
* Les statistiques affichent correctement le nombre de poules, de coqs et de poules en couvage.
|
||||
* Marquer une poule comme décédée la retire de la liste active et l’ajoute aux archives.
|
||||
* L’application démarre via Docker et conserve les données après redémarrage.
|
||||
|
||||
## 10. Livrables
|
||||
|
||||
* Code source de l’application ;
|
||||
* `Dockerfile` / `docker-compose` pour le lancement local ;
|
||||
* documentation courte de démarrage (installation, comptes, sauvegarde des JSON) ;
|
||||
* le présent cahier des charges à jour.
|
||||
|
||||
## 11. Evolutions possibles (hors v1)
|
||||
|
||||
* Suivi quotidien des œufs collectés ;
|
||||
* alertes (fin de ponte, couvage prolongé) ;
|
||||
* export CSV / PDF ;
|
||||
* multi-poulaillers ;
|
||||
* application mobile.
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"id": "mugpv73kclf5lu",
|
||||
"type": "poule",
|
||||
"photo": "/uploads/mugpvgsib4adaw.jpg",
|
||||
"couleurBague": "Rouge",
|
||||
"numeroBague": "38",
|
||||
"dateNaissance": "2026-04-05",
|
||||
"etatSante": "bon",
|
||||
"dateFinDeVie": null,
|
||||
"actif": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"id": "mugpvzh6249gwn",
|
||||
"idAnimal": "mugpv73kclf5lu",
|
||||
"dateDebut": "2026-03-03",
|
||||
"dateFin": null,
|
||||
"remarque": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"id": "mugpvr65eki768",
|
||||
"idAnimal": "mugpv73kclf5lu",
|
||||
"dateDebut": "2026-02-02",
|
||||
"dateFin": null,
|
||||
"remarque": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": "admin001",
|
||||
"login": "admin",
|
||||
"motDePasseHash": "$2a$10$PFIJmZ9LxGO5pAqR3d8Ld.zaMQdg0F..BLzPPWz8a.P0JsJkcGp4q",
|
||||
"profil": "admin"
|
||||
},
|
||||
{
|
||||
"id": "mugpwhdhkoe7ar",
|
||||
"login": "Greg",
|
||||
"motDePasseHash": "$2a$10$5H5xgUTeJvptVTNH5szKG.7.kfXDb8llbT2JNzaCiblAVivQE8HZq",
|
||||
"profil": "utilisateur"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
poulailler:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
# Les données JSON survivent au redémarrage du conteneur
|
||||
- ./data:/app/data
|
||||
# Les photos uploadées aussi
|
||||
- ./uploads:/app/uploads
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,50 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const dossierData = path.join(__dirname, "..", "data");
|
||||
|
||||
/**
|
||||
* Lit un fichier JSON et retourne son contenu.
|
||||
* Si le fichier n'existe pas, retourne la valeur par défaut.
|
||||
*/
|
||||
function lireJson(nomFichier, valeurParDefaut)
|
||||
{
|
||||
const chemin = path.join(dossierData, nomFichier);
|
||||
|
||||
if (!fs.existsSync(chemin))
|
||||
{
|
||||
return valeurParDefaut;
|
||||
}
|
||||
|
||||
const contenu = fs.readFileSync(chemin, "utf8");
|
||||
return JSON.parse(contenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Écrit un objet JavaScript dans un fichier JSON.
|
||||
*/
|
||||
function ecrireJson(nomFichier, donnees)
|
||||
{
|
||||
if (!fs.existsSync(dossierData))
|
||||
{
|
||||
fs.mkdirSync(dossierData, { recursive: true });
|
||||
}
|
||||
|
||||
const chemin = path.join(dossierData, nomFichier);
|
||||
fs.writeFileSync(chemin, JSON.stringify(donnees, null, 2), "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un identifiant simple à partir de la date et d'un nombre aléatoire.
|
||||
*/
|
||||
function genererId()
|
||||
{
|
||||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lireJson,
|
||||
ecrireJson,
|
||||
genererId,
|
||||
dossierData
|
||||
};
|
||||
Generated
+1668
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "mon-poulailler",
|
||||
"version": "1.0.0",
|
||||
"description": "Application web simple de gestion d'un poulailler",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.1",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fiche animal — Mon Poulailler</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="entete">
|
||||
<h1><a href="/">Mon Poulailler</a></h1>
|
||||
<nav>
|
||||
<a href="/">Animaux</a>
|
||||
<a href="/archives.html">Archives</a>
|
||||
<a href="/comptes.html" id="lienComptes" hidden>Comptes</a>
|
||||
<button type="button" id="boutonDeconnexion">Déconnexion</button>
|
||||
</nav>
|
||||
<p class="info-session">Connecté : <span id="nomUtilisateur"></span></p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<p><a href="/" class="lien-retour">← Retour à la liste</a></p>
|
||||
|
||||
<section id="ficheAnimal" class="fiche">
|
||||
<p>Chargement…</p>
|
||||
</section>
|
||||
|
||||
<section id="sectionPeriodes" hidden>
|
||||
<h2>Périodes de ponte</h2>
|
||||
<div id="listePonte"></div>
|
||||
<button type="button" id="boutonAjouterPonte">Ajouter une période de ponte</button>
|
||||
|
||||
<h2>Périodes de couvage</h2>
|
||||
<div id="listeCouvage"></div>
|
||||
<button type="button" id="boutonAjouterCouvage">Ajouter une période de couvage</button>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog id="dialoguePeriode">
|
||||
<form id="formPeriode" method="dialog">
|
||||
<h3 id="titrePeriode">Nouvelle période</h3>
|
||||
<input type="hidden" id="typePeriode">
|
||||
|
||||
<label for="dateDebut">Date de début</label>
|
||||
<input type="date" id="dateDebut" required>
|
||||
|
||||
<label for="dateFin">Date de fin (vide si en cours)</label>
|
||||
<input type="date" id="dateFin">
|
||||
|
||||
<label for="remarque">Remarque</label>
|
||||
<textarea id="remarque" rows="3"></textarea>
|
||||
|
||||
<p id="erreurPeriode" class="erreur" hidden></p>
|
||||
|
||||
<div class="actions-dialogue">
|
||||
<button type="button" id="annulerPeriode" class="secondaire">Annuler</button>
|
||||
<button type="submit">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="dialogueDeces">
|
||||
<form id="formDeces" method="dialog">
|
||||
<h3>Marquer comme décédé</h3>
|
||||
<p>L'animal passera dans les archives. Les périodes en cours seront clôturées.</p>
|
||||
|
||||
<label for="dateFinDeVie">Date de fin de vie</label>
|
||||
<input type="date" id="dateFinDeVie" required>
|
||||
|
||||
<div class="actions-dialogue">
|
||||
<button type="button" id="annulerDeces" class="secondaire">Annuler</button>
|
||||
<button type="submit" class="danger">Confirmer</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="dialogueAnimal">
|
||||
<form id="formAnimal" method="dialog">
|
||||
<h3>Modifier l'animal</h3>
|
||||
<input type="hidden" id="animalId">
|
||||
|
||||
<label for="type">Type</label>
|
||||
<select id="type" required>
|
||||
<option value="poule">Poule</option>
|
||||
<option value="coq">Coq</option>
|
||||
</select>
|
||||
|
||||
<label for="couleurBague">Couleur de bague</label>
|
||||
<input type="text" id="couleurBague">
|
||||
|
||||
<label for="numeroBague">Numéro de bague</label>
|
||||
<input type="text" id="numeroBague">
|
||||
|
||||
<label for="dateNaissance">Date de naissance / arrivée</label>
|
||||
<input type="date" id="dateNaissance">
|
||||
|
||||
<label for="etatSante">État de santé</label>
|
||||
<select id="etatSante">
|
||||
<option value="bon">Bon</option>
|
||||
<option value="moyen">Moyen</option>
|
||||
<option value="mauvais">Mauvais</option>
|
||||
</select>
|
||||
|
||||
<label for="photo">Nouvelle photo (optionnel)</label>
|
||||
<input type="file" id="photo" accept="image/*">
|
||||
|
||||
<p id="erreurAnimal" class="erreur" hidden></p>
|
||||
|
||||
<div class="actions-dialogue">
|
||||
<button type="button" id="annulerAnimal" class="secondaire">Annuler</button>
|
||||
<button type="submit">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="/js/commun.js"></script>
|
||||
<script src="/js/fiche.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Archives — Mon Poulailler</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="entete">
|
||||
<h1><a href="/">Mon Poulailler</a></h1>
|
||||
<nav>
|
||||
<a href="/">Animaux</a>
|
||||
<a href="/archives.html" class="actif">Archives</a>
|
||||
<a href="/comptes.html" id="lienComptes" hidden>Comptes</a>
|
||||
<button type="button" id="boutonDeconnexion">Déconnexion</button>
|
||||
</nav>
|
||||
<p class="info-session">Connecté : <span id="nomUtilisateur"></span></p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>Animaux décédés</h2>
|
||||
<div id="listeArchives" class="grille-animaux"></div>
|
||||
<p id="messageVide" class="message-vide" hidden>Aucun animal archivé.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/js/commun.js"></script>
|
||||
<script src="/js/archives.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Comptes — Mon Poulailler</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="entete">
|
||||
<h1><a href="/">Mon Poulailler</a></h1>
|
||||
<nav>
|
||||
<a href="/">Animaux</a>
|
||||
<a href="/archives.html">Archives</a>
|
||||
<a href="/comptes.html" class="actif" id="lienComptes">Comptes</a>
|
||||
<button type="button" id="boutonDeconnexion">Déconnexion</button>
|
||||
</nav>
|
||||
<p class="info-session">Connecté : <span id="nomUtilisateur"></span></p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<div class="titre-avec-action">
|
||||
<h2>Comptes utilisateurs</h2>
|
||||
<button type="button" id="boutonNouveauCompte">Créer un compte</button>
|
||||
</div>
|
||||
|
||||
<table class="tableau">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Login</th>
|
||||
<th>Profil</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="listeComptes"></tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog id="dialogueCompte">
|
||||
<form id="formCompte" method="dialog">
|
||||
<h3>Créer un compte</h3>
|
||||
|
||||
<label for="login">Identifiant</label>
|
||||
<input type="text" id="login" required>
|
||||
|
||||
<label for="motDePasse">Mot de passe</label>
|
||||
<input type="password" id="motDePasse" required minlength="4">
|
||||
|
||||
<label for="profil">Profil</label>
|
||||
<select id="profil">
|
||||
<option value="utilisateur">Utilisateur</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
|
||||
<p id="erreurCompte" class="erreur" hidden></p>
|
||||
|
||||
<div class="actions-dialogue">
|
||||
<button type="button" id="annulerCompte" class="secondaire">Annuler</button>
|
||||
<button type="submit">Créer</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="/js/commun.js"></script>
|
||||
<script src="/js/comptes.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion — Mon Poulailler</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="page-connexion">
|
||||
<main class="boite-connexion">
|
||||
<h1>Mon Poulailler</h1>
|
||||
<p class="sous-titre">Connexion</p>
|
||||
|
||||
<form id="formConnexion">
|
||||
<label for="login">Identifiant</label>
|
||||
<input type="text" id="login" name="login" required autocomplete="username">
|
||||
|
||||
<label for="motDePasse">Mot de passe</label>
|
||||
<input type="password" id="motDePasse" name="motDePasse" required autocomplete="current-password">
|
||||
|
||||
<p id="messageErreur" class="erreur" hidden></p>
|
||||
|
||||
<button type="submit">Se connecter</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="/js/commun.js"></script>
|
||||
<script src="/js/connexion.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,476 @@
|
||||
:root
|
||||
{
|
||||
--vert: #3d6b4f;
|
||||
--vert-fonce: #2a4a37;
|
||||
--beige: #f7f3eb;
|
||||
--creme: #fffdf8;
|
||||
--texte: #2c2c2c;
|
||||
--bordure: #d4cbb8;
|
||||
--erreur: #a33;
|
||||
--danger: #8b3a3a;
|
||||
}
|
||||
|
||||
*
|
||||
{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: var(--beige);
|
||||
color: var(--texte);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a
|
||||
{
|
||||
color: var(--vert);
|
||||
}
|
||||
|
||||
/* ----- Connexion ----- */
|
||||
|
||||
.page-connexion
|
||||
{
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background:
|
||||
linear-gradient(160deg, #e8f0e4 0%, var(--beige) 50%, #efe6d4 100%);
|
||||
}
|
||||
|
||||
.boite-connexion
|
||||
{
|
||||
background: var(--creme);
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--bordure);
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.boite-connexion h1
|
||||
{
|
||||
margin: 0 0 0.25rem;
|
||||
color: var(--vert-fonce);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sous-titre
|
||||
{
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* ----- En-tête ----- */
|
||||
|
||||
.entete
|
||||
{
|
||||
background: var(--vert);
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.entete h1
|
||||
{
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.entete h1 a
|
||||
{
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.entete nav
|
||||
{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.entete nav a
|
||||
{
|
||||
color: #e8f0e4;
|
||||
text-decoration: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.entete nav a.actif,
|
||||
.entete nav a:hover
|
||||
{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info-session
|
||||
{
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
main
|
||||
{
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* ----- Formulaires ----- */
|
||||
|
||||
label
|
||||
{
|
||||
display: block;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button
|
||||
{
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="date"],
|
||||
input[type="file"],
|
||||
select,
|
||||
textarea
|
||||
{
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--bordure);
|
||||
background: white;
|
||||
}
|
||||
|
||||
button
|
||||
{
|
||||
margin-top: 1rem;
|
||||
padding: 0.55rem 1rem;
|
||||
background: var(--vert);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover
|
||||
{
|
||||
background: var(--vert-fonce);
|
||||
}
|
||||
|
||||
button.secondaire
|
||||
{
|
||||
background: #888;
|
||||
}
|
||||
|
||||
button.secondaire:hover
|
||||
{
|
||||
background: #666;
|
||||
}
|
||||
|
||||
button.danger
|
||||
{
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.erreur
|
||||
{
|
||||
color: var(--erreur);
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
/* ----- Stats ----- */
|
||||
|
||||
.statistiques
|
||||
{
|
||||
background: var(--creme);
|
||||
border: 1px solid var(--bordure);
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.statistiques h2
|
||||
{
|
||||
margin-top: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.liste-stats
|
||||
{
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.liste-stats li
|
||||
{
|
||||
background: #eef5ee;
|
||||
padding: 0.5rem 0.85rem;
|
||||
}
|
||||
|
||||
.liste-stats span
|
||||
{
|
||||
font-weight: 700;
|
||||
color: var(--vert-fonce);
|
||||
}
|
||||
|
||||
/* ----- Liste animaux ----- */
|
||||
|
||||
.titre-avec-action
|
||||
{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.titre-avec-action h2
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.titre-avec-action button
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.grille-animaux
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.carte-animal
|
||||
{
|
||||
background: var(--creme);
|
||||
border: 1px solid var(--bordure);
|
||||
padding: 0.75rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.carte-animal:hover
|
||||
{
|
||||
border-color: var(--vert);
|
||||
}
|
||||
|
||||
.carte-animal img
|
||||
{
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
background: #ddd;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.carte-animal .sans-photo
|
||||
{
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
background: #ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.carte-animal h3
|
||||
{
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
font-size: 1rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.carte-animal p
|
||||
{
|
||||
margin: 0.15rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.badge
|
||||
{
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
background: #eef5ee;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.message-vide
|
||||
{
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ----- Fiche ----- */
|
||||
|
||||
.fiche
|
||||
{
|
||||
background: var(--creme);
|
||||
border: 1px solid var(--bordure);
|
||||
padding: 1.25rem;
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.fiche img,
|
||||
.fiche .sans-photo
|
||||
{
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.fiche .sans-photo
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.fiche dl
|
||||
{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fiche dt
|
||||
{
|
||||
font-weight: 600;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.fiche dd
|
||||
{
|
||||
margin: 0.15rem 0 0;
|
||||
}
|
||||
|
||||
.actions-fiche
|
||||
{
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.actions-fiche button
|
||||
{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.lien-retour
|
||||
{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#sectionPeriodes h2
|
||||
{
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.tableau-periodes
|
||||
{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0.75rem 0;
|
||||
background: var(--creme);
|
||||
}
|
||||
|
||||
.tableau-periodes th,
|
||||
.tableau-periodes td
|
||||
{
|
||||
border: 1px solid var(--bordure);
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tableau
|
||||
{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
background: var(--creme);
|
||||
}
|
||||
|
||||
.tableau th,
|
||||
.tableau td
|
||||
{
|
||||
border: 1px solid var(--bordure);
|
||||
padding: 0.6rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* ----- Dialogues ----- */
|
||||
|
||||
dialog
|
||||
{
|
||||
border: 1px solid var(--bordure);
|
||||
padding: 1.25rem;
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
background: var(--creme);
|
||||
}
|
||||
|
||||
dialog::backdrop
|
||||
{
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.actions-dialogue
|
||||
{
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.actions-dialogue button
|
||||
{
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* ----- Responsive ----- */
|
||||
|
||||
@media (max-width: 640px)
|
||||
{
|
||||
.fiche
|
||||
{
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.fiche img,
|
||||
.fiche .sans-photo
|
||||
{
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.entete
|
||||
{
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mon Poulailler</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="entete">
|
||||
<h1><a href="/">Mon Poulailler</a></h1>
|
||||
<nav>
|
||||
<a href="/" class="actif">Animaux</a>
|
||||
<a href="/archives.html">Archives</a>
|
||||
<a href="/comptes.html" id="lienComptes" hidden>Comptes</a>
|
||||
<button type="button" id="boutonDeconnexion">Déconnexion</button>
|
||||
</nav>
|
||||
<p class="info-session">Connecté : <span id="nomUtilisateur"></span></p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="statistiques" id="blocStatistiques">
|
||||
<h2>Statistiques</h2>
|
||||
<ul class="liste-stats">
|
||||
<li><span id="statPoules">0</span> poules</li>
|
||||
<li><span id="statCoqs">0</span> coqs</li>
|
||||
<li><span id="statTotal">0</span> actifs</li>
|
||||
<li><span id="statPonte">0</span> en ponte</li>
|
||||
<li><span id="statCouvage">0</span> en couvage</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="titre-avec-action">
|
||||
<h2>Animaux actifs</h2>
|
||||
<button type="button" id="boutonNouvelAnimal">Ajouter un animal</button>
|
||||
</div>
|
||||
|
||||
<div id="listeAnimaux" class="grille-animaux"></div>
|
||||
<p id="messageVide" class="message-vide" hidden>Aucun animal actif pour le moment.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Formulaire d'ajout / modification -->
|
||||
<dialog id="dialogueAnimal">
|
||||
<form id="formAnimal" method="dialog">
|
||||
<h3 id="titreDialogueAnimal">Ajouter un animal</h3>
|
||||
<input type="hidden" id="animalId">
|
||||
|
||||
<label for="type">Type</label>
|
||||
<select id="type" required>
|
||||
<option value="poule">Poule</option>
|
||||
<option value="coq">Coq</option>
|
||||
</select>
|
||||
|
||||
<label for="couleurBague">Couleur de bague</label>
|
||||
<input type="text" id="couleurBague" placeholder="ex. rouge">
|
||||
|
||||
<label for="numeroBague">Numéro de bague</label>
|
||||
<input type="text" id="numeroBague" placeholder="ex. 12">
|
||||
|
||||
<label for="dateNaissance">Date de naissance / arrivée</label>
|
||||
<input type="date" id="dateNaissance">
|
||||
|
||||
<label for="etatSante">État de santé</label>
|
||||
<select id="etatSante">
|
||||
<option value="bon">Bon</option>
|
||||
<option value="moyen">Moyen</option>
|
||||
<option value="mauvais">Mauvais</option>
|
||||
</select>
|
||||
|
||||
<label for="photo">Photo</label>
|
||||
<input type="file" id="photo" accept="image/*">
|
||||
|
||||
<p id="erreurAnimal" class="erreur" hidden></p>
|
||||
|
||||
<div class="actions-dialogue">
|
||||
<button type="button" id="annulerAnimal" class="secondaire">Annuler</button>
|
||||
<button type="submit">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script src="/js/commun.js"></script>
|
||||
<script src="/js/accueil.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Page d'accueil : statistiques + liste des animaux actifs.
|
||||
*/
|
||||
|
||||
let utilisateurCourant = null;
|
||||
|
||||
async function chargerStatistiques()
|
||||
{
|
||||
const stats = await appelerApi("/api/statistiques");
|
||||
|
||||
if (!stats)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("statPoules").textContent = stats.nombrePoulesActives;
|
||||
document.getElementById("statCoqs").textContent = stats.nombreCoqsActifs;
|
||||
document.getElementById("statTotal").textContent = stats.nombreTotalActifs;
|
||||
document.getElementById("statPonte").textContent = stats.nombreEnPonte;
|
||||
document.getElementById("statCouvage").textContent = stats.nombreEnCouvage;
|
||||
}
|
||||
|
||||
async function chargerAnimaux()
|
||||
{
|
||||
const animaux = await appelerApi("/api/animaux?actifs=oui");
|
||||
|
||||
if (!animaux)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const liste = document.getElementById("listeAnimaux");
|
||||
const messageVide = document.getElementById("messageVide");
|
||||
|
||||
if (animaux.length === 0)
|
||||
{
|
||||
liste.innerHTML = "";
|
||||
messageVide.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
messageVide.hidden = true;
|
||||
liste.innerHTML = animaux.map(htmlCarteAnimal).join("");
|
||||
}
|
||||
|
||||
function ouvrirDialogueAjout()
|
||||
{
|
||||
document.getElementById("titreDialogueAnimal").textContent = "Ajouter un animal";
|
||||
document.getElementById("animalId").value = "";
|
||||
document.getElementById("formAnimal").reset();
|
||||
document.getElementById("erreurAnimal").hidden = true;
|
||||
document.getElementById("dialogueAnimal").showModal();
|
||||
}
|
||||
|
||||
document.getElementById("boutonNouvelAnimal").addEventListener("click", ouvrirDialogueAjout);
|
||||
|
||||
document.getElementById("annulerAnimal").addEventListener("click", function ()
|
||||
{
|
||||
document.getElementById("dialogueAnimal").close();
|
||||
});
|
||||
|
||||
document.getElementById("formAnimal").addEventListener("submit", async function (event)
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
const erreurAnimal = document.getElementById("erreurAnimal");
|
||||
erreurAnimal.hidden = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("type", document.getElementById("type").value);
|
||||
formData.append("couleurBague", document.getElementById("couleurBague").value);
|
||||
formData.append("numeroBague", document.getElementById("numeroBague").value);
|
||||
formData.append("dateNaissance", document.getElementById("dateNaissance").value);
|
||||
formData.append("etatSante", document.getElementById("etatSante").value);
|
||||
|
||||
const photo = document.getElementById("photo").files[0];
|
||||
if (photo)
|
||||
{
|
||||
formData.append("photo", photo);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await appelerApi("/api/animaux", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
||||
document.getElementById("dialogueAnimal").close();
|
||||
await chargerAnimaux();
|
||||
await chargerStatistiques();
|
||||
}
|
||||
catch (erreur)
|
||||
{
|
||||
erreurAnimal.textContent = erreur.message;
|
||||
erreurAnimal.hidden = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function demarrer()
|
||||
{
|
||||
utilisateurCourant = await verifierSession();
|
||||
|
||||
if (!utilisateurCourant)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
brancherDeconnexion();
|
||||
await chargerStatistiques();
|
||||
await chargerAnimaux();
|
||||
}
|
||||
|
||||
demarrer();
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Page archives : animaux décédés.
|
||||
*/
|
||||
|
||||
async function chargerArchives()
|
||||
{
|
||||
const animaux = await appelerApi("/api/animaux?archives=oui");
|
||||
|
||||
if (!animaux)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const liste = document.getElementById("listeArchives");
|
||||
const messageVide = document.getElementById("messageVide");
|
||||
|
||||
if (animaux.length === 0)
|
||||
{
|
||||
liste.innerHTML = "";
|
||||
messageVide.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
messageVide.hidden = true;
|
||||
liste.innerHTML = animaux.map(htmlCarteAnimal).join("");
|
||||
}
|
||||
|
||||
async function demarrer()
|
||||
{
|
||||
const utilisateur = await verifierSession();
|
||||
|
||||
if (!utilisateur)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
brancherDeconnexion();
|
||||
await chargerArchives();
|
||||
}
|
||||
|
||||
demarrer();
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Fonctions partagées entre les pages.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Appelle l'API et retourne la réponse JSON.
|
||||
* Redirige vers la page de connexion si non authentifié (401).
|
||||
*/
|
||||
async function appelerApi(url, options)
|
||||
{
|
||||
const reponse = await fetch(url, options);
|
||||
const donnees = await reponse.json().catch(function ()
|
||||
{
|
||||
return {};
|
||||
});
|
||||
|
||||
if (reponse.status === 401 && !url.includes("/api/connexion"))
|
||||
{
|
||||
window.location.href = "/connexion.html";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!reponse.ok)
|
||||
{
|
||||
throw new Error(donnees.erreur || "Erreur serveur");
|
||||
}
|
||||
|
||||
return donnees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie la session. Redirige vers connexion si besoin.
|
||||
* Affiche le nom et le lien Comptes pour l'admin.
|
||||
*/
|
||||
async function verifierSession()
|
||||
{
|
||||
const session = await appelerApi("/api/session");
|
||||
|
||||
if (!session || !session.connecte)
|
||||
{
|
||||
window.location.href = "/connexion.html";
|
||||
return null;
|
||||
}
|
||||
|
||||
const nomUtilisateur = document.getElementById("nomUtilisateur");
|
||||
if (nomUtilisateur)
|
||||
{
|
||||
nomUtilisateur.textContent = session.utilisateur.login + " (" + session.utilisateur.profil + ")";
|
||||
}
|
||||
|
||||
const lienComptes = document.getElementById("lienComptes");
|
||||
if (lienComptes && session.utilisateur.profil === "admin")
|
||||
{
|
||||
lienComptes.hidden = false;
|
||||
}
|
||||
|
||||
return session.utilisateur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Branche le bouton de déconnexion s'il est présent sur la page.
|
||||
*/
|
||||
function brancherDeconnexion()
|
||||
{
|
||||
const bouton = document.getElementById("boutonDeconnexion");
|
||||
|
||||
if (!bouton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bouton.addEventListener("click", async function ()
|
||||
{
|
||||
await appelerApi("/api/deconnexion", { method: "POST" });
|
||||
window.location.href = "/connexion.html";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche une étiquette lisible pour le type d'animal.
|
||||
*/
|
||||
function libelleType(type)
|
||||
{
|
||||
return type === "coq" ? "Coq" : "Poule";
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche une étiquette pour l'état de santé.
|
||||
*/
|
||||
function libelleSante(etat)
|
||||
{
|
||||
if (etat === "moyen")
|
||||
{
|
||||
return "Moyen";
|
||||
}
|
||||
|
||||
if (etat === "mauvais")
|
||||
{
|
||||
return "Mauvais";
|
||||
}
|
||||
|
||||
return "Bon";
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le HTML d'une carte animal (liste).
|
||||
*/
|
||||
function htmlCarteAnimal(animal)
|
||||
{
|
||||
let photoHtml = "<div class=\"sans-photo\">Pas de photo</div>";
|
||||
|
||||
if (animal.photo)
|
||||
{
|
||||
photoHtml = "<img src=\"" + animal.photo + "\" alt=\"Photo de l'animal\">";
|
||||
}
|
||||
|
||||
let badges = "";
|
||||
|
||||
if (animal.actif === false && animal.dateFinDeVie)
|
||||
{
|
||||
badges += "<span class=\"badge\">Décédé le " + animal.dateFinDeVie + "</span>";
|
||||
}
|
||||
|
||||
return (
|
||||
"<a class=\"carte-animal\" href=\"/animal.html?id=" + animal.id + "\">" +
|
||||
photoHtml +
|
||||
"<h3>" + libelleType(animal.type) + "</h3>" +
|
||||
"<p>Bague : " + (animal.couleurBague || "—") + " " + (animal.numeroBague || "") + "</p>" +
|
||||
"<p>Santé : " + libelleSante(animal.etatSante) + "</p>" +
|
||||
badges +
|
||||
"</a>"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Page comptes (admin uniquement) : liste et création.
|
||||
*/
|
||||
|
||||
async function chargerComptes()
|
||||
{
|
||||
const comptes = await appelerApi("/api/utilisateurs");
|
||||
|
||||
if (!comptes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const tbody = document.getElementById("listeComptes");
|
||||
tbody.innerHTML = comptes.map(function (compte)
|
||||
{
|
||||
return (
|
||||
"<tr>" +
|
||||
"<td>" + compte.login + "</td>" +
|
||||
"<td>" + compte.profil + "</td>" +
|
||||
"</tr>"
|
||||
);
|
||||
}).join("");
|
||||
}
|
||||
|
||||
document.getElementById("boutonNouveauCompte").addEventListener("click", function ()
|
||||
{
|
||||
document.getElementById("formCompte").reset();
|
||||
document.getElementById("erreurCompte").hidden = true;
|
||||
document.getElementById("dialogueCompte").showModal();
|
||||
});
|
||||
|
||||
document.getElementById("annulerCompte").addEventListener("click", function ()
|
||||
{
|
||||
document.getElementById("dialogueCompte").close();
|
||||
});
|
||||
|
||||
document.getElementById("formCompte").addEventListener("submit", async function (event)
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
const erreurCompte = document.getElementById("erreurCompte");
|
||||
erreurCompte.hidden = true;
|
||||
|
||||
try
|
||||
{
|
||||
await appelerApi("/api/utilisateurs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
login: document.getElementById("login").value.trim(),
|
||||
motDePasse: document.getElementById("motDePasse").value,
|
||||
profil: document.getElementById("profil").value
|
||||
})
|
||||
});
|
||||
|
||||
document.getElementById("dialogueCompte").close();
|
||||
await chargerComptes();
|
||||
}
|
||||
catch (erreur)
|
||||
{
|
||||
erreurCompte.textContent = erreur.message;
|
||||
erreurCompte.hidden = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function demarrer()
|
||||
{
|
||||
const utilisateur = await verifierSession();
|
||||
|
||||
if (!utilisateur)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (utilisateur.profil !== "admin")
|
||||
{
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
brancherDeconnexion();
|
||||
await chargerComptes();
|
||||
}
|
||||
|
||||
demarrer();
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Page de connexion.
|
||||
*/
|
||||
|
||||
document.getElementById("formConnexion").addEventListener("submit", async function (event)
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
const messageErreur = document.getElementById("messageErreur");
|
||||
messageErreur.hidden = true;
|
||||
|
||||
const login = document.getElementById("login").value.trim();
|
||||
const motDePasse = document.getElementById("motDePasse").value;
|
||||
|
||||
try
|
||||
{
|
||||
await appelerApi("/api/connexion", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
login: login,
|
||||
motDePasse: motDePasse
|
||||
})
|
||||
});
|
||||
|
||||
window.location.href = "/";
|
||||
}
|
||||
catch (erreur)
|
||||
{
|
||||
messageErreur.textContent = erreur.message;
|
||||
messageErreur.hidden = false;
|
||||
}
|
||||
});
|
||||
|
||||
/* Si déjà connecté, aller directement à l'accueil */
|
||||
appelerApi("/api/session").then(function (session)
|
||||
{
|
||||
if (session && session.connecte)
|
||||
{
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* Page fiche animal : détail, modification, périodes, décès.
|
||||
*/
|
||||
|
||||
let animalCourant = null;
|
||||
let periodesPonte = [];
|
||||
let periodesCouvage = [];
|
||||
|
||||
function recupererIdDepuisUrl()
|
||||
{
|
||||
const parametres = new URLSearchParams(window.location.search);
|
||||
return parametres.get("id");
|
||||
}
|
||||
|
||||
function afficherFiche()
|
||||
{
|
||||
const animal = animalCourant;
|
||||
let photoHtml = "<div class=\"sans-photo\">Pas de photo</div>";
|
||||
|
||||
if (animal.photo)
|
||||
{
|
||||
photoHtml = "<img src=\"" + animal.photo + "\" alt=\"Photo\">";
|
||||
}
|
||||
|
||||
let actions = "";
|
||||
|
||||
if (animal.actif)
|
||||
{
|
||||
actions =
|
||||
"<div class=\"actions-fiche\">" +
|
||||
"<button type=\"button\" id=\"boutonModifier\">Modifier</button>" +
|
||||
"<button type=\"button\" id=\"boutonDeces\" class=\"danger\">Marquer comme décédé</button>" +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
const dateFin = animal.dateFinDeVie
|
||||
? "<dt>Date de fin de vie</dt><dd>" + animal.dateFinDeVie + "</dd>"
|
||||
: "";
|
||||
|
||||
document.getElementById("ficheAnimal").innerHTML =
|
||||
photoHtml +
|
||||
"<div>" +
|
||||
"<h2>" + libelleType(animal.type) + "</h2>" +
|
||||
"<dl>" +
|
||||
"<dt>Couleur de bague</dt><dd>" + (animal.couleurBague || "—") + "</dd>" +
|
||||
"<dt>Numéro de bague</dt><dd>" + (animal.numeroBague || "—") + "</dd>" +
|
||||
"<dt>Date de naissance</dt><dd>" + (animal.dateNaissance || "—") + "</dd>" +
|
||||
"<dt>État de santé</dt><dd>" + libelleSante(animal.etatSante) + "</dd>" +
|
||||
"<dt>Statut</dt><dd>" + (animal.actif ? "Actif" : "Archivé") + "</dd>" +
|
||||
dateFin +
|
||||
"</dl>" +
|
||||
actions +
|
||||
"</div>";
|
||||
|
||||
if (animal.actif)
|
||||
{
|
||||
document.getElementById("boutonModifier").addEventListener("click", ouvrirModification);
|
||||
document.getElementById("boutonDeces").addEventListener("click", ouvrirDeces);
|
||||
}
|
||||
|
||||
/* Ponte et couvage : poules uniquement */
|
||||
const sectionPeriodes = document.getElementById("sectionPeriodes");
|
||||
|
||||
if (animal.type === "poule")
|
||||
{
|
||||
sectionPeriodes.hidden = false;
|
||||
afficherPeriodes();
|
||||
}
|
||||
else
|
||||
{
|
||||
sectionPeriodes.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function htmlTableauPeriodes(periodes, typePeriode)
|
||||
{
|
||||
if (periodes.length === 0)
|
||||
{
|
||||
return "<p class=\"message-vide\">Aucune période enregistrée.</p>";
|
||||
}
|
||||
|
||||
let lignes = periodes.map(function (periode)
|
||||
{
|
||||
const enCours = !periode.dateFin;
|
||||
const statut = enCours ? "<span class=\"badge\">En cours</span>" : "";
|
||||
let boutonCloture = "";
|
||||
|
||||
if (enCours && animalCourant.actif)
|
||||
{
|
||||
boutonCloture =
|
||||
"<button type=\"button\" class=\"secondaire\" data-cloturer=\"" +
|
||||
typePeriode + "\" data-id=\"" + periode.id + "\">Clôturer</button>";
|
||||
}
|
||||
|
||||
return (
|
||||
"<tr>" +
|
||||
"<td>" + periode.dateDebut + "</td>" +
|
||||
"<td>" + (periode.dateFin || "—") + " " + statut + "</td>" +
|
||||
"<td>" + (periode.remarque || "—") + "</td>" +
|
||||
"<td>" + boutonCloture + "</td>" +
|
||||
"</tr>"
|
||||
);
|
||||
}).join("");
|
||||
|
||||
return (
|
||||
"<table class=\"tableau-periodes\">" +
|
||||
"<thead><tr><th>Début</th><th>Fin</th><th>Remarque</th><th></th></tr></thead>" +
|
||||
"<tbody>" + lignes + "</tbody>" +
|
||||
"</table>"
|
||||
);
|
||||
}
|
||||
|
||||
function afficherPeriodes()
|
||||
{
|
||||
document.getElementById("listePonte").innerHTML = htmlTableauPeriodes(periodesPonte, "ponte");
|
||||
document.getElementById("listeCouvage").innerHTML = htmlTableauPeriodes(periodesCouvage, "couvage");
|
||||
|
||||
document.querySelectorAll("[data-cloturer]").forEach(function (bouton)
|
||||
{
|
||||
bouton.addEventListener("click", async function ()
|
||||
{
|
||||
const type = bouton.getAttribute("data-cloturer");
|
||||
const id = bouton.getAttribute("data-id");
|
||||
const aujourdHui = new Date().toISOString().slice(0, 10);
|
||||
const url = type === "ponte"
|
||||
? "/api/periodes-ponte/" + id
|
||||
: "/api/periodes-couvage/" + id;
|
||||
|
||||
await appelerApi(url, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ dateFin: aujourdHui })
|
||||
});
|
||||
|
||||
await chargerFiche();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ouvrirModification()
|
||||
{
|
||||
document.getElementById("animalId").value = animalCourant.id;
|
||||
document.getElementById("type").value = animalCourant.type;
|
||||
document.getElementById("couleurBague").value = animalCourant.couleurBague || "";
|
||||
document.getElementById("numeroBague").value = animalCourant.numeroBague || "";
|
||||
document.getElementById("dateNaissance").value = animalCourant.dateNaissance || "";
|
||||
document.getElementById("etatSante").value = animalCourant.etatSante || "bon";
|
||||
document.getElementById("photo").value = "";
|
||||
document.getElementById("erreurAnimal").hidden = true;
|
||||
document.getElementById("dialogueAnimal").showModal();
|
||||
}
|
||||
|
||||
function ouvrirDeces()
|
||||
{
|
||||
document.getElementById("dateFinDeVie").value = new Date().toISOString().slice(0, 10);
|
||||
document.getElementById("dialogueDeces").showModal();
|
||||
}
|
||||
|
||||
function ouvrirPeriode(type)
|
||||
{
|
||||
document.getElementById("typePeriode").value = type;
|
||||
document.getElementById("titrePeriode").textContent =
|
||||
type === "ponte" ? "Nouvelle période de ponte" : "Nouvelle période de couvage";
|
||||
document.getElementById("formPeriode").reset();
|
||||
document.getElementById("typePeriode").value = type;
|
||||
document.getElementById("dateDebut").value = new Date().toISOString().slice(0, 10);
|
||||
document.getElementById("erreurPeriode").hidden = true;
|
||||
document.getElementById("dialoguePeriode").showModal();
|
||||
}
|
||||
|
||||
async function chargerFiche()
|
||||
{
|
||||
const id = recupererIdDepuisUrl();
|
||||
|
||||
if (!id)
|
||||
{
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
const donnees = await appelerApi("/api/animaux/" + id);
|
||||
|
||||
if (!donnees)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
animalCourant = donnees.animal;
|
||||
periodesPonte = donnees.periodesPonte;
|
||||
periodesCouvage = donnees.periodesCouvage;
|
||||
afficherFiche();
|
||||
}
|
||||
|
||||
document.getElementById("annulerAnimal").addEventListener("click", function ()
|
||||
{
|
||||
document.getElementById("dialogueAnimal").close();
|
||||
});
|
||||
|
||||
document.getElementById("annulerDeces").addEventListener("click", function ()
|
||||
{
|
||||
document.getElementById("dialogueDeces").close();
|
||||
});
|
||||
|
||||
document.getElementById("annulerPeriode").addEventListener("click", function ()
|
||||
{
|
||||
document.getElementById("dialoguePeriode").close();
|
||||
});
|
||||
|
||||
document.getElementById("boutonAjouterPonte").addEventListener("click", function ()
|
||||
{
|
||||
ouvrirPeriode("ponte");
|
||||
});
|
||||
|
||||
document.getElementById("boutonAjouterCouvage").addEventListener("click", function ()
|
||||
{
|
||||
ouvrirPeriode("couvage");
|
||||
});
|
||||
|
||||
document.getElementById("formAnimal").addEventListener("submit", async function (event)
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
const erreurAnimal = document.getElementById("erreurAnimal");
|
||||
erreurAnimal.hidden = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("type", document.getElementById("type").value);
|
||||
formData.append("couleurBague", document.getElementById("couleurBague").value);
|
||||
formData.append("numeroBague", document.getElementById("numeroBague").value);
|
||||
formData.append("dateNaissance", document.getElementById("dateNaissance").value);
|
||||
formData.append("etatSante", document.getElementById("etatSante").value);
|
||||
|
||||
const photo = document.getElementById("photo").files[0];
|
||||
if (photo)
|
||||
{
|
||||
formData.append("photo", photo);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await appelerApi("/api/animaux/" + animalCourant.id, {
|
||||
method: "PUT",
|
||||
body: formData
|
||||
});
|
||||
|
||||
document.getElementById("dialogueAnimal").close();
|
||||
await chargerFiche();
|
||||
}
|
||||
catch (erreur)
|
||||
{
|
||||
erreurAnimal.textContent = erreur.message;
|
||||
erreurAnimal.hidden = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("formDeces").addEventListener("submit", async function (event)
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
await appelerApi("/api/animaux/" + animalCourant.id + "/deces", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
dateFinDeVie: document.getElementById("dateFinDeVie").value
|
||||
})
|
||||
});
|
||||
|
||||
document.getElementById("dialogueDeces").close();
|
||||
window.location.href = "/archives.html";
|
||||
});
|
||||
|
||||
document.getElementById("formPeriode").addEventListener("submit", async function (event)
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
const erreurPeriode = document.getElementById("erreurPeriode");
|
||||
erreurPeriode.hidden = true;
|
||||
|
||||
const type = document.getElementById("typePeriode").value;
|
||||
const url = type === "ponte" ? "/api/periodes-ponte" : "/api/periodes-couvage";
|
||||
const dateFin = document.getElementById("dateFin").value || null;
|
||||
|
||||
try
|
||||
{
|
||||
await appelerApi(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
idAnimal: animalCourant.id,
|
||||
dateDebut: document.getElementById("dateDebut").value,
|
||||
dateFin: dateFin,
|
||||
remarque: document.getElementById("remarque").value
|
||||
})
|
||||
});
|
||||
|
||||
document.getElementById("dialoguePeriode").close();
|
||||
await chargerFiche();
|
||||
}
|
||||
catch (erreur)
|
||||
{
|
||||
erreurPeriode.textContent = erreur.message;
|
||||
erreurPeriode.hidden = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function demarrer()
|
||||
{
|
||||
const utilisateur = await verifierSession();
|
||||
|
||||
if (!utilisateur)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
brancherDeconnexion();
|
||||
await chargerFiche();
|
||||
}
|
||||
|
||||
demarrer();
|
||||
@@ -0,0 +1,583 @@
|
||||
const path = require("path");
|
||||
const express = require("express");
|
||||
const session = require("express-session");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const multer = require("multer");
|
||||
const { lireJson, ecrireJson, genererId } = require("./lib/donnees");
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
/* ---------- Configuration ---------- */
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
|
||||
|
||||
app.use(session({
|
||||
secret: "mon-poulailler-secret-local",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { maxAge: 24 * 60 * 60 * 1000 }
|
||||
}));
|
||||
|
||||
/* Stockage des photos uploadées */
|
||||
const stockage = multer.diskStorage({
|
||||
destination: function (req, file, callback)
|
||||
{
|
||||
callback(null, path.join(__dirname, "uploads"));
|
||||
},
|
||||
filename: function (req, file, callback)
|
||||
{
|
||||
const extension = path.extname(file.originalname);
|
||||
callback(null, genererId() + extension);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: stockage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
fileFilter: function (req, file, callback)
|
||||
{
|
||||
if (file.mimetype.startsWith("image/"))
|
||||
{
|
||||
callback(null, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(new Error("Seules les images sont acceptées"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* ---------- Middlewares d'accès ---------- */
|
||||
|
||||
function exigerConnexion(req, res, next)
|
||||
{
|
||||
if (!req.session.utilisateur)
|
||||
{
|
||||
return res.status(401).json({ erreur: "Vous devez être connecté" });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function exigerAdmin(req, res, next)
|
||||
{
|
||||
if (!req.session.utilisateur || req.session.utilisateur.profil !== "admin")
|
||||
{
|
||||
return res.status(403).json({ erreur: "Réservé à l'administrateur" });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/* ---------- Authentification ---------- */
|
||||
|
||||
app.get("/api/session", function (req, res)
|
||||
{
|
||||
if (req.session.utilisateur)
|
||||
{
|
||||
return res.json({ connecte: true, utilisateur: req.session.utilisateur });
|
||||
}
|
||||
|
||||
res.json({ connecte: false });
|
||||
});
|
||||
|
||||
app.post("/api/connexion", function (req, res)
|
||||
{
|
||||
const login = req.body.login;
|
||||
const motDePasse = req.body.motDePasse;
|
||||
const utilisateurs = lireJson("utilisateurs.json", []);
|
||||
|
||||
const utilisateur = utilisateurs.find(function (u)
|
||||
{
|
||||
return u.login === login;
|
||||
});
|
||||
|
||||
if (!utilisateur)
|
||||
{
|
||||
return res.status(401).json({ erreur: "Identifiant ou mot de passe incorrect" });
|
||||
}
|
||||
|
||||
const motDePasseOk = bcrypt.compareSync(motDePasse, utilisateur.motDePasseHash);
|
||||
|
||||
if (!motDePasseOk)
|
||||
{
|
||||
return res.status(401).json({ erreur: "Identifiant ou mot de passe incorrect" });
|
||||
}
|
||||
|
||||
req.session.utilisateur = {
|
||||
id: utilisateur.id,
|
||||
login: utilisateur.login,
|
||||
profil: utilisateur.profil
|
||||
};
|
||||
|
||||
res.json({ message: "Connexion réussie", utilisateur: req.session.utilisateur });
|
||||
});
|
||||
|
||||
app.post("/api/deconnexion", function (req, res)
|
||||
{
|
||||
req.session.destroy(function ()
|
||||
{
|
||||
res.json({ message: "Déconnexion réussie" });
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/utilisateurs", exigerConnexion, exigerAdmin, function (req, res)
|
||||
{
|
||||
const login = req.body.login;
|
||||
const motDePasse = req.body.motDePasse;
|
||||
const profil = req.body.profil || "utilisateur";
|
||||
|
||||
if (!login || !motDePasse)
|
||||
{
|
||||
return res.status(400).json({ erreur: "Login et mot de passe obligatoires" });
|
||||
}
|
||||
|
||||
if (profil !== "admin" && profil !== "utilisateur")
|
||||
{
|
||||
return res.status(400).json({ erreur: "Profil invalide" });
|
||||
}
|
||||
|
||||
const utilisateurs = lireJson("utilisateurs.json", []);
|
||||
const existeDeja = utilisateurs.some(function (u)
|
||||
{
|
||||
return u.login === login;
|
||||
});
|
||||
|
||||
if (existeDeja)
|
||||
{
|
||||
return res.status(400).json({ erreur: "Ce login existe déjà" });
|
||||
}
|
||||
|
||||
const nouvelUtilisateur = {
|
||||
id: genererId(),
|
||||
login: login,
|
||||
motDePasseHash: bcrypt.hashSync(motDePasse, 10),
|
||||
profil: profil
|
||||
};
|
||||
|
||||
utilisateurs.push(nouvelUtilisateur);
|
||||
ecrireJson("utilisateurs.json", utilisateurs);
|
||||
|
||||
res.status(201).json({
|
||||
message: "Compte créé",
|
||||
utilisateur: {
|
||||
id: nouvelUtilisateur.id,
|
||||
login: nouvelUtilisateur.login,
|
||||
profil: nouvelUtilisateur.profil
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/utilisateurs", exigerConnexion, exigerAdmin, function (req, res)
|
||||
{
|
||||
const utilisateurs = lireJson("utilisateurs.json", []);
|
||||
const liste = utilisateurs.map(function (u)
|
||||
{
|
||||
return { id: u.id, login: u.login, profil: u.profil };
|
||||
});
|
||||
|
||||
res.json(liste);
|
||||
});
|
||||
|
||||
/* ---------- Animaux ---------- */
|
||||
|
||||
app.get("/api/animaux", exigerConnexion, function (req, res)
|
||||
{
|
||||
const actifsSeulement = req.query.actifs === "oui";
|
||||
const archivesSeulement = req.query.archives === "oui";
|
||||
let animaux = lireJson("animaux.json", []);
|
||||
|
||||
if (actifsSeulement)
|
||||
{
|
||||
animaux = animaux.filter(function (a)
|
||||
{
|
||||
return a.actif === true;
|
||||
});
|
||||
}
|
||||
|
||||
if (archivesSeulement)
|
||||
{
|
||||
animaux = animaux.filter(function (a)
|
||||
{
|
||||
return a.actif === false;
|
||||
});
|
||||
}
|
||||
|
||||
res.json(animaux);
|
||||
});
|
||||
|
||||
app.get("/api/animaux/:id", exigerConnexion, function (req, res)
|
||||
{
|
||||
const animaux = lireJson("animaux.json", []);
|
||||
const animal = animaux.find(function (a)
|
||||
{
|
||||
return a.id === req.params.id;
|
||||
});
|
||||
|
||||
if (!animal)
|
||||
{
|
||||
return res.status(404).json({ erreur: "Animal introuvable" });
|
||||
}
|
||||
|
||||
const periodesPonte = lireJson("periodesPonte.json", []).filter(function (p)
|
||||
{
|
||||
return p.idAnimal === animal.id;
|
||||
});
|
||||
|
||||
const periodesCouvage = lireJson("periodesCouvage.json", []).filter(function (p)
|
||||
{
|
||||
return p.idAnimal === animal.id;
|
||||
});
|
||||
|
||||
res.json({
|
||||
animal: animal,
|
||||
periodesPonte: periodesPonte,
|
||||
periodesCouvage: periodesCouvage
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/animaux", exigerConnexion, upload.single("photo"), function (req, res)
|
||||
{
|
||||
const type = req.body.type;
|
||||
const couleurBague = req.body.couleurBague || "";
|
||||
const numeroBague = req.body.numeroBague || "";
|
||||
const dateNaissance = req.body.dateNaissance || "";
|
||||
const etatSante = req.body.etatSante || "bon";
|
||||
|
||||
if (type !== "poule" && type !== "coq")
|
||||
{
|
||||
return res.status(400).json({ erreur: "Le type doit être poule ou coq" });
|
||||
}
|
||||
|
||||
const animaux = lireJson("animaux.json", []);
|
||||
|
||||
const nouvelAnimal = {
|
||||
id: genererId(),
|
||||
type: type,
|
||||
photo: req.file ? "/uploads/" + req.file.filename : "",
|
||||
couleurBague: couleurBague,
|
||||
numeroBague: numeroBague,
|
||||
dateNaissance: dateNaissance,
|
||||
etatSante: etatSante,
|
||||
dateFinDeVie: null,
|
||||
actif: true
|
||||
};
|
||||
|
||||
animaux.push(nouvelAnimal);
|
||||
ecrireJson("animaux.json", animaux);
|
||||
|
||||
res.status(201).json(nouvelAnimal);
|
||||
});
|
||||
|
||||
app.put("/api/animaux/:id", exigerConnexion, upload.single("photo"), function (req, res)
|
||||
{
|
||||
const animaux = lireJson("animaux.json", []);
|
||||
const index = animaux.findIndex(function (a)
|
||||
{
|
||||
return a.id === req.params.id;
|
||||
});
|
||||
|
||||
if (index === -1)
|
||||
{
|
||||
return res.status(404).json({ erreur: "Animal introuvable" });
|
||||
}
|
||||
|
||||
const animal = animaux[index];
|
||||
|
||||
if (req.body.type === "poule" || req.body.type === "coq")
|
||||
{
|
||||
animal.type = req.body.type;
|
||||
}
|
||||
|
||||
if (req.body.couleurBague !== undefined)
|
||||
{
|
||||
animal.couleurBague = req.body.couleurBague;
|
||||
}
|
||||
|
||||
if (req.body.numeroBague !== undefined)
|
||||
{
|
||||
animal.numeroBague = req.body.numeroBague;
|
||||
}
|
||||
|
||||
if (req.body.dateNaissance !== undefined)
|
||||
{
|
||||
animal.dateNaissance = req.body.dateNaissance;
|
||||
}
|
||||
|
||||
if (req.body.etatSante !== undefined)
|
||||
{
|
||||
animal.etatSante = req.body.etatSante;
|
||||
}
|
||||
|
||||
if (req.file)
|
||||
{
|
||||
animal.photo = "/uploads/" + req.file.filename;
|
||||
}
|
||||
|
||||
animaux[index] = animal;
|
||||
ecrireJson("animaux.json", animaux);
|
||||
|
||||
res.json(animal);
|
||||
});
|
||||
|
||||
app.post("/api/animaux/:id/deces", exigerConnexion, function (req, res)
|
||||
{
|
||||
const animaux = lireJson("animaux.json", []);
|
||||
const index = animaux.findIndex(function (a)
|
||||
{
|
||||
return a.id === req.params.id;
|
||||
});
|
||||
|
||||
if (index === -1)
|
||||
{
|
||||
return res.status(404).json({ erreur: "Animal introuvable" });
|
||||
}
|
||||
|
||||
const animal = animaux[index];
|
||||
animal.actif = false;
|
||||
animal.dateFinDeVie = req.body.dateFinDeVie || new Date().toISOString().slice(0, 10);
|
||||
|
||||
/* Clôturer les périodes en cours */
|
||||
const periodesPonte = lireJson("periodesPonte.json", []);
|
||||
periodesPonte.forEach(function (periode)
|
||||
{
|
||||
if (periode.idAnimal === animal.id && !periode.dateFin)
|
||||
{
|
||||
periode.dateFin = animal.dateFinDeVie;
|
||||
}
|
||||
});
|
||||
ecrireJson("periodesPonte.json", periodesPonte);
|
||||
|
||||
const periodesCouvage = lireJson("periodesCouvage.json", []);
|
||||
periodesCouvage.forEach(function (periode)
|
||||
{
|
||||
if (periode.idAnimal === animal.id && !periode.dateFin)
|
||||
{
|
||||
periode.dateFin = animal.dateFinDeVie;
|
||||
}
|
||||
});
|
||||
ecrireJson("periodesCouvage.json", periodesCouvage);
|
||||
|
||||
animaux[index] = animal;
|
||||
ecrireJson("animaux.json", animaux);
|
||||
|
||||
res.json(animal);
|
||||
});
|
||||
|
||||
/* ---------- Périodes de ponte ---------- */
|
||||
|
||||
app.post("/api/periodes-ponte", exigerConnexion, function (req, res)
|
||||
{
|
||||
const idAnimal = req.body.idAnimal;
|
||||
const dateDebut = req.body.dateDebut;
|
||||
const dateFin = req.body.dateFin || null;
|
||||
const remarque = req.body.remarque || "";
|
||||
|
||||
if (!idAnimal || !dateDebut)
|
||||
{
|
||||
return res.status(400).json({ erreur: "idAnimal et dateDebut sont obligatoires" });
|
||||
}
|
||||
|
||||
const animaux = lireJson("animaux.json", []);
|
||||
const animal = animaux.find(function (a)
|
||||
{
|
||||
return a.id === idAnimal;
|
||||
});
|
||||
|
||||
if (!animal)
|
||||
{
|
||||
return res.status(404).json({ erreur: "Animal introuvable" });
|
||||
}
|
||||
|
||||
if (animal.type !== "poule")
|
||||
{
|
||||
return res.status(400).json({ erreur: "La ponte concerne uniquement les poules" });
|
||||
}
|
||||
|
||||
const periodes = lireJson("periodesPonte.json", []);
|
||||
const nouvellePeriode = {
|
||||
id: genererId(),
|
||||
idAnimal: idAnimal,
|
||||
dateDebut: dateDebut,
|
||||
dateFin: dateFin,
|
||||
remarque: remarque
|
||||
};
|
||||
|
||||
periodes.push(nouvellePeriode);
|
||||
ecrireJson("periodesPonte.json", periodes);
|
||||
|
||||
res.status(201).json(nouvellePeriode);
|
||||
});
|
||||
|
||||
app.put("/api/periodes-ponte/:id", exigerConnexion, function (req, res)
|
||||
{
|
||||
const periodes = lireJson("periodesPonte.json", []);
|
||||
const index = periodes.findIndex(function (p)
|
||||
{
|
||||
return p.id === req.params.id;
|
||||
});
|
||||
|
||||
if (index === -1)
|
||||
{
|
||||
return res.status(404).json({ erreur: "Période introuvable" });
|
||||
}
|
||||
|
||||
if (req.body.dateDebut !== undefined)
|
||||
{
|
||||
periodes[index].dateDebut = req.body.dateDebut;
|
||||
}
|
||||
|
||||
if (req.body.dateFin !== undefined)
|
||||
{
|
||||
periodes[index].dateFin = req.body.dateFin || null;
|
||||
}
|
||||
|
||||
if (req.body.remarque !== undefined)
|
||||
{
|
||||
periodes[index].remarque = req.body.remarque;
|
||||
}
|
||||
|
||||
ecrireJson("periodesPonte.json", periodes);
|
||||
res.json(periodes[index]);
|
||||
});
|
||||
|
||||
/* ---------- Périodes de couvage ---------- */
|
||||
|
||||
app.post("/api/periodes-couvage", exigerConnexion, function (req, res)
|
||||
{
|
||||
const idAnimal = req.body.idAnimal;
|
||||
const dateDebut = req.body.dateDebut;
|
||||
const dateFin = req.body.dateFin || null;
|
||||
const remarque = req.body.remarque || "";
|
||||
|
||||
if (!idAnimal || !dateDebut)
|
||||
{
|
||||
return res.status(400).json({ erreur: "idAnimal et dateDebut sont obligatoires" });
|
||||
}
|
||||
|
||||
const animaux = lireJson("animaux.json", []);
|
||||
const animal = animaux.find(function (a)
|
||||
{
|
||||
return a.id === idAnimal;
|
||||
});
|
||||
|
||||
if (!animal)
|
||||
{
|
||||
return res.status(404).json({ erreur: "Animal introuvable" });
|
||||
}
|
||||
|
||||
if (animal.type !== "poule")
|
||||
{
|
||||
return res.status(400).json({ erreur: "Le couvage concerne uniquement les poules" });
|
||||
}
|
||||
|
||||
const periodes = lireJson("periodesCouvage.json", []);
|
||||
const nouvellePeriode = {
|
||||
id: genererId(),
|
||||
idAnimal: idAnimal,
|
||||
dateDebut: dateDebut,
|
||||
dateFin: dateFin,
|
||||
remarque: remarque
|
||||
};
|
||||
|
||||
periodes.push(nouvellePeriode);
|
||||
ecrireJson("periodesCouvage.json", periodes);
|
||||
|
||||
res.status(201).json(nouvellePeriode);
|
||||
});
|
||||
|
||||
app.put("/api/periodes-couvage/:id", exigerConnexion, function (req, res)
|
||||
{
|
||||
const periodes = lireJson("periodesCouvage.json", []);
|
||||
const index = periodes.findIndex(function (p)
|
||||
{
|
||||
return p.id === req.params.id;
|
||||
});
|
||||
|
||||
if (index === -1)
|
||||
{
|
||||
return res.status(404).json({ erreur: "Période introuvable" });
|
||||
}
|
||||
|
||||
if (req.body.dateDebut !== undefined)
|
||||
{
|
||||
periodes[index].dateDebut = req.body.dateDebut;
|
||||
}
|
||||
|
||||
if (req.body.dateFin !== undefined)
|
||||
{
|
||||
periodes[index].dateFin = req.body.dateFin || null;
|
||||
}
|
||||
|
||||
if (req.body.remarque !== undefined)
|
||||
{
|
||||
periodes[index].remarque = req.body.remarque;
|
||||
}
|
||||
|
||||
ecrireJson("periodesCouvage.json", periodes);
|
||||
res.json(periodes[index]);
|
||||
});
|
||||
|
||||
/* ---------- Statistiques ---------- */
|
||||
|
||||
app.get("/api/statistiques", exigerConnexion, function (req, res)
|
||||
{
|
||||
const animaux = lireJson("animaux.json", []);
|
||||
const actifs = animaux.filter(function (a)
|
||||
{
|
||||
return a.actif === true;
|
||||
});
|
||||
|
||||
const poulesActives = actifs.filter(function (a)
|
||||
{
|
||||
return a.type === "poule";
|
||||
});
|
||||
|
||||
const coqsActifs = actifs.filter(function (a)
|
||||
{
|
||||
return a.type === "coq";
|
||||
});
|
||||
|
||||
const periodesPonte = lireJson("periodesPonte.json", []);
|
||||
const periodesCouvage = lireJson("periodesCouvage.json", []);
|
||||
|
||||
const idsPoules = poulesActives.map(function (p)
|
||||
{
|
||||
return p.id;
|
||||
});
|
||||
|
||||
const enPonte = idsPoules.filter(function (id)
|
||||
{
|
||||
return periodesPonte.some(function (periode)
|
||||
{
|
||||
return periode.idAnimal === id && !periode.dateFin;
|
||||
});
|
||||
});
|
||||
|
||||
const enCouvage = idsPoules.filter(function (id)
|
||||
{
|
||||
return periodesCouvage.some(function (periode)
|
||||
{
|
||||
return periode.idAnimal === id && !periode.dateFin;
|
||||
});
|
||||
});
|
||||
|
||||
res.json({
|
||||
nombrePoulesActives: poulesActives.length,
|
||||
nombreCoqsActifs: coqsActifs.length,
|
||||
nombreTotalActifs: actifs.length,
|
||||
nombreEnCouvage: enCouvage.length,
|
||||
nombreEnPonte: enPonte.length
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- Démarrage ---------- */
|
||||
|
||||
app.listen(port, function ()
|
||||
{
|
||||
console.log("Mon Poulailler écoute sur http://localhost:" + port);
|
||||
});
|
||||
Whitespace-only changes.