10 Commits

Author SHA1 Message Date
yann64 79b831367c Release 1.0.2 — changelog + bump version dev 1.0.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 22:04:55 +02:00
yann64 715aad58e1 Fix wizard : config:clear avant migrations pour éviter conflit pgsql/mysql
Sans ce clear, un bootstrap/cache/config.php résiduel (produit par
php artisan optimize en dev) est lu en priorité sur le .env réécrit
par writeEnv(), forçant une connexion pgsql même quand mysql est choisi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 22:02:45 +02:00
yann64 c3e1d4378c Release 1.0.1 — changelog + bump version dev 1.0.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:44:35 +02:00
yann64 5acc6ff5c8 Fix wizard : auto-création du .env minimal quand absent au démarrage
Sans .env, Laravel lève MissingAppKeyException avant que le moindre
middleware ou contrôleur ne s'exécute, rendant /setup inaccessible.

public/index.php génère maintenant un .env depuis .env.example avec
une APP_KEY temporaire (random_bytes 32) et force SESSION_DRIVER=file,
CACHE_STORE=file, QUEUE_CONNECTION=sync pour un boot sans base de données.
Le wizard remplace ce fichier lors de l'installation définitive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:41:51 +02:00
yann64 2e6ac29e95 Ajout de public/servercheck.php (outil de diagnostic serveur)
Fichier autonome (sans dépendance Laravel) vérifiant :
- Version PHP (≥ 8.2)
- Extensions requises et optionnelles
- Directives php.ini (memory_limit, upload_max_filesize…)
- Répertoires accessibles en écriture (storage/, bootstrap/cache/)
- Présence des fichiers clés (.env, vendor, assets compilés)
- Test de connexion BDD (MySQL / PostgreSQL) via formulaire
Interface HTML auto-adaptative (dark mode OS), sans dépendance externe.

À exclure des archives de production (--exclude public/servercheck.php
dans le rsync de build-release).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:35:55 +02:00
yann64 a1860e9462 Page carte interactive des relevés (Leaflet + OpenStreetMap)
- CarteController : index() + data() (JSON) — requête lieux géolocalisés
  ayant des sources avec relevés, agrégats par lieu
- Lieu model : relations sources() et releves() (hasManyThrough)
- Vue carte/index.blade.php : carte Leaflet pleine hauteur, marqueurs
  colorés par nombre de sources (taille/couleur proportionnels),
  popup par lieu avec liste des sources, statuts, années et lien recherche
- Tuiles OpenStreetMap inversées en mode sombre
- Route GET /carte + GET /carte/data
- Lien "Carte" dans la navigation desktop et mobile
- @stack('head') dans le layout pour injecter Leaflet uniquement sur la page carte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:02:28 +02:00
yann64 6f55663984 Fallback routing sans mod_rewrite via FallbackResource
Utilise mod_rewrite si disponible, sinon FallbackResource (Apache 2.2.16+,
aucun module supplémentaire requis) pour les hébergements mutualisés
qui n'ont pas mod_rewrite activé.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 20:30:36 +02:00
yann64 8efb7e30df README : correction de la procédure d'installation Docker
- Stack réelle : PHP-FPM + Nginx + PostgreSQL (pas de Redis en prod)
- Détail des 4 étapes : extraction, premier lancement, config .env, install finale
- Paramètres .env minimaux documentés (APP_KEY auto-généré, SMTP via UI admin)
- Commandes courantes (logs, shell, restart)
- Section mises à jour complète avec rollback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 20:16:23 +02:00
yann64 3faa74640d Fix dark mode : formulaires et composant lieu-picker
- app.css : règle @layer base globale pour tous les <input>, <select>,
  <textarea> en mode sombre (bg-gray-700, border-gray-600, text-gray-100)
  sans toucher checkboxes, radios ni boutons
- lieu-picker : bouton déclencheur, fenêtre modale, champ de recherche,
  liste de résultats et badges entièrement adaptés au thème sombre

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 20:14:44 +02:00
yann64 f530f55577 Mode sombre, option désactivation mises à jour, user-picker avec recherche
- Dark mode complet : darkMode:'class' Tailwind, sélecteur clair/sombre/auto
  dans la navigation (mémorisé dans localStorage, sans flash au chargement) ;
  53 vues et 8 composants Breeze mis à jour avec classes dark:
- Composant user-picker : fenêtre modale avec recherche temps réel (nom/email)
  remplace les <select> d'ajout de membres dans sections et sources
- Paramètres : option "Désactiver la vérification automatique des mises à jour"
  (case à cochage auto-soumise, route POST parametres/updates)
- Panneau "Paramètres généraux" remonté en tête de la page de paramètres
- README recentré sur l'installation manuelle hébergement PHP+MySQL
- VERSION 1.0.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:46:22 +02:00
83 changed files with 2163 additions and 925 deletions
+70
View File
@@ -0,0 +1,70 @@
# Changelog
Toutes les modifications notables de MesRelevés sont documentées dans ce fichier.
Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnage [Semver](https://semver.org/lang/fr/).
---
## [1.0.2] — 2026-06-04
### Corrigé
- **Assistant d'installation — conflit de connexion pgsql/mysql** : un fichier `bootstrap/cache/config.php` résiduel (produit par `php artisan optimize` sur le poste de développement) était lu en priorité sur le `.env` réécrit par le wizard, forçant une connexion PostgreSQL même quand MySQL était sélectionné. Un appel à `config:clear` est maintenant effectué entre l'écriture du `.env` et l'exécution des migrations.
- **Archives de déploiement** : `bootstrap/cache/*.php` exclu du build rsync pour éviter qu'un cache de configuration de développement ne soit embarqué dans les distributables.
---
## [1.0.1] — 2026-06-04
### Ajouté
- **Page Carte** — carte interactive (Leaflet + OpenStreetMap) affichant les lieux géolocalisés ayant des relevés ; marqueurs proportionnels au nombre de sources, popups avec détail des sources et lien vers la recherche ; compatible mode sombre
- **Mode sombre** — détection automatique de la préférence système ; sélecteur clair / sombre / automatique dans la barre de navigation (mémorisé dans `localStorage`, sans flash au chargement) ; couverture complète des vues et composants
- **2FA par e-mail** — code PIN à 6 chiffres envoyé à la connexion dès qu'un serveur SMTP est configuré ; code valable 10 minutes, renvoi possible
- **Configuration SMTP depuis l'interface** — formulaire dans Administration → Paramètres du site avec test de connexion en temps réel ; activation du 2FA automatique à l'enregistrement
- **Import / export CSV utilisateurs** — import en masse avec détection automatique du séparateur (`;`/`,`), validation par ligne, génération de mot de passe aléatoire ; export filtré au format CSV UTF-8 BOM (compatible Excel)
- **Filtre actif / inactif** sur la liste des utilisateurs
- **Titre du site configurable** depuis les paramètres (sans éditer `.env`)
- **Version affichée** dans Administration → Paramètres du site avec indicateur de mise à jour disponible
- **Option de désactivation des mises à jour automatiques** dans les paramètres
- **Sélecteur d'utilisateur avec recherche** — fenêtre modale remplaçant les `<select>` pour l'ajout de membres aux sections et sources (adapté à plusieurs centaines d'utilisateurs)
- **`public/servercheck.php`** — outil de diagnostic serveur autonome (PHP, extensions, répertoires, test BDD) pour les environnements de test
### Modifié
- **Assistant d'installation** — auto-création du `.env` minimal (clé temporaire, drivers fichier) à la première requête si aucun `.env` n'existe, évitant l'erreur 500 sur un serveur vierge
- **`.htaccess`** — ajout de `FallbackResource /index.php` en fallback pour les hébergements sans `mod_rewrite`
- **Logo dans la navigation** — contraint par `max-height` inline pour s'adapter à la hauteur de la barre sans débordement
- **Panneau "Paramètres généraux"** remonté en première position dans la page de paramètres
- **README** — procédure d'installation mutualisée (PHP + MySQL) mise en avant ; procédure Docker corrigée (stack réelle : PHP-FPM + Nginx + PostgreSQL, sans Redis)
### Corrigé
- Champs de formulaire illisibles en mode sombre (texte clair sur fond clair) — règle CSS globale `@layer base` couvrant tous les `<input>`, `<select>`, `<textarea>`
- Composant `lieu-picker` entièrement sans style en mode sombre
- Erreur 500 à l'ouverture de `/setup` sur un serveur sans `.env` (`MissingAppKeyException`)
- Menu Administration débordant hors de la zone visible en haut de l'écran
---
## [1.0.0] — 2026-05-29
### Ajouté
- Scaffold Laravel 12 + PostgreSQL + authentification Breeze
- Modèle de données complet : lieux (hiérarchie récursive), sections, dépôts, types de sources, sources, relevés (JSONB), utilisateurs et rôles
- CRUD Lieux avec arbre hiérarchique et calcul automatique de `nom_long`
- CRUD Sections, Dépôts, Types de sources (admin)
- CRUD Sources avec assignation de membres et workflow de statut (`à_faire → en_cours → à_valider → terminé`)
- Formulaire de saisie dynamique des relevés piloté par `source_type_fields`
- Calendriers grégorien, julien et républicain avec conversion automatique
- Recherche plein texte sur les relevés avec filtres (type, lieu, plage d'années)
- Export GEDCOM 5.5.1 par source ou par sélection de recherche
- Notifications in-app et par e-mail lors des transitions de statut
- Interface d'administration : tableau de bord, gestion utilisateurs (actifs/inactifs), sections, types de lieux
- Comptes actifs / inactifs avec statistiques de section dans le tableau de bord
- Logo du site, favicon et contrôle des inscriptions publiques
- Assistant d'installation web en 5 étapes (`/setup`)
- Système de versioning et de mise à jour automatique (`app:update`, `app:rollback`)
- Compatibilité MySQL / MariaDB en plus de PostgreSQL
- Déploiement Docker (PHP-FPM + Nginx + PostgreSQL) via `docker-compose.prod.yml`
+213 -96
View File
@@ -3,32 +3,167 @@
Application web de saisie et de recherche de relevés généalogiques pour associations. Application web de saisie et de recherche de relevés généalogiques pour associations.
Permet la saisie collaborative de relevés d'actes (naissance, mariage, décès, etc.), la recherche plein texte et l'export au format GEDCOM 5.5.1. Permet la saisie collaborative de relevés d'actes (naissance, mariage, décès, etc.), la recherche plein texte et l'export au format GEDCOM 5.5.1.
---
## Installation sur hébergement mutualisé (recommandé)
### Prérequis serveur
| Composant | Version minimale |
|---|---|
| PHP | 8.2 avec extensions : `pdo_mysql`, `mbstring`, `openssl`, `tokenizer`, `xml`, `ctype`, `fileinfo`, `bcmath`, `curl` |
| MySQL / MariaDB | MySQL 8.0+ ou MariaDB 10.5+ |
| Serveur web | Apache (mod_rewrite activé) ou Nginx |
| Accès fichier | Écriture sur `storage/` et `bootstrap/cache/` |
> **PostgreSQL** est également supporté et recommandé pour les grandes bases de relevés (full-text search natif, JSONB). Voir la section [Docker](#docker) pour une installation avec PostgreSQL.
---
### Étape 1 — Télécharger et décompresser l'archive
Récupérez la dernière archive depuis la page des [releases](https://git.barbel.synology.me/CGL/mesreleves-php/releases) :
```
mesreleves-X.Y.Z.tar.gz
```
Décompressez-la dans le dossier de votre hébergement (via FTP ou le gestionnaire de fichiers du panneau de contrôle) :
```bash
tar -xzf mesreleves-X.Y.Z.tar.gz
```
Cela crée un dossier `mesreleves/` contenant toute l'application.
---
### Étape 2 — Faire pointer le site vers `public/`
Le point d'entrée de l'application est le dossier `public/`.
**Le dossier racine de votre site (document root) doit pointer sur `public/`, pas sur la racine de l'archive.**
**Exemples de configurations :**
<details>
<summary>Apache — <code>.htaccess</code> (déjà inclus dans <code>public/</code>)</summary>
Rien à faire : le `.htaccess` livré avec l'application configure automatiquement la réécriture d'URL. Vérifiez que `mod_rewrite` est activé sur votre hébergement.
Si vous ne pouvez pas modifier le document root, vous pouvez déposer les fichiers directement dans le dossier `public_html/` de votre hébergement en copiant le contenu de `public/` à sa racine et en adaptant le chemin dans `index.php` :
```php
// public/index.php — adapter le chemin si l'arborescence est modifiée
require __DIR__.'/../mesreleves/vendor/autoload.php';
$app = require_once __DIR__.'/../mesreleves/bootstrap/app.php';
```
</details>
<details>
<summary>Nginx</summary>
```nginx
server {
listen 80;
server_name votre-domaine.fr;
root /var/www/mesreleves/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
```
</details>
---
### Étape 3 — Lancer l'assistant d'installation
Ouvrez votre navigateur sur `https://votre-domaine.fr/setup`.
L'assistant vous guide à travers **5 étapes** :
1. **Prérequis** — vérification automatique des extensions PHP et des droits d'écriture
2. **Base de données** — saisie des paramètres MySQL/PostgreSQL ; un bouton « Tester la connexion » valide la configuration avant de continuer
3. **Application** — nom du site, URL de base, activation des inscriptions publiques
4. **Compte administrateur** — création du premier compte admin
5. **Installation** — migrations, génération de la clé d'application, résumé
> L'assistant crée automatiquement le fichier `.env` et exécute toutes les migrations.
> Une fois terminé, la page `/setup` est désactivée.
---
### Étape 4 — Configurer les tâches planifiées (optionnel)
Pour activer les notifications par e-mail et la vérification automatique des mises à jour, ajoutez cette ligne à votre `crontab` (via le panneau de contrôle de l'hébergement → **Tâches planifiées / Cron**) :
```
* * * * * php /chemin/absolu/vers/mesreleves/artisan schedule:run >> /dev/null 2>&1
```
Remplacez `/chemin/absolu/vers/mesreleves/` par le chemin réel sur votre serveur (visible dans le gestionnaire de fichiers, généralement `/home/user/www/mesreleves/`).
---
### Mise à jour
Dans **Administration → Paramètres du site → Version du logiciel**, le tableau de bord signale automatiquement les nouvelles versions disponibles (si la vérification n'est pas désactivée et que les tâches planifiées sont actives).
**Pour appliquer une mise à jour manuellement :**
1. Télécharger la nouvelle archive `mesreleves-X.Y.Z.tar.gz`
2. Décompresser et écraser les fichiers existants (**sauf** `.env` et `storage/`)
3. Exécuter les migrations depuis un terminal SSH :
```bash
php artisan migrate --force
php artisan optimize:clear
```
---
## Fonctionnalités ## Fonctionnalités
- **Saisie collaborative** — formulaires dynamiques pilotés par des types de sources configurables ; champs libres stockés en JSONB PostgreSQL - **Saisie collaborative** — formulaires dynamiques pilotés par des types de sources configurables
- **Calendriers** — saisie en calendrier grégorien, julien ou républicain avec conversion automatique - **Calendriers** — saisie en calendrier grégorien, julien ou républicain avec conversion automatique
- **Recherche plein texte** recherche PostgreSQL native sur les relevés, filtrable par type de source, lieu (avec subdivisions récursives) et plage d'années - **Recherche plein texte** — filtrable par type de source, lieu (avec subdivisions récursives) et plage d'années
- **Gestion des lieux** — arbre hiérarchique (pays → région → département → commune…), types de lieux configurables, recherche par picker contextuel - **Gestion des lieux** — arbre hiérarchique (pays → région → département → commune…), types configurables
- **Workflow de validation** — statuts `à faire → en cours → à valider → terminé`, notifications mail + in-app - **Workflow de validation** — statuts `à faire → en cours → à valider → terminé`, notifications mail + in-app
- **Export GEDCOM 5.5.1** — par source ou par sélection de recherche, conversion des dates vers le grégorien - **Export GEDCOM 5.5.1** — par source ou par sélection de recherche, conversion des dates vers le grégorien
- **Gestion des rôles** — administrateur, responsable de section, membre ; autorisations granulaires par source - **Gestion des rôles** — administrateur, responsable de section, membre ; autorisations granulaires par source
- **Interface admin** — tableau de bord statistiques, gestion des utilisateurs, sections, dépôts, types de sources et de lieux - **Interface admin** — tableau de bord, gestion des utilisateurs, sections, dépôts d'archives, types de sources
- **Mises à jour automatiques** — vérification quotidienne via l'API Gitea, application sans rebuild Docker - **Import / export CSV** — gestion en masse des utilisateurs
- **Mode sombre** — détection automatique, commutable manuellement (clair / sombre / automatique)
- **2FA par e-mail** — code PIN à 6 chiffres à la connexion, activé dès qu'un serveur SMTP est configuré
---
## Stack technique ## Stack technique
| Composant | Technologie | | Composant | Technologie |
|---|---| |---|---|
| Backend | PHP 8.5 · Laravel 12 | | Backend | PHP 8.2+ · Laravel 12 |
| Base de données | PostgreSQL 18 (JSONB, full-text search, CTE récursives) | | Base de données | MySQL 8.0+ / MariaDB 10.5+ · ou PostgreSQL 16+ |
| Cache / Sessions | Redis 7 |
| Frontend | Blade · Alpine.js · Tailwind CSS | | Frontend | Blade · Alpine.js · Tailwind CSS |
| Auth | Laravel Breeze (sessions) | | Auth | Laravel Breeze (sessions) |
| Conteneurs | Docker + Docker Compose |
--- ---
## Installation ## Docker
Installation avec Docker Compose : **PHP-FPM + Nginx + PostgreSQL** (pas de Redis — cache, sessions et queue utilisent la base de données).
> La stack Docker utilise exclusivement **PostgreSQL**. Pour MySQL, utilisez l'installation manuelle sur hébergement mutualisé décrite ci-dessus.
### Prérequis ### Prérequis
@@ -37,100 +172,91 @@ Permet la saisie collaborative de relevés d'actes (naissance, mariage, décès,
### Première installation ### Première installation
**1. Télécharger la dernière release** **1. Extraire l'archive**
Récupérez l'archive `mesreleves-X.Y.Z.tar.gz` depuis la page des [releases](https://git.barbel.synology.me/CGL/mesreleves-php/releases) et extrayez-la :
```bash ```bash
tar -xzf mesreleves-X.Y.Z.tar.gz tar -xzf mesreleves-X.Y.Z.tar.gz
cd mesreleves-X.Y.Z cd mesreleves
``` ```
**2. Lancer le script d'installation** **2. Premier lancement — création du `.env`**
```bash ```bash
./install.sh ./install.sh
``` ```
Le script crée un `.env` depuis `.env.example` puis s'arrête pour vous laisser le configurer. Le script détecte l'absence de `.env`, le crée depuis `.env.example` et s'arrête pour vous laisser le configurer.
Éditez `.env` avec vos paramètres :
**3. Configurer `.env`**
Ouvrez `.env` et renseignez au minimum :
```env ```env
APP_URL=https://votre-domaine.fr APP_URL=https://votre-domaine.fr
APP_KEY= # généré automatiquement au démarrage
DB_PASSWORD=mot_de_passe_fort DB_PASSWORD=mot_de_passe_fort # mot de passe PostgreSQL
DB_DATABASE=mesreleves # nom de la base (créée automatiquement)
MAIL_MAILER=smtp DB_USERNAME=mesreleves # utilisateur PostgreSQL
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_FROM_ADDRESS=mesreleves@example.com
``` ```
**3. Relancer le script** > **`APP_KEY`** est généré automatiquement au premier démarrage — laissez-le vide.
> **SMTP** peut être configuré après installation via **Administration → Paramètres du site** (le 2FA par e-mail s'active dès qu'un serveur SMTP est enregistré).
**4. Installation complète**
```bash ```bash
./install.sh ./install.sh
``` ```
L'application est accessible sur le port 80 (ou la valeur de `APP_URL`). Le script :
1. Construit l'image Docker (php-fpm 8.5, extensions PostgreSQL)
2. Démarre les services `app`, `nginx` et `db`
3. Attend que PostgreSQL soit prêt
4. Exécute les migrations
5. Crée le lien symbolique `storage/`
6. Affiche la commande crontab à copier
**4. Activer les tâches planifiées** (optionnel — nécessaire pour la vérification automatique des mises à jour) L'application est accessible sur **http://votre-domaine.fr** (port 80).
Ajoutez à votre crontab (`crontab -e`) la ligne affichée à la fin du script d'installation : ### Commandes courantes
```bash
# Voir les logs en temps réel
docker compose -f docker-compose.prod.yml logs -f app
# Ouvrir un shell dans le container
docker compose -f docker-compose.prod.yml exec app sh
# Relancer après modification du .env
docker compose -f docker-compose.prod.yml restart app
```
### Tâches planifiées
Ajoutez à votre crontab (`crontab -e`) la ligne affichée à la fin de `./install.sh` :
``` ```
* * * * * cd /chemin/vers/mesreleves && docker compose -f docker-compose.prod.yml exec -T app php artisan schedule:run >> /dev/null 2>&1 * * * * * cd /chemin/vers/mesreleves && docker compose -f docker-compose.prod.yml exec -T app php artisan schedule:run >> /dev/null 2>&1
``` ```
--- ### Mises à jour
## Mises à jour Vérifier la version disponible :
### Vérifier la version installée et les mises à jour disponibles
```bash ```bash
docker compose -f docker-compose.prod.yml exec app php artisan app:check-update docker compose -f docker-compose.prod.yml exec app php artisan app:check-update
``` ```
### Appliquer une mise à jour Appliquer une mise à jour (sauvegarde BDD → téléchargement → migrations) :
```bash ```bash
docker compose -f docker-compose.prod.yml exec app php artisan app:update docker compose -f docker-compose.prod.yml exec app php artisan app:update
``` ```
Le processus : Lister les sauvegardes et restaurer en cas de problème :
1. Sauvegarde PostgreSQL dans `storage/app/backups/`
2. Téléchargement de la nouvelle archive
3. Synchronisation des fichiers (`.env` et `storage/` préservés)
4. `composer install --no-dev`
5. Migrations
6. Rechargement gracieux de php-fpm
7. L'application est disponible pendant toute l'opération (sauf la fenêtre de migration)
### Mise à jour automatique
Pour activer la mise à jour automatique lors de la vérification planifiée, ajoutez dans `.env` :
```env
AUTO_UPDATE=true
```
> Par défaut `AUTO_UPDATE=false` : la vérification quotidienne notifie uniquement dans le tableau de bord admin.
### Rollback
En cas de problème, lister les sauvegardes disponibles :
```bash ```bash
docker compose -f docker-compose.prod.yml exec app php artisan app:rollback --list docker compose -f docker-compose.prod.yml exec app php artisan app:rollback --list
```
Restaurer la base de données :
```bash
docker compose -f docker-compose.prod.yml exec app php artisan app:rollback docker compose -f docker-compose.prod.yml exec app php artisan app:rollback
``` ```
@@ -140,38 +266,35 @@ docker compose -f docker-compose.prod.yml exec app php artisan app:rollback
### Prérequis ### Prérequis
- PHP 8.2+, Composer, Node.js 20+, npm - PHP 8.2+, Composer, Node.js 20+
- PostgreSQL 16+ ou Docker - MySQL 8+ / MariaDB 10.5+ ou PostgreSQL 16+
### Démarrage rapide ### Démarrage rapide
```bash ```bash
# Cloner et installer les dépendances
composer install composer install
npm install && npm run build npm install && npm run build
# Configuration
cp .env.example .env cp .env.example .env
php artisan key:generate php artisan key:generate
# Démarrer PostgreSQL et Redis (Docker) # Démarrer la base de données (Docker)
docker compose up -d docker compose up -d
# Base de données
php artisan migrate php artisan migrate
php artisan migrate:fresh --seed # reset + données de test php artisan migrate:fresh --seed # reset + données de test
# Serveur de développement php artisan serve # http://localhost:8000
php artisan serve # http://localhost:8000 npm run dev # Vite en watch (CSS/JS)
npm run dev # Vite en watch (CSS/JS)
``` ```
Compte administrateur créé par le seeder : Comptes de test créés par le seeder :
| Champ | Valeur | | E-mail | Rôle | Mot de passe |
|---|---| |---|---|---|
| E-mail | `admin@example.com` | | `admin@mesreleves.local` | Administrateur | `password` |
| Mot de passe | `password` | | `responsable@mesreleves.local` | Responsable de section | `password` |
| `membre@mesreleves.local` | Membre | `password` |
### Commandes utiles ### Commandes utiles
@@ -180,26 +303,18 @@ php artisan test # tous les tests
php artisan test --filter=NomTest # un test précis php artisan test --filter=NomTest # un test précis
./vendor/bin/pint # formatage PHP (Laravel Pint) ./vendor/bin/pint # formatage PHP (Laravel Pint)
./vendor/bin/phpstan analyse # analyse statique ./vendor/bin/phpstan analyse # analyse statique
php artisan app:check-update # vérifier les mises à jour
php artisan app:update # appliquer une mise à jour
php artisan app:rollback --list # lister les sauvegardes
``` ```
### Créer une release ### Créer une release
```bash ```bash
# Modifier VERSION (ex : 1.1.0)
echo "1.1.0" > VERSION echo "1.1.0" > VERSION
git add VERSION && git commit -m "bump version 1.1.0" git add VERSION && git commit -m "bump version 1.1.0"
# Construire l'archive
bin/build-release.sh bin/build-release.sh
# → mesreleves-1.1.0.tar.gz + mesreleves-1.1.0.tar.gz.sha256 # → mesreleves-1.1.0.tar.gz
# Publier
git tag v1.1.0 && git push origin v1.1.0 git tag v1.1.0 && git push origin v1.1.0
# Créer une release sur Gitea et joindre les deux fichiers
``` ```
--- ---
@@ -210,26 +325,28 @@ git tag v1.1.0 && git push origin v1.1.0
app/ app/
Models/ Eloquent : User, Source, Releve, Lieu, Section, Depot… Models/ Eloquent : User, Source, Releve, Lieu, Section, Depot…
Http/ Http/
Controllers/ Un controller par entité (+ Admin/ pour la gestion) Controllers/ Un controller par entité (+ Admin/ + Auth/)
Middleware/ RoleMiddleware (admin, section_manager, member) Middleware/ RoleMiddleware, CheckInstallation, EnsureUserIsActive
Requests/ Form requests avec validation dynamique Requests/ Form requests avec validation
Policies/ Autorisation par modèle (Gates/Policies) Policies/ Autorisation par modèle
Services/ GedcomExportService, DateConversionService, UpdateService Services/ GedcomExportService, DateConversionService, UpdateService,
SiteSettingsService
Enums/ SourceStatus, UserRole, CalendarType, FieldType Enums/ SourceStatus, UserRole, CalendarType, FieldType
database/ database/
migrations/ 10 migrations (lieux, sections, sources, relevés…) migrations/ Schéma complet (lieux, sections, sources, relevés…)
seeders/ Données de démonstration seeders/ Données de démonstration
resources/views/ resources/views/
layouts/ Navigation, app layout layouts/ Navigation (sélecteur de thème), app layout, guest layout
components/ lieu-picker (Alpine.js + AJAX) components/ lieu-picker, user-picker (recherche modale), dropdown…
setup/ Assistant d'installation en 5 étapes
sources/ CRUD + workflow de statut sources/ CRUD + workflow de statut
releves/ Formulaire dynamique par type de source releves/ Formulaire dynamique par type de source
recherche/ Recherche plein texte + filtres recherche/ Recherche plein texte + filtres
admin/ Tableau de bord, utilisateurs, sections, dépôts admin/ Tableau de bord, utilisateurs, sections, dépôts, paramètres
bin/
build-release.sh Construction de l'archive de distribution
``` ```
---
## Licence ## Licence
Usage interne — association de généalogie. Usage interne — association de généalogie.
+1 -1
View File
@@ -1 +1 @@
1.0.0 1.0.3
@@ -18,14 +18,16 @@ class SettingController extends Controller
{ {
public function index(UpdateService $updates): View public function index(UpdateService $updates): View
{ {
$updatesDisabled = SiteSettingsService::updatesDisabled();
$installedVersion = $updates->getInstalledVersion(); $installedVersion = $updates->getInstalledVersion();
$latestRelease = $updates->fetchLatestRelease(); $latestRelease = $updatesDisabled ? null : $updates->fetchLatestRelease();
$updateAvailable = $latestRelease $updateAvailable = $latestRelease
&& version_compare($latestRelease['version'], $installedVersion, '>'); && version_compare($latestRelease['version'], $installedVersion, '>');
return view('admin.parametres.index', [ return view('admin.parametres.index', [
'logoUrl' => SiteSettingsService::logoUrl(), 'logoUrl' => SiteSettingsService::logoUrl(),
'registrationEnabled' => SiteSettingsService::registrationEnabled(), 'registrationEnabled' => SiteSettingsService::registrationEnabled(),
'updatesDisabled' => $updatesDisabled,
'installedVersion' => $installedVersion, 'installedVersion' => $installedVersion,
'latestRelease' => $latestRelease, 'latestRelease' => $latestRelease,
'updateAvailable' => $updateAvailable, 'updateAvailable' => $updateAvailable,
@@ -144,6 +146,17 @@ class SettingController extends Controller
} }
} }
// ── Mises à jour ─────────────────────────────────────────────────────────
public function updateUpdates(Request $request): RedirectResponse
{
SiteSettingsService::set('updates_disabled', $request->boolean('updates_disabled'));
return back()->with('success', $request->boolean('updates_disabled')
? 'Vérification automatique des mises à jour désactivée.'
: 'Vérification automatique des mises à jour activée.');
}
// ── Paramètres généraux ─────────────────────────────────────────────────── // ── Paramètres généraux ───────────────────────────────────────────────────
public function updateSettings(Request $request): RedirectResponse public function updateSettings(Request $request): RedirectResponse
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Models\Lieu;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class CarteController extends Controller
{
public function index(): View
{
return view('carte.index');
}
public function data(Request $request): JsonResponse
{
$lieux = Lieu::whereNotNull('latitude')
->whereNotNull('longitude')
->whereHas('sources.releves')
->with([
'sources' => function ($q) {
$q->withCount('releves')
->select('id', 'nom', 'lieu_id', 'status', 'annee_debut', 'annee_fin')
->orderBy('nom');
},
])
->get()
->map(function (Lieu $lieu) {
$relevesTotal = $lieu->sources->sum('releves_count');
return [
'id' => $lieu->id,
'nom' => $lieu->nom_long ?? $lieu->nom,
'lat' => (float) $lieu->latitude,
'lng' => (float) $lieu->longitude,
'sources_count' => $lieu->sources->count(),
'releves_count' => $relevesTotal,
'sources' => $lieu->sources->map(fn ($s) => [
'id' => $s->id,
'nom' => $s->nom,
'releves_count' => $s->releves_count,
'status' => $s->status->label(),
'status_value' => $s->status->value,
'annees' => $s->annee_debut
? ($s->annee_debut === $s->annee_fin || ! $s->annee_fin
? (string) $s->annee_debut
: "{$s->annee_debut}{$s->annee_fin}")
: null,
]),
];
});
return response()->json($lieux);
}
}
+6
View File
@@ -145,6 +145,12 @@ class SetupController extends Controller
if (! $ok) $success = false; if (! $ok) $success = false;
} }
// 2b. Purge du cache de config pour forcer la lecture du nouveau .env
// (un bootstrap/cache/config.php résiduel sinon l'emporte sur le .env réécrit)
if ($success) {
$this->artisanRun($artisan, 'config:clear');
}
// 3. Migrations // 3. Migrations
if ($success) { if ($success) {
[$ok, $out] = $this->artisanRun($artisan, 'migrate --force'); [$ok, $out] = $this->artisanRun($artisan, 'migrate --force');
+11
View File
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Lieu extends Model class Lieu extends Model
{ {
@@ -32,6 +33,16 @@ class Lieu extends Model
return $this->hasMany(Section::class); return $this->hasMany(Section::class);
} }
public function sources(): HasMany
{
return $this->hasMany(Source::class);
}
public function releves(): HasManyThrough
{
return $this->hasManyThrough(Releve::class, Source::class);
}
public function calculerNomLong(): string public function calculerNomLong(): string
{ {
$noms = [$this->nom]; $noms = [$this->nom];
+7
View File
@@ -87,4 +87,11 @@ class SiteSettingsService
// Désactivées par défaut // Désactivées par défaut
return (bool) self::get('registration_enabled', false); return (bool) self::get('registration_enabled', false);
} }
// ── Mises à jour ──────────────────────────────────────────────────────────
public static function updatesDisabled(): bool
{
return (bool) self::get('updates_disabled', false);
}
} }
+16 -14
View File
@@ -1,25 +1,27 @@
<IfModule mod_rewrite.c> # ── Sécurité ──────────────────────────────────────────────────────────────────
<IfModule mod_negotiation.c> Options -Indexes -MultiViews
Options -MultiViews -Indexes
</IfModule>
# ── En-têtes HTTP transmis à PHP ───────────────────────────────────────────────
# Nécessaire pour que Laravel reçoive le token Authorization (API) et CSRF
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
SetEnvIf X-XSRF-Token "(.*)" HTTP_X_XSRF_TOKEN=$1
# ── Routage vers le contrôleur frontal ────────────────────────────────────────
# FallbackResource ne requiert pas mod_rewrite (disponible depuis Apache 2.2.16).
# Tout chemin qui ne correspond pas à un fichier ou répertoire existant
# est servi par index.php — comportement identique à la règle mod_rewrite ci-dessous.
<IfModule mod_rewrite.c>
RewriteEngine On RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$ RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301] RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L] RewriteRule ^ index.php [L]
</IfModule> </IfModule>
<IfModule !mod_rewrite.c>
FallbackResource /index.php
</IfModule>
+19
View File
@@ -5,6 +5,25 @@ use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true)); define('LARAVEL_START', microtime(true));
// Si aucun .env n'existe, en créer un minimal depuis .env.example pour que
// l'assistant d'installation (/setup) puisse démarrer sans erreur de clé.
// Le wizard remplace ce fichier avec les paramètres définitifs lors de l'installation.
(function () {
$envFile = __DIR__ . '/../.env';
$example = __DIR__ . '/../.env.example';
if (! file_exists($envFile) && file_exists($example)) {
$content = file_get_contents($example);
// Génère une clé temporaire pour permettre le boot de Laravel
$content = preg_replace('/^APP_KEY=$/m',
'APP_KEY=base64:' . base64_encode(random_bytes(32)), $content);
// Pilotes sans dépendance BDD pendant l'installation
$content = preg_replace('/^SESSION_DRIVER=.*/m', 'SESSION_DRIVER=file', $content);
$content = preg_replace('/^CACHE_STORE=.*/m', 'CACHE_STORE=file', $content);
$content = preg_replace('/^QUEUE_CONNECTION=.*/m','QUEUE_CONNECTION=sync',$content);
file_put_contents($envFile, $content);
}
})();
// Determine if the application is in maintenance mode... // Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) { if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance; require $maintenance;
+412
View File
@@ -0,0 +1,412 @@
<?php
/**
* MesRelevés Diagnostic de configuration serveur
*
* ATTENTION : ce fichier est réservé aux tests. Il ne doit JAMAIS
* être déployé sur un environnement de production.
* Supprimez-le dès que votre vérification est terminée.
*/
// ── Collecte des informations ──────────────────────────────────────────────
$root = dirname(__DIR__); // répertoire racine du projet
$phpVer = PHP_VERSION;
$reqVer = '8.2.0';
$phpOk = version_compare($phpVer, $reqVer, '>=');
// Extensions requises : [nom => optionnel?]
$extensions = [
'pdo' => false,
'pdo_mysql' => false,
'mbstring' => false,
'openssl' => false,
'tokenizer' => false,
'xml' => false,
'ctype' => false,
'json' => false,
'bcmath' => false,
'fileinfo' => false,
'zip' => false,
'pdo_pgsql' => true, // optionnel : seulement si PostgreSQL
'curl' => true, // optionnel : mises à jour automatiques
'intl' => true, // optionnel : formatage avancé des dates
];
// Répertoires à vérifier en écriture
$dirs = [
'storage/' => $root . '/storage',
'storage/logs/' => $root . '/storage/logs',
'storage/app/' => $root . '/storage/app',
'storage/framework/' => $root . '/storage/framework',
'bootstrap/cache/' => $root . '/bootstrap/cache',
'Racine (écriture .env)' => $root,
];
// Fichiers à vérifier
$files = [
'.env' => [$root . '/.env', false],
'vendor/autoload.php' => [$root . '/vendor/autoload.php', false],
'storage/installed' => [$root . '/storage/installed', true],
'public/build/manifest.json' => [$root . '/public/build/manifest.json', false],
];
// Configuration PHP recommandée : [directive => [valeur_min_bytes, label_min]]
$iniChecks = [
'memory_limit' => [128 * 1024 * 1024, '128 Mo'],
'upload_max_filesize'=> [2 * 1024 * 1024, '2 Mo'],
'post_max_size' => [8 * 1024 * 1024, '8 Mo'],
'max_execution_time' => [30, '30 s'],
];
function iniBytes(string $val): int {
$val = trim($val);
$last = strtolower($val[-1] ?? '');
$n = (int) $val;
return match ($last) {
'g' => $n * 1024 * 1024 * 1024,
'm' => $n * 1024 * 1024,
'k' => $n * 1024,
default => $n,
};
}
// Test de connexion BDD (si formulaire soumis)
$dbResult = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['db_host'])) {
$driver = $_POST['db_driver'] ?? 'mysql';
$host = $_POST['db_host'] ?? '';
$port = (int) ($_POST['db_port'] ?? ($driver === 'pgsql' ? 5432 : 3306));
$dbname = $_POST['db_name'] ?? '';
$user = $_POST['db_user'] ?? '';
$password = $_POST['db_password'] ?? '';
try {
$dsn = $driver === 'pgsql'
? "pgsql:host={$host};port={$port};dbname={$dbname}"
: "mysql:host={$host};port={$port};dbname={$dbname};charset=utf8mb4";
$pdo = new PDO($dsn, $user, $password, [PDO::ATTR_TIMEOUT => 5]);
$ver = $pdo->query('SELECT version()')->fetchColumn();
$dbResult = ['ok' => true, 'msg' => "Connexion réussie — " . htmlspecialchars($ver)];
} catch (Exception $e) {
$dbResult = ['ok' => false, 'msg' => htmlspecialchars($e->getMessage())];
}
}
// ── Compteur global ────────────────────────────────────────────────────────
$totalChecks = 0;
$totalFailed = 0;
?><!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Diagnostic serveur MesRelevés</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f3f4f6; --card: #fff; --border: #e5e7eb;
--text: #111827; --muted: #6b7280;
--ok: #16a34a; --ok-bg: #f0fdf4; --ok-border: #bbf7d0;
--warn: #d97706; --warn-bg: #fffbeb; --warn-border: #fde68a;
--err: #dc2626; --err-bg: #fef2f2; --err-border: #fecaca;
--accent: #4f46e5;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111827; --card: #1f2937; --border: #374151;
--text: #f9fafb; --muted: #9ca3af;
--ok: #4ade80; --ok-bg: #052e16; --ok-border: #166534;
--warn: #fbbf24; --warn-bg: #1c1400; --warn-border: #92400e;
--err: #f87171; --err-bg: #1a0000; --err-border: #991b1b;
}
}
body { background: var(--bg); color: var(--text); font: 15px/1.6 system-ui, sans-serif; padding: 24px 16px 60px; }
h1 { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
.warning-banner {
background: #7f1d1d; color: #fef2f2; border: 2px solid #991b1b;
border-radius: 8px; padding: 12px 16px; margin-bottom: 24px;
font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 10px;
}
.warning-banner svg { flex-shrink: 0; }
.container { max-width: 860px; margin: 0 auto; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; margin-bottom: 20px; overflow: hidden; }
.card-header { padding: 14px 20px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: 13px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); display: flex; align-items: center; justify-content: space-between; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
td, th { padding: 9px 20px; text-align: left; border-bottom: 1px solid var(--border); }
tr:last-child td { border-bottom: none; }
th { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; background: var(--bg); }
.badge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 9999px; font-size: 12px; font-weight: 600; white-space: nowrap; }
.badge-ok { background: var(--ok-bg); color: var(--ok); border: 1px solid var(--ok-border); }
.badge-warn { background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-border); }
.badge-err { background: var(--err-bg); color: var(--err); border: 1px solid var(--err-border); }
.badge-info { background: var(--bg); color: var(--muted);border: 1px solid var(--border); }
.mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; }
.muted { color: var(--muted); font-size: 13px; }
.summary { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
.summary-item { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 14px 20px; flex: 1; min-width: 140px; }
.summary-item .val { font-size: 28px; font-weight: 800; line-height: 1; }
.summary-item .lbl { font-size: 12px; color: var(--muted); margin-top: 4px; }
.val-ok { color: var(--ok); }
.val-err { color: var(--err); }
form { padding: 16px 20px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 14px; }
label { display: block; font-size: 13px; font-weight: 600; color: var(--muted); margin-bottom: 4px; }
input, select { width: 100%; padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font-size: 14px; }
button[type=submit] { background: var(--accent); color: #fff; border: none; padding: 8px 20px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; }
button[type=submit]:hover { opacity: .9; }
.db-result { margin-top: 12px; padding: 10px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; }
.db-ok { background: var(--ok-bg); color: var(--ok); border: 1px solid var(--ok-border); }
.db-err { background: var(--err-bg); color: var(--err); border: 1px solid var(--err-border); }
.server-info { padding: 14px 20px; font-size: 13px; display: grid; grid-template-columns: 200px 1fr; gap: 6px 16px; }
.server-info dt { color: var(--muted); }
.server-info dd { font-family: monospace; word-break: break-all; }
</style>
</head>
<body>
<div class="container">
<h1>Diagnostic serveur MesRelevés</h1>
<p class="muted" style="margin-bottom:20px">Version PHP <?= htmlspecialchars($phpVer) ?> · <?= htmlspecialchars($_SERVER['SERVER_SOFTWARE'] ?? 'Serveur inconnu') ?></p>
<div class="warning-banner">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
</svg>
FICHIER DE TEST UNIQUEMENT À supprimer immédiatement après diagnostic. Ne jamais laisser en production.
</div>
<?php
// ── Comptage global (première passe) ──────────────────────────────────────
$extFailed = 0; $extMandatoryFailed = 0;
foreach ($extensions as $ext => $optional) {
if (!extension_loaded($ext) && !$optional) $extMandatoryFailed++;
}
$dirFailed = 0;
foreach ($dirs as $path) {
if (!is_writable($path)) $dirFailed++;
}
$fileFailed = 0;
foreach ($files as [$path, $optional]) {
if (!file_exists($path) && !$optional) $fileFailed++;
}
$totalFailed = (!$phpOk ? 1 : 0) + $extMandatoryFailed + $dirFailed + $fileFailed;
$totalOk = ($phpOk ? 1 : 0)
+ count(array_filter(array_keys($extensions), fn($e) => extension_loaded($e) && !$extensions[$e]))
+ count(array_filter(array_keys($dirs), fn($k) => is_writable($dirs[$k])))
+ count(array_filter(array_keys($files), fn($k) => file_exists($files[$k][0])));
?>
<div class="summary">
<div class="summary-item">
<div class="val <?= $totalFailed === 0 ? 'val-ok' : 'val-err' ?>"><?= $totalFailed === 0 ? '✓' : $totalFailed ?></div>
<div class="lbl"><?= $totalFailed === 0 ? 'Tout est OK' : 'Problème(s) bloquant(s)' ?></div>
</div>
<div class="summary-item">
<div class="val"><?= PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION ?></div>
<div class="lbl">Version PHP</div>
</div>
<div class="summary-item">
<div class="val"><?= count(array_filter(array_keys($extensions), fn($e) => extension_loaded($e))) ?>/<?= count($extensions) ?></div>
<div class="lbl">Extensions chargées</div>
</div>
<div class="summary-item">
<div class="val"><?= ini_get('memory_limit') ?></div>
<div class="lbl">memory_limit</div>
</div>
</div>
{{-- ── Informations serveur ────────────────────────────────────────────── --}}
<div class="card">
<div class="card-header">Environnement</div>
<dl class="server-info">
<dt>Serveur</dt> <dd><?= htmlspecialchars($_SERVER['SERVER_SOFTWARE'] ?? '—') ?></dd>
<dt>SAPI</dt> <dd><?= htmlspecialchars(php_sapi_name()) ?></dd>
<dt>Document root</dt><dd><?= htmlspecialchars($_SERVER['DOCUMENT_ROOT'] ?? '—') ?></dd>
<dt>Chemin du script</dt><dd><?= htmlspecialchars(__FILE__) ?></dd>
<dt>Racine projet</dt><dd><?= htmlspecialchars($root) ?></dd>
<dt>OS</dt> <dd><?= htmlspecialchars(PHP_OS_FAMILY) ?></dd>
<dt>Fuseau horaire</dt><dd><?= htmlspecialchars(ini_get('date.timezone') ?: 'non défini') ?></dd>
</dl>
</div>
{{-- ── Version PHP ─────────────────────────────────────────────────────── --}}
<div class="card">
<div class="card-header">Version PHP</div>
<table>
<tr>
<td><strong>PHP <?= htmlspecialchars($phpVer) ?></strong></td>
<td class="muted">Minimum requis : <?= $reqVer ?></td>
<td><span class="badge <?= $phpOk ? 'badge-ok' : 'badge-err' ?>"><?= $phpOk ? '✓ OK' : '✗ Insuffisant' ?></span></td>
</tr>
</table>
</div>
{{-- ── Extensions PHP ──────────────────────────────────────────────────── --}}
<div class="card">
<div class="card-header">
Extensions PHP
<span class="muted" style="font-weight:400;text-transform:none">
<?= count(array_filter(array_keys($extensions), fn($e) => extension_loaded($e))) ?>/<?= count($extensions) ?> chargées
</span>
</div>
<table>
<thead><tr><th>Extension</th><th>Statut</th><th>Note</th></tr></thead>
<tbody>
<?php foreach ($extensions as $ext => $optional):
$loaded = extension_loaded($ext); ?>
<tr>
<td class="mono"><?= htmlspecialchars($ext) ?></td>
<td>
<?php if ($loaded): ?>
<span class="badge badge-ok"> Présente</span>
<?php elseif ($optional): ?>
<span class="badge badge-warn"> Absente</span>
<?php else: ?>
<span class="badge badge-err"> Manquante</span>
<?php endif; ?>
</td>
<td class="muted">
<?php if ($optional && !$loaded): echo 'Optionnelle'; endif; ?>
<?php if ($ext === 'pdo_pgsql' && !$loaded): echo '— nécessaire uniquement si PostgreSQL'; endif; ?>
<?php if ($ext === 'curl' && !$loaded): echo '— nécessaire pour les mises à jour automatiques'; endif; ?>
<?php if ($ext === 'intl' && !$loaded): echo '— recommandée (formatage dates/nombres)'; endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
{{-- ── Configuration PHP ───────────────────────────────────────────────── --}}
<div class="card">
<div class="card-header">Configuration PHP (php.ini)</div>
<table>
<thead><tr><th>Directive</th><th>Valeur actuelle</th><th>Minimum recommandé</th><th>Statut</th></tr></thead>
<tbody>
<?php foreach ($iniChecks as $key => [$minBytes, $minLabel]):
$raw = ini_get($key);
$bytes = iniBytes($raw);
$ok = $bytes >= $minBytes || $minBytes === 30 && ($bytes === 0 || $bytes >= $minBytes);
// max_execution_time = 0 means unlimited
if ($key === 'max_execution_time') $ok = ($bytes === 0 || $bytes >= $minBytes);
?>
<tr>
<td class="mono"><?= htmlspecialchars($key) ?></td>
<td class="mono"><?= htmlspecialchars($raw) ?></td>
<td class="muted"><?= $minLabel ?></td>
<td><span class="badge <?= $ok ? 'badge-ok' : 'badge-warn' ?>"><?= $ok ? '✓ OK' : '⚠ Faible' ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
{{-- ── Répertoires ─────────────────────────────────────────────────────── --}}
<div class="card">
<div class="card-header">Répertoires accessibles en écriture</div>
<table>
<thead><tr><th>Répertoire</th><th>Chemin</th><th>Statut</th></tr></thead>
<tbody>
<?php foreach ($dirs as $label => $path):
$exists = is_dir($path);
$writable = $exists && is_writable($path);
?>
<tr>
<td><strong><?= htmlspecialchars($label) ?></strong></td>
<td class="mono muted"><?= htmlspecialchars(str_replace($root, '…', $path)) ?></td>
<td>
<?php if (!$exists): ?>
<span class="badge badge-err"> Inexistant</span>
<?php elseif ($writable): ?>
<span class="badge badge-ok"> OK</span>
<?php else: ?>
<span class="badge badge-err"> Non accessible</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
{{-- ── Fichiers clés ───────────────────────────────────────────────────── --}}
<div class="card">
<div class="card-header">Fichiers clés</div>
<table>
<thead><tr><th>Fichier</th><th>Présent</th><th>Note</th></tr></thead>
<tbody>
<?php foreach ($files as $label => [$path, $optional]):
$exists = file_exists($path); ?>
<tr>
<td class="mono"><?= htmlspecialchars($label) ?></td>
<td>
<?php if ($exists): ?>
<span class="badge badge-ok"> Présent</span>
<?php elseif ($optional): ?>
<span class="badge badge-info"> Absent</span>
<?php else: ?>
<span class="badge badge-err"> Manquant</span>
<?php endif; ?>
</td>
<td class="muted">
<?php if ($label === '.env' && !$exists): echo 'Créé automatiquement par l\'assistant /setup'; endif; ?>
<?php if ($label === 'storage/installed'): echo $exists ? 'Application déjà installée' : 'Installation non encore effectuée → aller sur /setup'; endif; ?>
<?php if ($label === 'vendor/autoload.php' && !$exists): echo 'Exécutez : composer install --no-dev'; endif; ?>
<?php if ($label === 'public/build/manifest.json' && !$exists): echo 'Assets non compilés — npm run build requis'; endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
{{-- ── Test de connexion BDD ───────────────────────────────────────────── --}}
<div class="card">
<div class="card-header">Test de connexion base de données</div>
<form method="POST">
<div class="form-grid">
<div>
<label>Driver</label>
<select name="db_driver">
<option value="mysql" <?= ($_POST['db_driver'] ?? 'mysql') === 'mysql' ? 'selected' : '' ?>>MySQL / MariaDB</option>
<option value="pgsql" <?= ($_POST['db_driver'] ?? '') === 'pgsql' ? 'selected' : '' ?>>PostgreSQL</option>
</select>
</div>
<div>
<label>Hôte</label>
<input type="text" name="db_host" value="<?= htmlspecialchars($_POST['db_host'] ?? 'localhost') ?>" placeholder="localhost">
</div>
<div>
<label>Port</label>
<input type="number" name="db_port" value="<?= htmlspecialchars($_POST['db_port'] ?? '3306') ?>" placeholder="3306">
</div>
<div>
<label>Base de données</label>
<input type="text" name="db_name" value="<?= htmlspecialchars($_POST['db_name'] ?? '') ?>" placeholder="mesreleves">
</div>
<div>
<label>Utilisateur</label>
<input type="text" name="db_user" value="<?= htmlspecialchars($_POST['db_user'] ?? '') ?>" placeholder="mesreleves">
</div>
<div>
<label>Mot de passe</label>
<input type="password" name="db_password" placeholder="••••••••">
</div>
</div>
<button type="submit">Tester la connexion</button>
<?php if ($dbResult !== null): ?>
<div class="db-result <?= $dbResult['ok'] ? 'db-ok' : 'db-err' ?>">
<?= $dbResult['ok'] ? '✓ ' : '✗ ' ?><?= $dbResult['msg'] ?>
</div>
<?php endif; ?>
</form>
</div>
<p class="muted" style="text-align:center;font-size:13px;margin-top:8px">
Généré le <?= date('d/m/Y à H:i:s') ?> · MesRelevés <?= htmlspecialchars(trim(@file_get_contents($root . '/VERSION') ?: '')) ?>
</p>
</div>
</body>
</html>
+29
View File
@@ -1,3 +1,32 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
[x-cloak] { display: none !important; }
@layer base {
/*
* Dark mode champs de formulaire
* Appliqué globalement pour couvrir tous les <input>, <select>, <textarea>
* bruts qui n'utilisent pas le composant <x-text-input>.
* La spécificité (.dark + élément) surpasse les resets de @tailwindcss/forms.
*/
.dark input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="range"]),
.dark select,
.dark textarea {
background-color: theme('colors.gray.700');
border-color: theme('colors.gray.600');
color: theme('colors.gray.100');
}
.dark input::placeholder,
.dark textarea::placeholder {
color: theme('colors.gray.400');
}
/* Options du <select> natif (fond navigateur) */
.dark select option {
background-color: theme('colors.gray.700');
color: theme('colors.gray.100');
}
}
+39 -39
View File
@@ -1,20 +1,20 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold text-gray-800">Tableau de bord Administration</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Tableau de bord Administration</h2>
</x-slot> </x-slot>
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
{{-- Bandeau mise à jour disponible --}} {{-- Bandeau mise à jour disponible --}}
@if($updateAvailable) @if($updateAvailable)
<div class="bg-indigo-50 border border-indigo-300 rounded-xl p-4 flex items-center justify-between gap-4"> <div class="bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-300 rounded-xl p-4 flex items-center justify-between gap-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<svg class="w-5 h-5 text-indigo-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-indigo-500 dark:text-indigo-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/> d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</svg> </svg>
<div> <div>
<p class="text-sm font-semibold text-indigo-800"> <p class="text-sm font-semibold text-indigo-800 dark:text-indigo-200">
Mise à jour disponible : v{{ $latestRelease['version'] }} Mise à jour disponible : v{{ $latestRelease['version'] }}
<span class="ml-2 font-normal text-indigo-600">(installé : v{{ $installedVersion }})</span> <span class="ml-2 font-normal text-indigo-600">(installé : v{{ $installedVersion }})</span>
</p> </p>
@@ -35,10 +35,10 @@
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@php @php
$statusCards = [ $statusCards = [
['label' => 'À faire', 'key' => 'a_faire', 'color' => 'bg-gray-50 border-gray-200 text-gray-700'], ['label' => 'À faire', 'key' => 'a_faire', 'color' => 'bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300'],
['label' => 'En cours', 'key' => 'en_cours', 'color' => 'bg-blue-50 border-blue-200 text-blue-700'], ['label' => 'En cours', 'key' => 'en_cours', 'color' => 'bg-blue-50 border-blue-200 text-blue-700'],
['label' => 'À valider', 'key' => 'a_valider', 'color' => 'bg-yellow-50 border-yellow-200 text-yellow-700'], ['label' => 'À valider', 'key' => 'a_valider', 'color' => 'bg-yellow-50 border-yellow-200 dark:border-yellow-700 text-yellow-700'],
['label' => 'Terminé', 'key' => 'termine', 'color' => 'bg-green-50 border-green-200 text-green-700'], ['label' => 'Terminé', 'key' => 'termine', 'color' => 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-700 text-green-700'],
]; ];
@endphp @endphp
@foreach($statusCards as $card) @foreach($statusCards as $card)
@@ -53,19 +53,19 @@
{{-- Ligne de métriques secondaires --}} {{-- Ligne de métriques secondaires --}}
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div class="bg-white border border-gray-200 rounded-xl p-5 flex items-center gap-4"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-5 flex items-center gap-4">
<div class="p-3 bg-indigo-100 rounded-lg"> <div class="p-3 bg-indigo-100 dark:bg-indigo-900/50 rounded-lg">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg> </svg>
</div> </div>
<div> <div>
<p class="text-2xl font-bold text-gray-900">{{ number_format($totalReleves) }}</p> <p class="text-2xl font-bold text-gray-900 dark:text-white">{{ number_format($totalReleves) }}</p>
<p class="text-sm text-gray-500">relevé{{ $totalReleves > 1 ? 's' : '' }} saisi{{ $totalReleves > 1 ? 's' : '' }}</p> <p class="text-sm text-gray-500 dark:text-gray-400">relevé{{ $totalReleves > 1 ? 's' : '' }} saisi{{ $totalReleves > 1 ? 's' : '' }}</p>
</div> </div>
</div> </div>
<div class="bg-white border border-gray-200 rounded-xl p-5 flex items-center gap-4"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-5 flex items-center gap-4">
<div class="p-3 bg-purple-100 rounded-lg"> <div class="p-3 bg-purple-100 rounded-lg">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -73,18 +73,18 @@
</svg> </svg>
</div> </div>
<div> <div>
<p class="text-2xl font-bold text-gray-900">{{ $totalUsers }}</p> <p class="text-2xl font-bold text-gray-900 dark:text-white">{{ $totalUsers }}</p>
<p class="text-sm text-gray-500">utilisateur{{ $totalUsers > 1 ? 's' : '' }}</p> <p class="text-sm text-gray-500 dark:text-gray-400">utilisateur{{ $totalUsers > 1 ? 's' : '' }}</p>
</div> </div>
</div> </div>
<div class="bg-white border border-gray-200 rounded-xl p-5"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-5">
<p class="text-xs font-medium text-gray-500 uppercase mb-3">Répartition des rôles</p> <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-3">Répartition des rôles</p>
<div class="space-y-2"> <div class="space-y-2">
@foreach(\App\Enums\UserRole::cases() as $role) @foreach(\App\Enums\UserRole::cases() as $role)
@php $count = (int)($usersByRole[$role->value] ?? 0); @endphp @php $count = (int)($usersByRole[$role->value] ?? 0); @endphp
<div class="flex items-center justify-between text-sm"> <div class="flex items-center justify-between text-sm">
<span class="text-gray-600">{{ $role->label() }}</span> <span class="text-gray-600 dark:text-gray-400">{{ $role->label() }}</span>
<span class="font-semibold text-gray-900">{{ $count }}</span> <span class="font-semibold text-gray-900 dark:text-white">{{ $count }}</span>
</div> </div>
@endforeach @endforeach
</div> </div>
@@ -93,16 +93,16 @@
{{-- Activité mensuelle (6 derniers mois) --}} {{-- Activité mensuelle (6 derniers mois) --}}
@if($activiteMensuelle->isNotEmpty()) @if($activiteMensuelle->isNotEmpty())
<div class="bg-white border border-gray-200 rounded-xl p-6"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-4">Relevés saisis 6 derniers mois</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-4">Relevés saisis 6 derniers mois</h3>
@php $maxReleves = $activiteMensuelle->max('total') ?: 1; @endphp @php $maxReleves = $activiteMensuelle->max('total') ?: 1; @endphp
<div class="flex items-end gap-3 h-24"> <div class="flex items-end gap-3 h-24">
@foreach($activiteMensuelle as $mois) @foreach($activiteMensuelle as $mois)
@php $h = max(4, round(($mois->total / $maxReleves) * 96)); @endphp @php $h = max(4, round(($mois->total / $maxReleves) * 96)); @endphp
<div class="flex-1 flex flex-col items-center gap-1"> <div class="flex-1 flex flex-col items-center gap-1">
<span class="text-xs text-gray-500">{{ $mois->total }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{ $mois->total }}</span>
<div class="w-full bg-indigo-500 rounded-t" style="height: {{ $h }}px" title="{{ $mois->mois }} : {{ $mois->total }}"></div> <div class="w-full bg-indigo-50 dark:bg-indigo-900/300 rounded-t" style="height: {{ $h }}px" title="{{ $mois->mois }} : {{ $mois->total }}"></div>
<span class="text-xs text-gray-400 whitespace-nowrap">{{ $mois->mois }}</span> <span class="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ $mois->mois }}</span>
</div> </div>
@endforeach @endforeach
</div> </div>
@@ -118,8 +118,8 @@
['label' => 'Types de lieux', 'route' => 'admin.lieu-types.index', 'icon' => 'M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z'], ['label' => 'Types de lieux', 'route' => 'admin.lieu-types.index', 'icon' => 'M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z'],
] as $link) ] as $link)
<a href="{{ route($link['route']) }}" <a href="{{ route($link['route']) }}"
class="flex items-center gap-3 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-indigo-300 hover:shadow-sm transition-all text-sm text-gray-700 font-medium"> class="flex items-center gap-3 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:border-indigo-300 hover:shadow-sm transition-all text-sm text-gray-700 dark:text-gray-300 font-medium">
<svg class="w-5 h-5 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-gray-400 dark:text-gray-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $link['icon'] }}"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $link['icon'] }}"/>
</svg> </svg>
{{ $link['label'] }} {{ $link['label'] }}
@@ -130,57 +130,57 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{{-- Sources à valider --}} {{-- Sources à valider --}}
<div class="bg-white border border-gray-200 rounded-xl p-6"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">En attente de validation</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">En attente de validation</h3>
<a href="{{ route('sources.index', ['status' => 'a_valider']) }}" <a href="{{ route('sources.index', ['status' => 'a_valider']) }}"
class="text-xs text-indigo-600 hover:underline">Voir tout</a> class="text-xs text-indigo-600 hover:underline">Voir tout</a>
</div> </div>
@forelse($sourcesAValider as $source) @forelse($sourcesAValider as $source)
<div class="flex items-center justify-between py-2 border-b border-gray-100 last:border-0"> <div class="flex items-center justify-between py-2 border-b border-gray-100 dark:border-gray-700 last:border-0">
<div> <div>
<a href="{{ route('sources.show', $source) }}" <a href="{{ route('sources.show', $source) }}"
class="text-sm font-medium text-indigo-600 hover:underline">{{ $source->nom }}</a> class="text-sm font-medium text-indigo-600 hover:underline">{{ $source->nom }}</a>
<p class="text-xs text-gray-400">{{ $source->sourceType->nom }}</p> <p class="text-xs text-gray-400 dark:text-gray-500">{{ $source->sourceType->nom }}</p>
</div> </div>
<span class="text-xs text-gray-400 whitespace-nowrap"> <span class="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">
{{ $source->updated_at->diffForHumans() }} {{ $source->updated_at->diffForHumans() }}
</span> </span>
</div> </div>
@empty @empty
<p class="text-sm text-gray-400 py-4 text-center">Aucune source en attente.</p> <p class="text-sm text-gray-400 dark:text-gray-500 py-4 text-center">Aucune source en attente.</p>
@endforelse @endforelse
</div> </div>
{{-- Relevés récents --}} {{-- Relevés récents --}}
<div class="bg-white border border-gray-200 rounded-xl p-6"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Derniers relevés saisis</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Derniers relevés saisis</h3>
<a href="{{ route('recherche') }}" class="text-xs text-indigo-600 hover:underline">Recherche</a> <a href="{{ route('recherche') }}" class="text-xs text-indigo-600 hover:underline">Recherche</a>
</div> </div>
@forelse($relevesRecents as $releve) @forelse($relevesRecents as $releve)
<div class="flex items-center justify-between py-2 border-b border-gray-100 last:border-0"> <div class="flex items-center justify-between py-2 border-b border-gray-100 dark:border-gray-700 last:border-0">
<div class="min-w-0"> <div class="min-w-0">
<a href="{{ route('releves.show', $releve) }}" <a href="{{ route('releves.show', $releve) }}"
class="text-sm font-medium text-gray-900 hover:text-indigo-600 truncate block"> class="text-sm font-medium text-gray-900 dark:text-white hover:text-indigo-600 truncate block">
{{ $releve->nom ?? '—' }} {{ $releve->nom ?? '—' }}
@if($releve->prenom) {{ $releve->prenom }} @endif @if($releve->prenom) {{ $releve->prenom }} @endif
</a> </a>
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400 dark:text-gray-500">
{{ $releve->source->nom }} · {{ $releve->createur?->name ?? '?' }} {{ $releve->source->nom }} · {{ $releve->createur?->name ?? '?' }}
</p> </p>
</div> </div>
<span class="text-xs text-gray-400 whitespace-nowrap ml-3"> <span class="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap ml-3">
{{ $releve->created_at->diffForHumans() }} {{ $releve->created_at->diffForHumans() }}
</span> </span>
</div> </div>
@empty @empty
<p class="text-sm text-gray-400 py-4 text-center">Aucun relevé pour l'instant.</p> <p class="text-sm text-gray-400 dark:text-gray-500 py-4 text-center">Aucun relevé pour l'instant.</p>
@endforelse @endforelse
</div> </div>
</div> </div>
{{-- Version --}} {{-- Version --}}
<div class="flex items-center justify-end gap-2 text-xs text-gray-400 pt-2"> <div class="flex items-center justify-end gap-2 text-xs text-gray-400 dark:text-gray-500 pt-2">
<span>MesRelevés v{{ $installedVersion }}</span> <span>MesRelevés v{{ $installedVersion }}</span>
@if(! $updateAvailable) @if(! $updateAvailable)
<span class="inline-flex items-center gap-1 text-green-600"> <span class="inline-flex items-center gap-1 text-green-600">
+8 -8
View File
@@ -1,24 +1,24 @@
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<label for="nom" class="block text-sm font-medium text-gray-700">Nom <span class="text-red-500">*</span></label> <label for="nom" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nom <span class="text-red-500">*</span></label>
<input type="text" id="nom" name="nom" value="{{ old('nom', $depot?->nom) }}" required <input type="text" id="nom" name="nom" value="{{ old('nom', $depot?->nom) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $depot?->description) }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $depot?->description) }}</textarea>
</div> </div>
<div> <div>
<label for="adresse_postale" class="block text-sm font-medium text-gray-700">Adresse postale</label> <label for="adresse_postale" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Adresse postale</label>
<input type="text" id="adresse_postale" name="adresse_postale" value="{{ old('adresse_postale', $depot?->adresse_postale) }}" <input type="text" id="adresse_postale" name="adresse_postale" value="{{ old('adresse_postale', $depot?->adresse_postale) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div> <div>
<label for="url" class="block text-sm font-medium text-gray-700">Site internet</label> <label for="url" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Site internet</label>
<input type="url" id="url" name="url" value="{{ old('url', $depot?->url) }}" <input type="url" id="url" name="url" value="{{ old('url', $depot?->url) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('url') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('url') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
</div> </div>
@@ -1,13 +1,13 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Nouveau dépôt</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Nouveau dépôt</h2></x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('admin.depots.store') }}"> <form method="POST" action="{{ route('admin.depots.store') }}">
@csrf @csrf
@include('admin.depots._form', ['depot' => null]) @include('admin.depots._form', ['depot' => null])
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button>
<a href="{{ route('admin.depots.index') }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a> <a href="{{ route('admin.depots.index') }}" class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+3 -3
View File
@@ -1,13 +1,13 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Modifier : {{ $depot->nom }}</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Modifier : {{ $depot->nom }}</h2></x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('admin.depots.update', $depot) }}"> <form method="POST" action="{{ route('admin.depots.update', $depot) }}">
@csrf @method('PUT') @csrf @method('PUT')
@include('admin.depots._form', ['depot' => $depot]) @include('admin.depots._form', ['depot' => $depot])
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button>
<a href="{{ route('admin.depots.show', $depot) }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a> <a href="{{ route('admin.depots.show', $depot) }}" class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+15 -15
View File
@@ -1,37 +1,37 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Dépôts d'archives</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Dépôts d'archives</h2>
<a href="{{ route('admin.depots.create') }}" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouveau dépôt</a> <a href="{{ route('admin.depots.create') }}" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouveau dépôt</a>
</div> </div>
</x-slot> </x-slot>
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@if(session('success')) <div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> @endif @if(session('success')) <div class="mb-4 p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div> @endif
@if(session('error')) <div class="mb-4 p-4 bg-red-50 border border-red-200 text-red-800 rounded-md">{{ session('error') }}</div> @endif @if(session('error')) <div class="mb-4 p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200 rounded-md">{{ session('error') }}</div> @endif
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Sources</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Sources</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Site</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Site</th>
<th class="px-6 py-3"></th> <th class="px-6 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($depots as $depot) @forelse($depots as $depot)
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 font-medium"> <td class="px-6 py-4 font-medium">
<a href="{{ route('admin.depots.show', $depot) }}" class="text-indigo-600 hover:underline">{{ $depot->nom }}</a> <a href="{{ route('admin.depots.show', $depot) }}" class="text-indigo-600 hover:underline">{{ $depot->nom }}</a>
</td> </td>
<td class="px-6 py-4 text-sm text-gray-500">{{ $depot->sources_count }}</td> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $depot->sources_count }}</td>
<td class="px-6 py-4 text-sm text-gray-500"> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
@if($depot->url) <a href="{{ $depot->url }}" target="_blank" class="text-indigo-600 hover:underline truncate max-w-xs block">{{ $depot->url }}</a> @if($depot->url) <a href="{{ $depot->url }}" target="_blank" class="text-indigo-600 hover:underline truncate max-w-xs block">{{ $depot->url }}</a>
@else @else
@endif @endif
</td> </td>
<td class="px-6 py-4 text-right text-sm space-x-3"> <td class="px-6 py-4 text-right text-sm space-x-3">
<a href="{{ route('admin.depots.edit', $depot) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a> <a href="{{ route('admin.depots.edit', $depot) }}" class="text-gray-600 dark:text-gray-400 hover:text-indigo-600">Modifier</a>
<form method="POST" action="{{ route('admin.depots.destroy', $depot) }}" class="inline" <form method="POST" action="{{ route('admin.depots.destroy', $depot) }}" class="inline"
x-data @submit.prevent="if(confirm('Supprimer ce dépôt ?')) $el.submit()"> x-data @submit.prevent="if(confirm('Supprimer ce dépôt ?')) $el.submit()">
@csrf @method('DELETE') @csrf @method('DELETE')
@@ -40,7 +40,7 @@
</td> </td>
</tr> </tr>
@empty @empty
<tr><td colspan="4" class="px-6 py-10 text-center text-gray-400">Aucun dépôt.</td></tr> <tr><td colspan="4" class="px-6 py-10 text-center text-gray-400 dark:text-gray-500">Aucun dépôt.</td></tr>
@endforelse @endforelse
</tbody> </tbody>
</table> </table>
+9 -9
View File
@@ -1,29 +1,29 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">{{ $depot->nom }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">{{ $depot->nom }}</h2>
<a href="{{ route('admin.depots.edit', $depot) }}" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">Modifier</a> <a href="{{ route('admin.depots.edit', $depot) }}" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">Modifier</a>
</div> </div>
</x-slot> </x-slot>
<div class="py-8 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
<div class="bg-white shadow rounded-lg divide-y divide-gray-100 text-sm"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg divide-y divide-gray-100 dark:divide-gray-700 text-sm">
@if($depot->description) @if($depot->description)
<div class="px-6 py-4 grid grid-cols-3 gap-4"><dt class="font-medium text-gray-500">Description</dt><dd class="col-span-2">{{ $depot->description }}</dd></div> <div class="px-6 py-4 grid grid-cols-3 gap-4"><dt class="font-medium text-gray-500 dark:text-gray-400">Description</dt><dd class="col-span-2">{{ $depot->description }}</dd></div>
@endif @endif
@if($depot->adresse_postale) @if($depot->adresse_postale)
<div class="px-6 py-4 grid grid-cols-3 gap-4"><dt class="font-medium text-gray-500">Adresse</dt><dd class="col-span-2">{{ $depot->adresse_postale }}</dd></div> <div class="px-6 py-4 grid grid-cols-3 gap-4"><dt class="font-medium text-gray-500 dark:text-gray-400">Adresse</dt><dd class="col-span-2">{{ $depot->adresse_postale }}</dd></div>
@endif @endif
@if($depot->url) @if($depot->url)
<div class="px-6 py-4 grid grid-cols-3 gap-4"><dt class="font-medium text-gray-500">Site</dt><dd class="col-span-2"><a href="{{ $depot->url }}" target="_blank" class="text-indigo-600 hover:underline">{{ $depot->url }}</a></dd></div> <div class="px-6 py-4 grid grid-cols-3 gap-4"><dt class="font-medium text-gray-500 dark:text-gray-400">Site</dt><dd class="col-span-2"><a href="{{ $depot->url }}" target="_blank" class="text-indigo-600 hover:underline">{{ $depot->url }}</a></dd></div>
@endif @endif
</div> </div>
@if($depot->sources->isNotEmpty()) @if($depot->sources->isNotEmpty())
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b font-medium text-gray-900">Sources ({{ $depot->sources->count() }})</div> <div class="px-6 py-4 border-b font-medium text-gray-900 dark:text-white">Sources ({{ $depot->sources->count() }})</div>
<ul class="divide-y divide-gray-100"> <ul class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($depot->sources as $source) @foreach($depot->sources as $source)
<li class="px-6 py-3 text-sm text-gray-700">{{ $source->nom }}</li> <li class="px-6 py-3 text-sm text-gray-700 dark:text-gray-300">{{ $source->nom }}</li>
@endforeach @endforeach
</ul> </ul>
</div> </div>
@@ -1,17 +1,17 @@
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<label for="nom" class="block text-sm font-medium text-gray-700">Nom <span class="text-red-500">*</span></label> <label for="nom" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nom <span class="text-red-500">*</span></label>
<input type="text" id="nom" name="nom" value="{{ old('nom', $lieuType?->nom) }}" required <input type="text" id="nom" name="nom" value="{{ old('nom', $lieuType?->nom) }}" required
placeholder="ex : Pays, Région, Département, Ville…" placeholder="ex : Pays, Région, Département, Ville…"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror">
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label for="ordre" class="block text-sm font-medium text-gray-700">Ordre d'affichage <span class="text-red-500">*</span></label> <label for="ordre" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Ordre d'affichage <span class="text-red-500">*</span></label>
<input type="number" id="ordre" name="ordre" value="{{ old('ordre', $lieuType?->ordre ?? 0) }}" <input type="number" id="ordre" name="ordre" value="{{ old('ordre', $lieuType?->ordre ?? 0) }}"
min="0" max="999" required min="0" max="999" required
class="mt-1 block w-32 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-32 rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-xs text-gray-400">Les valeurs les plus basses apparaissent en premier (ex : Pays=0, Région=10, Département=20, Ville=30)</p> <p class="mt-1 text-xs text-gray-400 dark:text-gray-500">Les valeurs les plus basses apparaissent en premier (ex : Pays=0, Région=10, Département=20, Ville=30)</p>
</div> </div>
</div> </div>
@@ -1,13 +1,13 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Nouveau type de lieu</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Nouveau type de lieu</h2></x-slot>
<div class="py-8 max-w-lg mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-lg mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('admin.lieu-types.store') }}"> <form method="POST" action="{{ route('admin.lieu-types.store') }}">
@csrf @csrf
@include('admin.lieu-types._form', ['lieuType' => null]) @include('admin.lieu-types._form', ['lieuType' => null])
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button>
<a href="{{ route('admin.lieu-types.index') }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a> <a href="{{ route('admin.lieu-types.index') }}" class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
@@ -1,13 +1,13 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Modifier : {{ $lieuType->nom }}</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Modifier : {{ $lieuType->nom }}</h2></x-slot>
<div class="py-8 max-w-lg mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-lg mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('admin.lieu-types.update', $lieuType) }}"> <form method="POST" action="{{ route('admin.lieu-types.update', $lieuType) }}">
@csrf @method('PUT') @csrf @method('PUT')
@include('admin.lieu-types._form', ['lieuType' => $lieuType]) @include('admin.lieu-types._form', ['lieuType' => $lieuType])
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button>
<a href="{{ route('admin.lieu-types.index') }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a> <a href="{{ route('admin.lieu-types.index') }}" class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Types de lieux</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Types de lieux</h2>
<a href="{{ route('admin.lieu-types.create') }}" <a href="{{ route('admin.lieu-types.create') }}"
class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouveau type</a> class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouveau type</a>
</div> </div>
@@ -10,31 +10,31 @@
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
@foreach(['success','error'] as $flash) @foreach(['success','error'] as $flash)
@if(session($flash)) @if(session($flash))
<div class="mb-4 p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 border border-green-200 text-green-800' : 'bg-red-50 border border-red-200 text-red-800' }}"> <div class="mb-4 p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200' : 'bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200' }}">
{{ session($flash) }} {{ session($flash) }}
</div> </div>
@endif @endif
@endforeach @endforeach
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Ordre</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Ordre</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Lieux</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Lieux</th>
<th class="px-6 py-3"></th> <th class="px-6 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($lieuTypes as $lt) @forelse($lieuTypes as $lt)
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 text-sm text-gray-400 w-16">{{ $lt->ordre }}</td> <td class="px-6 py-4 text-sm text-gray-400 dark:text-gray-500 w-16">{{ $lt->ordre }}</td>
<td class="px-6 py-4 font-medium text-gray-900">{{ $lt->nom }}</td> <td class="px-6 py-4 font-medium text-gray-900 dark:text-white">{{ $lt->nom }}</td>
<td class="px-6 py-4 text-sm text-gray-500">{{ $lt->lieux_count }}</td> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $lt->lieux_count }}</td>
<td class="px-6 py-4 text-right text-sm space-x-3"> <td class="px-6 py-4 text-right text-sm space-x-3">
<a href="{{ route('admin.lieu-types.edit', $lt) }}" <a href="{{ route('admin.lieu-types.edit', $lt) }}"
class="text-gray-600 hover:text-indigo-600">Modifier</a> class="text-gray-600 dark:text-gray-400 hover:text-indigo-600">Modifier</a>
<form method="POST" action="{{ route('admin.lieu-types.destroy', $lt) }}" class="inline" <form method="POST" action="{{ route('admin.lieu-types.destroy', $lt) }}" class="inline"
x-data @submit.prevent="if(confirm('Supprimer ce type ?')) $el.submit()"> x-data @submit.prevent="if(confirm('Supprimer ce type ?')) $el.submit()">
@csrf @method('DELETE') @csrf @method('DELETE')
@@ -44,7 +44,7 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="4" class="px-6 py-10 text-center text-gray-400"> <td colspan="4" class="px-6 py-10 text-center text-gray-400 dark:text-gray-500">
Aucun type de lieu défini. Aucun type de lieu défini.
</td> </td>
</tr> </tr>
+129 -94
View File
@@ -1,23 +1,76 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold text-gray-800">Paramètres du site</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Paramètres du site</h2>
</x-slot> </x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@if(session('success')) @if(session('success'))
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> <div class="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div>
@endif @endif
{{-- Paramètres généraux (titre + inscriptions) --}}
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-6">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Paramètres généraux</h3>
<form method="POST" action="{{ route('admin.parametres.update') }}" class="space-y-5">
@csrf
{{-- Titre du site --}}
<div>
<label for="site_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Titre du site
</label>
<input type="text" id="site_name" name="site_name"
value="{{ old('site_name', \App\Services\SiteSettingsService::get('site_name')) }}"
placeholder="{{ config('app.name', 'MesRelevés') }}"
maxlength="100"
class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm
focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
Affiché dans la navigation, les e-mails et les exports.
Laisser vide pour utiliser la valeur par défaut
(« {{ config('app.name', 'MesRelevés') }} »).
</p>
@error('site_name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
{{-- Inscriptions --}}
<div class="pt-4 border-t border-gray-100 dark:border-gray-700">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Inscription publique des comptes</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
Autorise ou non les visiteurs à créer un compte via la page d'inscription.
Quand désactivée, seul un administrateur peut créer des comptes.
</p>
<label class="flex items-center gap-3 cursor-pointer">
<input type="hidden" name="registration_enabled" value="0">
<input type="checkbox" name="registration_enabled" value="1"
{{ $registrationEnabled ? 'checked' : '' }}
class="w-4 h-4 text-indigo-600 border-gray-300 dark:border-gray-600 rounded focus:ring-indigo-500">
<span class="text-sm text-gray-700 dark:text-gray-300">Autoriser l'inscription de nouveaux comptes</span>
</label>
</div>
<div>
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
Enregistrer
</button>
</div>
</form>
</div>
{{-- Logo --}} {{-- Logo --}}
<div class="bg-white shadow rounded-lg p-6 space-y-5"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-5">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Logo du site</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Logo du site</h3>
@if($logoUrl) @if($logoUrl)
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
<img src="{{ $logoUrl }}" alt="Logo actuel" class="h-20 w-auto object-contain rounded border border-gray-200 p-2"> <img src="{{ $logoUrl }}" alt="Logo actuel" class="h-20 w-auto object-contain rounded border border-gray-200 dark:border-gray-700 p-2">
<div> <div>
<p class="text-sm text-gray-600 mb-2">Logo actuel</p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Logo actuel</p>
<form method="POST" action="{{ route('admin.parametres.logo.delete') }}" <form method="POST" action="{{ route('admin.parametres.logo.delete') }}"
x-data @submit.prevent="if(confirm('Supprimer le logo ?')) $el.submit()"> x-data @submit.prevent="if(confirm('Supprimer le logo ?')) $el.submit()">
@csrf @method('DELETE') @csrf @method('DELETE')
@@ -26,19 +79,19 @@
</div> </div>
</div> </div>
@else @else
<p class="text-sm text-gray-400">Aucun logo configuré le nom de l'application est affiché.</p> <p class="text-sm text-gray-400 dark:text-gray-500">Aucun logo configuré le nom de l'application est affiché.</p>
@endif @endif
<form method="POST" action="{{ route('admin.parametres.logo.update') }}" <form method="POST" action="{{ route('admin.parametres.logo.update') }}"
enctype="multipart/form-data" class="space-y-4"> enctype="multipart/form-data" class="space-y-4">
@csrf @csrf
<div> <div>
<label for="logo" class="block text-sm font-medium text-gray-700 mb-1"> <label for="logo" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{{ $logoUrl ? 'Remplacer le logo' : 'Téléverser un logo' }} {{ $logoUrl ? 'Remplacer le logo' : 'Téléverser un logo' }}
</label> </label>
<input type="file" id="logo" name="logo" accept="image/*" <input type="file" id="logo" name="logo" accept="image/*"
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"> class="block w-full text-sm text-gray-500 dark:text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
<p class="mt-1 text-xs text-gray-400">PNG, JPG, SVG ou WebP · max 2 Mo · format recommandé : carré ou paysage, fond transparent</p> <p class="mt-1 text-xs text-gray-400 dark:text-gray-500">PNG, JPG, SVG ou WebP · max 2 Mo · format recommandé : carré ou paysage, fond transparent</p>
@error('logo') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('logo') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
<button type="submit" <button type="submit"
@@ -50,7 +103,7 @@
{{-- SMTP --}} {{-- SMTP --}}
@php $smtp = \App\Services\SiteSettingsService::smtpConfig(); @endphp @php $smtp = \App\Services\SiteSettingsService::smtpConfig(); @endphp
<div class="bg-white shadow rounded-lg p-6 space-y-5" <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-5"
x-data="{ x-data="{
host: '{{ $smtp['host'] ?? '' }}', host: '{{ $smtp['host'] ?? '' }}',
port: '{{ $smtp['port'] ?? 587 }}', port: '{{ $smtp['port'] ?? 587 }}',
@@ -98,13 +151,13 @@
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Serveur SMTP</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Serveur SMTP</h3>
<p class="text-xs text-gray-400 mt-0.5"> <p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Quand configuré, la connexion nécessitera un code PIN envoyé par e-mail (2FA). Quand configuré, la connexion nécessitera un code PIN envoyé par e-mail (2FA).
</p> </p>
</div> </div>
@if(\App\Services\SiteSettingsService::smtpConfigured()) @if(\App\Services\SiteSettingsService::smtpConfigured())
<span class="inline-flex items-center gap-1.5 text-xs text-green-700 bg-green-50 border border-green-200 px-2.5 py-1 rounded-full"> <span class="inline-flex items-center gap-1.5 text-xs text-green-700 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 px-2.5 py-1 rounded-full">
<svg class="w-3 h-3 fill-current" viewBox="0 0 20 20"> <svg class="w-3 h-3 fill-current" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg> </svg>
@@ -118,24 +171,24 @@
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div class="col-span-2"> <div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">Hôte SMTP</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Hôte SMTP</label>
<input type="text" name="smtp_host" x-model="host" <input type="text" name="smtp_host" x-model="host"
placeholder="smtp.exemple.fr" placeholder="smtp.exemple.fr"
class="w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('smtp_host') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror @error('smtp_host') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Port</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Port</label>
<input type="number" name="smtp_port" x-model="port" <input type="number" name="smtp_port" x-model="port"
class="w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('smtp_port') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror @error('smtp_port') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div> </div>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Chiffrement</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Chiffrement</label>
<select name="smtp_encryption" x-model="encryption" <select name="smtp_encryption" x-model="encryption"
class="w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="tls">TLS / STARTTLS (port 587 recommandé)</option> <option value="tls">TLS / STARTTLS (port 587 recommandé)</option>
<option value="ssl">SSL (port 465)</option> <option value="ssl">SSL (port 465)</option>
<option value="">Aucun (déconseillé)</option> <option value="">Aucun (déconseillé)</option>
@@ -144,39 +197,39 @@
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Identifiant</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Identifiant</label>
<input type="text" name="smtp_username" x-model="username" <input type="text" name="smtp_username" x-model="username"
autocomplete="off" autocomplete="off"
class="w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mot de passe</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Mot de passe</label>
<input type="password" name="smtp_password" x-model="password" <input type="password" name="smtp_password" x-model="password"
autocomplete="new-password" autocomplete="new-password"
class="w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500" class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="{{ \App\Services\SiteSettingsService::smtpConfigured() ? '(inchangé si vide)' : '' }}"> placeholder="{{ \App\Services\SiteSettingsService::smtpConfigured() ? '(inchangé si vide)' : '' }}">
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-4 pt-2 border-t border-gray-100"> <div class="grid grid-cols-2 gap-4 pt-2 border-t border-gray-100 dark:border-gray-700">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Adresse d'expéditeur</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Adresse d'expéditeur</label>
<input type="email" name="smtp_from_address" x-model="fromAddress" <input type="email" name="smtp_from_address" x-model="fromAddress"
placeholder="noreply@exemple.fr" placeholder="noreply@exemple.fr"
class="w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('smtp_from_address') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror @error('smtp_from_address') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nom d'expéditeur</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Nom d'expéditeur</label>
<input type="text" name="smtp_from_name" x-model="fromName" <input type="text" name="smtp_from_name" x-model="fromName"
class="w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('smtp_from_name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror @error('smtp_from_name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div> </div>
</div> </div>
{{-- Résultat test --}} {{-- Résultat test --}}
<div x-show="tested" x-cloak class="p-3 rounded-lg text-sm flex items-start gap-2" <div x-show="tested" x-cloak class="p-3 rounded-lg text-sm flex items-start gap-2"
:class="testOk ? 'bg-green-50 border border-green-200 text-green-800' : 'bg-red-50 border border-red-200 text-red-800'"> :class="testOk ? 'bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200' : 'bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200'">
<span x-text="testOk ? '✓' : '✗'" class="font-bold shrink-0"></span> <span x-text="testOk ? '✓' : '✗'" class="font-bold shrink-0"></span>
<span x-text="testMsg" class="break-all"></span> <span x-text="testMsg" class="break-all"></span>
</div> </div>
@@ -210,26 +263,26 @@
</form> </form>
</div> </div>
{{-- Version --}} {{-- Version du logiciel --}}
<div class="bg-white shadow rounded-lg p-6 space-y-4"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-4">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Version du logiciel</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Version du logiciel</h3>
@if($updateAvailable) @if($updateAvailable)
<div class="flex items-start gap-3 p-4 bg-indigo-50 border border-indigo-200 rounded-lg"> <div class="flex items-start gap-3 p-4 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-700 rounded-lg">
<svg class="w-5 h-5 text-indigo-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-indigo-500 dark:text-indigo-400 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/> d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</svg> </svg>
<div> <div>
<p class="text-sm font-semibold text-indigo-800"> <p class="text-sm font-semibold text-indigo-800 dark:text-indigo-200">
Mise à jour disponible : v{{ $latestRelease['version'] }} Mise à jour disponible : v{{ $latestRelease['version'] }}
</p> </p>
@if($latestRelease['published_at']) @if($latestRelease['published_at'])
<p class="text-xs text-indigo-500 mt-0.5"> <p class="text-xs text-indigo-500 dark:text-indigo-400 mt-0.5">
Publié {{ \Carbon\Carbon::parse($latestRelease['published_at'])->diffForHumans() }} Publié {{ \Carbon\Carbon::parse($latestRelease['published_at'])->diffForHumans() }}
</p> </p>
@endif @endif
<p class="text-xs text-indigo-600 mt-2 font-mono bg-indigo-100 inline-block px-2 py-1 rounded"> <p class="text-xs text-indigo-600 mt-2 font-mono bg-indigo-100 dark:bg-indigo-900/50 inline-block px-2 py-1 rounded">
php artisan app:update php artisan app:update
</p> </p>
</div> </div>
@@ -238,16 +291,16 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm text-gray-700 font-medium">MesRelevés v{{ $installedVersion }}</p> <p class="text-sm text-gray-700 dark:text-gray-300 font-medium">MesRelevés v{{ $installedVersion }}</p>
@php $installedAt = storage_path('installed'); @endphp @php $installedAt = storage_path('installed'); @endphp
@if(file_exists($installedAt)) @if(file_exists($installedAt))
<p class="text-xs text-gray-400 mt-0.5"> <p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Installé le {{ \Carbon\Carbon::createFromTimestamp(filemtime($installedAt))->isoFormat('LL') }} Installé le {{ \Carbon\Carbon::createFromTimestamp(filemtime($installedAt))->isoFormat('LL') }}
</p> </p>
@endif @endif
</div> </div>
@if(! $updateAvailable) @if(! $updateAvailable && ! $updatesDisabled)
<span class="inline-flex items-center gap-1.5 text-xs text-green-700 bg-green-50 border border-green-200 px-3 py-1.5 rounded-full"> <span class="inline-flex items-center gap-1.5 text-xs text-green-700 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 px-3 py-1.5 rounded-full">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg> </svg>
@@ -255,59 +308,41 @@
</span> </span>
@endif @endif
</div> </div>
{{-- Option désactiver les mises à jour --}}
<div class="pt-4 border-t border-gray-100 dark:border-gray-700">
<form method="POST" action="{{ route('admin.parametres.updates') }}">
@csrf
<div class="flex items-start gap-3">
<div class="flex items-center h-5 mt-0.5">
<input type="hidden" name="updates_disabled" value="0">
<input type="checkbox" id="updates_disabled" name="updates_disabled" value="1"
{{ $updatesDisabled ? 'checked' : '' }}
onchange="this.form.submit()"
class="w-4 h-4 text-amber-600 border-gray-300 dark:border-gray-600 rounded focus:ring-amber-500">
</div>
<div>
<label for="updates_disabled" class="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">
Désactiver la vérification automatique des mises à jour
</label>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
Quand coché, le site ne contacte plus le serveur distant pour vérifier l'existence
d'une nouvelle version. Utile en environnement sans accès Internet ou en production
isolée.
</p>
</div>
</div>
@if($updatesDisabled)
<p class="mt-2 ml-7 text-xs text-amber-600 flex items-center gap-1">
<svg class="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
Vérification des mises à jour désactivée les nouvelles versions ne seront pas signalées.
</p>
@endif
</form>
</div>
</div> </div>
{{-- Titre du site + Inscriptions (formulaire commun) --}}
<div class="bg-white shadow rounded-lg p-6 space-y-6">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Paramètres généraux</h3>
<form method="POST" action="{{ route('admin.parametres.update') }}" class="space-y-5">
@csrf
{{-- Titre du site --}}
<div>
<label for="site_name" class="block text-sm font-medium text-gray-700 mb-1">
Titre du site
</label>
<input type="text" id="site_name" name="site_name"
value="{{ old('site_name', \App\Services\SiteSettingsService::get('site_name')) }}"
placeholder="{{ config('app.name', 'MesRelevés') }}"
maxlength="100"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm
focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-xs text-gray-400">
Affiché dans la navigation, les e-mails et les exports.
Laisser vide pour utiliser la valeur par défaut
(« {{ config('app.name', 'MesRelevés') }} »).
</p>
@error('site_name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
{{-- Inscriptions --}}
<div class="pt-4 border-t border-gray-100">
<p class="text-sm font-medium text-gray-700 mb-2">Inscription publique des comptes</p>
<p class="text-xs text-gray-500 mb-3">
Autorise ou non les visiteurs à créer un compte via la page d'inscription.
Quand désactivée, seul un administrateur peut créer des comptes.
</p>
<label class="flex items-center gap-3 cursor-pointer">
<input type="hidden" name="registration_enabled" value="0">
<input type="checkbox" name="registration_enabled" value="1"
{{ $registrationEnabled ? 'checked' : '' }}
class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
<span class="text-sm text-gray-700">Autoriser l'inscription de nouveaux comptes</span>
</label>
</div>
<div>
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
Enregistrer
</button>
</div>
</form>
</div>
</div> </div>
</x-app-layout> </x-app-layout>
@@ -1,8 +1,8 @@
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<label for="nom" class="block text-sm font-medium text-gray-700">Nom <span class="text-red-500">*</span></label> <label for="nom" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nom <span class="text-red-500">*</span></label>
<input type="text" id="nom" name="nom" value="{{ old('nom', $section?->nom) }}" required <input type="text" id="nom" name="nom" value="{{ old('nom', $section?->nom) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror">
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
@@ -16,22 +16,22 @@
/> />
<div> <div>
<label for="adresse" class="block text-sm font-medium text-gray-700">Adresse</label> <label for="adresse" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Adresse</label>
<input type="text" id="adresse" name="adresse" value="{{ old('adresse', $section?->adresse) }}" <input type="text" id="adresse" name="adresse" value="{{ old('adresse', $section?->adresse) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div> <div>
<label for="email_contact" class="block text-sm font-medium text-gray-700">Email de contact</label> <label for="email_contact" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Email de contact</label>
<input type="email" id="email_contact" name="email_contact" value="{{ old('email_contact', $section?->email_contact) }}" <input type="email" id="email_contact" name="email_contact" value="{{ old('email_contact', $section?->email_contact) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('email_contact') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('email_contact') border-red-500 @enderror">
@error('email_contact') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('email_contact') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label for="url" class="block text-sm font-medium text-gray-700">Site internet</label> <label for="url" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Site internet</label>
<input type="url" id="url" name="url" value="{{ old('url', $section?->url) }}" <input type="url" id="url" name="url" value="{{ old('url', $section?->url) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('url') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('url') border-red-500 @enderror">
@error('url') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('url') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
</div> </div>
@@ -1,13 +1,13 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Nouvelle section</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Nouvelle section</h2></x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('admin.sections.store') }}"> <form method="POST" action="{{ route('admin.sections.store') }}">
@csrf @csrf
@include('admin.sections._form', ['section' => null]) @include('admin.sections._form', ['section' => null])
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button>
<a href="{{ route('admin.sections.index') }}" class="text-sm text-gray-500 hover:text-gray-700 self-center">Annuler</a> <a href="{{ route('admin.sections.index') }}" class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 self-center">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
@@ -1,13 +1,13 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Modifier : {{ $section->nom }}</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Modifier : {{ $section->nom }}</h2></x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('admin.sections.update', $section) }}"> <form method="POST" action="{{ route('admin.sections.update', $section) }}">
@csrf @method('PUT') @csrf @method('PUT')
@include('admin.sections._form', ['section' => $section]) @include('admin.sections._form', ['section' => $section])
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button>
<a href="{{ route('admin.sections.show', $section) }}" class="text-sm text-gray-500 hover:text-gray-700 self-center">Annuler</a> <a href="{{ route('admin.sections.show', $section) }}" class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 self-center">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+14 -14
View File
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Sections locales</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Sections locales</h2>
<a href="{{ route('admin.sections.create') }}" <a href="{{ route('admin.sections.create') }}"
class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouvelle section</a> class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouvelle section</a>
</div> </div>
@@ -9,29 +9,29 @@
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@if(session('success')) @if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> <div class="mb-4 p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div>
@endif @endif
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Lieu</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Lieu</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Contact</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Contact</th>
<th class="px-6 py-3"></th> <th class="px-6 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($sections as $section) @forelse($sections as $section)
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 font-medium"> <td class="px-6 py-4 font-medium">
<a href="{{ route('admin.sections.show', $section) }}" class="text-indigo-600 hover:underline">{{ $section->nom }}</a> <a href="{{ route('admin.sections.show', $section) }}" class="text-indigo-600 hover:underline">{{ $section->nom }}</a>
</td> </td>
<td class="px-6 py-4 text-sm text-gray-500">{{ $section->lieu?->nom ?? '—' }}</td> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $section->lieu?->nom ?? '—' }}</td>
<td class="px-6 py-4 text-sm text-gray-500">{{ $section->email_contact ?? '—' }}</td> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $section->email_contact ?? '—' }}</td>
<td class="px-6 py-4 text-right text-sm space-x-3"> <td class="px-6 py-4 text-right text-sm space-x-3">
<a href="{{ route('admin.sections.edit', $section) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a> <a href="{{ route('admin.sections.edit', $section) }}" class="text-gray-600 dark:text-gray-400 hover:text-indigo-600">Modifier</a>
<form method="POST" action="{{ route('admin.sections.destroy', $section) }}" class="inline" <form method="POST" action="{{ route('admin.sections.destroy', $section) }}" class="inline"
x-data @submit.prevent="if(confirm('Supprimer cette section ?')) $el.submit()"> x-data @submit.prevent="if(confirm('Supprimer cette section ?')) $el.submit()">
@csrf @method('DELETE') @csrf @method('DELETE')
@@ -40,7 +40,7 @@
</td> </td>
</tr> </tr>
@empty @empty
<tr><td colspan="4" class="px-6 py-10 text-center text-gray-400">Aucune section.</td></tr> <tr><td colspan="4" class="px-6 py-10 text-center text-gray-400 dark:text-gray-500">Aucune section.</td></tr>
@endforelse @endforelse
</tbody> </tbody>
</table> </table>
+31 -31
View File
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">{{ $section->nom }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">{{ $section->nom }}</h2>
<a href="{{ route('admin.sections.edit', $section) }}" <a href="{{ route('admin.sections.edit', $section) }}"
class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">Modifier</a> class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">Modifier</a>
</div> </div>
@@ -10,42 +10,42 @@
<div class="py-8 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@foreach(['success','error'] as $flash) @foreach(['success','error'] as $flash)
@if(session($flash)) @if(session($flash))
<div class="p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 border border-green-200 text-green-800' : 'bg-red-50 border border-red-200 text-red-800' }}"> <div class="p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200' : 'bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200' }}">
{{ session($flash) }} {{ session($flash) }}
</div> </div>
@endif @endif
@endforeach @endforeach
{{-- Fiche --}} {{-- Fiche --}}
<div class="bg-white shadow rounded-lg divide-y divide-gray-100 text-sm"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg divide-y divide-gray-100 dark:divide-gray-700 text-sm">
@foreach([['Lieu', $section->lieu?->nom_long ?? '—'], ['Adresse', $section->adresse ?? '—'], ['Email', $section->email_contact ?? '—'], ['Site', $section->url ?? '—']] as [$label, $val]) @foreach([['Lieu', $section->lieu?->nom_long ?? '—'], ['Adresse', $section->adresse ?? '—'], ['Email', $section->email_contact ?? '—'], ['Site', $section->url ?? '—']] as [$label, $val])
<div class="px-6 py-4 grid grid-cols-3 gap-4"> <div class="px-6 py-4 grid grid-cols-3 gap-4">
<dt class="font-medium text-gray-500">{{ $label }}</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">{{ $label }}</dt>
<dd class="col-span-2 text-gray-900">{{ $val }}</dd> <dd class="col-span-2 text-gray-900 dark:text-white">{{ $val }}</dd>
</div> </div>
@endforeach @endforeach
</div> </div>
{{-- Membres --}} {{-- Membres --}}
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 font-medium text-gray-900"> <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 font-medium text-gray-900 dark:text-white">
Membres ({{ $section->membres->count() }}) Membres ({{ $section->membres->count() }})
</div> </div>
@if($section->membres->isNotEmpty()) @if($section->membres->isNotEmpty())
<table class="min-w-full divide-y divide-gray-200 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Email</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Rôle dans la section</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Rôle dans la section</th>
<th class="px-6 py-3"></th> <th class="px-6 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($section->membres as $membre) @foreach($section->membres as $membre)
<tr> <tr>
<td class="px-6 py-3">{{ $membre->name }}</td> <td class="px-6 py-3">{{ $membre->name }}</td>
<td class="px-6 py-3 text-gray-500">{{ $membre->email }}</td> <td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ $membre->email }}</td>
<td class="px-6 py-3"> <td class="px-6 py-3">
{{ $membre->pivot->role_in_section === 'section_manager' ? 'Responsable' : 'Membre' }} {{ $membre->pivot->role_in_section === 'section_manager' ? 'Responsable' : 'Membre' }}
</td> </td>
@@ -63,25 +63,25 @@
@endif @endif
{{-- Ajouter un membre --}} {{-- Ajouter un membre --}}
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200"> <div class="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-700">
<form method="POST" action="{{ route('admin.sections.membres.add', $section) }}" class="flex gap-3 items-end"> <form method="POST" action="{{ route('admin.sections.membres.add', $section) }}"
x-data="{ canSubmit: false }"
@submit.prevent="if ($el.querySelector('[name=user_id]').value) $el.submit()">
@csrf @csrf
<div class="flex-1"> <div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-end">
<label class="block text-xs font-medium text-gray-600 mb-1">Utilisateur</label> <div class="flex-1">
<select name="user_id" class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Utilisateur</label>
@foreach($users as $user) <x-user-picker :users="$users" placeholder="Rechercher un utilisateur…" required />
<option value="{{ $user->id }}">{{ $user->name }} ({{ $user->email }})</option> </div>
@endforeach <div>
</select> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Rôle</label>
<select name="role_in_section" class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="member">Membre</option>
<option value="section_manager">Responsable</option>
</select>
</div>
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700 shrink-0">Ajouter</button>
</div> </div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Rôle</label>
<select name="role_in_section" class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="member">Membre</option>
<option value="section_manager">Responsable</option>
</select>
</div>
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">Ajouter</button>
</form> </form>
</div> </div>
</div> </div>
@@ -1,25 +1,25 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Nouveau type de source</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Nouveau type de source</h2></x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('admin.source-types.store') }}"> <form method="POST" action="{{ route('admin.source-types.store') }}">
@csrf @csrf
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<label for="nom" class="block text-sm font-medium text-gray-700">Nom <span class="text-red-500">*</span></label> <label for="nom" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nom <span class="text-red-500">*</span></label>
<input type="text" id="nom" name="nom" value="{{ old('nom') }}" required <input type="text" id="nom" name="nom" value="{{ old('nom') }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description') }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description') }}</textarea>
</div> </div>
</div> </div>
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer et définir les champs</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer et définir les champs</button>
<a href="{{ route('admin.source-types.index') }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a> <a href="{{ route('admin.source-types.index') }}" class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
@@ -1,24 +1,24 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Modifier : {{ $sourceType->nom }}</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Modifier : {{ $sourceType->nom }}</h2></x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('admin.source-types.update', $sourceType) }}"> <form method="POST" action="{{ route('admin.source-types.update', $sourceType) }}">
@csrf @method('PUT') @csrf @method('PUT')
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<label for="nom" class="block text-sm font-medium text-gray-700">Nom <span class="text-red-500">*</span></label> <label for="nom" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nom <span class="text-red-500">*</span></label>
<input type="text" id="nom" name="nom" value="{{ old('nom', $sourceType->nom) }}" required <input type="text" id="nom" name="nom" value="{{ old('nom', $sourceType->nom) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $sourceType->description) }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $sourceType->description) }}</textarea>
</div> </div>
</div> </div>
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button>
<a href="{{ route('admin.source-types.show', $sourceType) }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a> <a href="{{ route('admin.source-types.show', $sourceType) }}" class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
@@ -1,33 +1,33 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Types de sources</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Types de sources</h2>
<a href="{{ route('admin.source-types.create') }}" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouveau type</a> <a href="{{ route('admin.source-types.create') }}" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouveau type</a>
</div> </div>
</x-slot> </x-slot>
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@if(session('success')) <div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> @endif @if(session('success')) <div class="mb-4 p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div> @endif
@if(session('error')) <div class="mb-4 p-4 bg-red-50 border border-red-200 text-red-800 rounded-md">{{ session('error') }}</div> @endif @if(session('error')) <div class="mb-4 p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200 rounded-md">{{ session('error') }}</div> @endif
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Champs</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Champs</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Sources liées</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Sources liées</th>
<th class="px-6 py-3"></th> <th class="px-6 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($sourceTypes as $st) @forelse($sourceTypes as $st)
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 font-medium"> <td class="px-6 py-4 font-medium">
<a href="{{ route('admin.source-types.show', $st) }}" class="text-indigo-600 hover:underline">{{ $st->nom }}</a> <a href="{{ route('admin.source-types.show', $st) }}" class="text-indigo-600 hover:underline">{{ $st->nom }}</a>
</td> </td>
<td class="px-6 py-4 text-sm text-gray-500">{{ $st->fields_count ?? '—' }}</td> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $st->fields_count ?? '—' }}</td>
<td class="px-6 py-4 text-sm text-gray-500">{{ $st->sources_count }}</td> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $st->sources_count }}</td>
<td class="px-6 py-4 text-right text-sm space-x-3"> <td class="px-6 py-4 text-right text-sm space-x-3">
<a href="{{ route('admin.source-types.edit', $st) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a> <a href="{{ route('admin.source-types.edit', $st) }}" class="text-gray-600 dark:text-gray-400 hover:text-indigo-600">Modifier</a>
<form method="POST" action="{{ route('admin.source-types.destroy', $st) }}" class="inline" <form method="POST" action="{{ route('admin.source-types.destroy', $st) }}" class="inline"
x-data @submit.prevent="if(confirm('Supprimer ce type ?')) $el.submit()"> x-data @submit.prevent="if(confirm('Supprimer ce type ?')) $el.submit()">
@csrf @method('DELETE') @csrf @method('DELETE')
@@ -36,7 +36,7 @@
</td> </td>
</tr> </tr>
@empty @empty
<tr><td colspan="4" class="px-6 py-10 text-center text-gray-400">Aucun type de source.</td></tr> <tr><td colspan="4" class="px-6 py-10 text-center text-gray-400 dark:text-gray-500">Aucun type de source.</td></tr>
@endforelse @endforelse
</tbody> </tbody>
</table> </table>
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">{{ $sourceType->nom }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">{{ $sourceType->nom }}</h2>
<a href="{{ route('admin.source-types.edit', $sourceType) }}" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">Modifier</a> <a href="{{ route('admin.source-types.edit', $sourceType) }}" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">Modifier</a>
</div> </div>
</x-slot> </x-slot>
@@ -9,21 +9,21 @@
<div class="py-8 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@foreach(['success','error'] as $flash) @foreach(['success','error'] as $flash)
@if(session($flash)) @if(session($flash))
<div class="p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 border border-green-200 text-green-800' : 'bg-red-50 border border-red-200 text-red-800' }}"> <div class="p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200' : 'bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200' }}">
{{ session($flash) }} {{ session($flash) }}
</div> </div>
@endif @endif
@endforeach @endforeach
@if($sourceType->description) @if($sourceType->description)
<div class="bg-white shadow rounded-lg px-6 py-4 text-sm text-gray-700">{{ $sourceType->description }}</div> <div class="bg-white dark:bg-gray-800 shadow rounded-lg px-6 py-4 text-sm text-gray-700 dark:text-gray-300">{{ $sourceType->description }}</div>
@endif @endif
{{-- Champs existants --}} {{-- Champs existants --}}
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between"> <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 class="font-medium text-gray-900">Champs ({{ $sourceType->fields->count() }})</h3> <h3 class="font-medium text-gray-900 dark:text-white">Champs ({{ $sourceType->fields->count() }})</h3>
<span class="text-xs text-gray-400">Glisser-déposer pour réordonner</span> <span class="text-xs text-gray-400 dark:text-gray-500">Glisser-déposer pour réordonner</span>
</div> </div>
@if($sourceType->fields->isNotEmpty()) @if($sourceType->fields->isNotEmpty())
@@ -31,21 +31,21 @@
@csrf @csrf
</form> </form>
<ul id="fields-list" class="divide-y divide-gray-100"> <ul id="fields-list" class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($sourceType->fields as $field) @foreach($sourceType->fields as $field)
<li class="px-6 py-3 flex items-center gap-4" data-id="{{ $field->id }}"> <li class="px-6 py-3 flex items-center gap-4" data-id="{{ $field->id }}">
<span class="cursor-move text-gray-300 hover:text-gray-500 select-none"></span> <span class="cursor-move text-gray-300 dark:text-gray-600 hover:text-gray-500 select-none"></span>
<div class="flex-1 grid grid-cols-4 gap-3 text-sm"> <div class="flex-1 grid grid-cols-4 gap-3 text-sm">
<span class="font-mono text-indigo-700">{{ $field->name }}</span> <span class="font-mono text-indigo-700">{{ $field->name }}</span>
<span class="text-gray-700">{{ $field->label }}</span> <span class="text-gray-700 dark:text-gray-300">{{ $field->label }}</span>
<span class="text-gray-500">{{ $field->type->value }}</span> <span class="text-gray-500 dark:text-gray-400">{{ $field->type->value }}</span>
<span class="text-gray-400">{{ $field->required ? 'Obligatoire' : 'Optionnel' }}</span> <span class="text-gray-400 dark:text-gray-500">{{ $field->required ? 'Obligatoire' : 'Optionnel' }}</span>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="button" <button type="button"
x-data="{ open: false }" x-data="{ open: false }"
@click="open = !open" @click="open = !open"
class="text-xs text-gray-500 hover:text-indigo-600">Modifier</button> class="text-xs text-gray-500 dark:text-gray-400 hover:text-indigo-600">Modifier</button>
<form method="POST" action="{{ route('admin.source-types.fields.destroy', [$sourceType, $field]) }}" <form method="POST" action="{{ route('admin.source-types.fields.destroy', [$sourceType, $field]) }}"
x-data @submit.prevent="if(confirm('Supprimer ce champ ?')) $el.submit()"> x-data @submit.prevent="if(confirm('Supprimer ce champ ?')) $el.submit()">
@csrf @method('DELETE') @csrf @method('DELETE')
@@ -56,34 +56,34 @@
@endforeach @endforeach
</ul> </ul>
@else @else
<p class="px-6 py-8 text-center text-gray-400 text-sm">Aucun champ défini. Ajoutez-en ci-dessous.</p> <p class="px-6 py-8 text-center text-gray-400 dark:text-gray-500 text-sm">Aucun champ défini. Ajoutez-en ci-dessous.</p>
@endif @endif
</div> </div>
{{-- Ajouter un champ --}} {{-- Ajouter un champ --}}
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<h3 class="font-medium text-gray-900 mb-4">Ajouter un champ</h3> <h3 class="font-medium text-gray-900 dark:text-white mb-4">Ajouter un champ</h3>
<form method="POST" action="{{ route('admin.source-types.fields.store', $sourceType) }}"> <form method="POST" action="{{ route('admin.source-types.fields.store', $sourceType) }}">
@csrf @csrf
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Nom technique <span class="text-red-500">*</span></label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Nom technique <span class="text-red-500">*</span></label>
<input type="text" name="name" value="{{ old('name') }}" required <input type="text" name="name" value="{{ old('name') }}" required
placeholder="ex: nom_pere" placeholder="ex: nom_pere"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error('name') border-red-500 @enderror"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error('name') border-red-500 @enderror">
@error('name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror @error('name') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Libellé affiché <span class="text-red-500">*</span></label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Libellé affiché <span class="text-red-500">*</span></label>
<input type="text" name="label" value="{{ old('label') }}" required <input type="text" name="label" value="{{ old('label') }}" required
placeholder="ex: Nom du père" placeholder="ex: Nom du père"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error('label') border-red-500 @enderror"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error('label') border-red-500 @enderror">
@error('label') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror @error('label') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div x-data="{ type: '{{ old('type', 'text') }}' }"> <div x-data="{ type: '{{ old('type', 'text') }}' }">
<label class="block text-xs font-medium text-gray-600 mb-1">Type <span class="text-red-500">*</span></label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Type <span class="text-red-500">*</span></label>
<select name="type" x-model="type" <select name="type" x-model="type"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
@foreach(\App\Enums\FieldType::cases() as $ft) @foreach(\App\Enums\FieldType::cases() as $ft)
<option value="{{ $ft->value }}" {{ old('type') === $ft->value ? 'selected' : '' }}>{{ $ft->value }}</option> <option value="{{ $ft->value }}" {{ old('type') === $ft->value ? 'selected' : '' }}>{{ $ft->value }}</option>
@endforeach @endforeach
@@ -91,15 +91,15 @@
{{-- Options pour type=select --}} {{-- Options pour type=select --}}
<div x-show="type === 'select'" x-cloak class="mt-3"> <div x-show="type === 'select'" x-cloak class="mt-3">
<label class="block text-xs font-medium text-gray-600 mb-1">Options (une par ligne)</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Options (une par ligne)</label>
<textarea name="options_raw" rows="3" placeholder="Option A&#10;Option B&#10;Option C" <textarea name="options_raw" rows="3" placeholder="Option A&#10;Option B&#10;Option C"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('options_raw') }}</textarea> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('options_raw') }}</textarea>
</div> </div>
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<label class="flex items-center gap-2 text-sm text-gray-700"> <label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" name="required" value="1" {{ old('required') ? 'checked' : '' }} <input type="checkbox" name="required" value="1" {{ old('required') ? 'checked' : '' }}
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"> class="rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500">
Champ obligatoire Champ obligatoire
</label> </label>
</div> </div>
@@ -2,48 +2,48 @@
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<a href="{{ route('admin.utilisateurs.index') }}" class="text-sm text-indigo-600 hover:underline"> Utilisateurs</a> <a href="{{ route('admin.utilisateurs.index') }}" class="text-sm text-indigo-600 hover:underline"> Utilisateurs</a>
<span class="text-gray-400">/</span> <span class="text-gray-400 dark:text-gray-500">/</span>
<h2 class="text-xl font-semibold text-gray-800">{{ $user->name }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">{{ $user->name }}</h2>
</div> </div>
</x-slot> </x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@if(session('success')) @if(session('success'))
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> <div class="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div>
@endif @endif
@if(session('error')) @if(session('error'))
<div class="p-4 bg-red-50 border border-red-200 text-red-800 rounded-md">{{ session('error') }}</div> <div class="p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200 rounded-md">{{ session('error') }}</div>
@endif @endif
{{-- Informations --}} {{-- Informations --}}
<div class="bg-white shadow rounded-lg p-6 space-y-3"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-3">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Informations</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Informations</h3>
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm"> <dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
<dt class="text-gray-500">Nom</dt> <dt class="text-gray-500 dark:text-gray-400">Nom</dt>
<dd class="text-gray-900 font-medium">{{ $user->name }}</dd> <dd class="text-gray-900 dark:text-white font-medium">{{ $user->name }}</dd>
<dt class="text-gray-500">E-mail</dt> <dt class="text-gray-500 dark:text-gray-400">E-mail</dt>
<dd class="text-gray-900">{{ $user->email }}</dd> <dd class="text-gray-900 dark:text-white">{{ $user->email }}</dd>
<dt class="text-gray-500">Inscrit le</dt> <dt class="text-gray-500 dark:text-gray-400">Inscrit le</dt>
<dd class="text-gray-900">{{ $user->created_at->format('d/m/Y') }}</dd> <dd class="text-gray-900 dark:text-white">{{ $user->created_at->format('d/m/Y') }}</dd>
<dt class="text-gray-500">Sections</dt> <dt class="text-gray-500 dark:text-gray-400">Sections</dt>
<dd class="text-gray-900"> <dd class="text-gray-900 dark:text-white">
@if($user->sections->isNotEmpty()) @if($user->sections->isNotEmpty())
{{ $user->sections->pluck('nom')->join(', ') }} {{ $user->sections->pluck('nom')->join(', ') }}
@else @else
@endif @endif
</dd> </dd>
<dt class="text-gray-500">Sources assignées</dt> <dt class="text-gray-500 dark:text-gray-400">Sources assignées</dt>
<dd class="text-gray-900">{{ $user->sourcesAssignees->count() }}</dd> <dd class="text-gray-900 dark:text-white">{{ $user->sourcesAssignees->count() }}</dd>
</dl> </dl>
</div> </div>
{{-- Statut actif / inactif --}} {{-- Statut actif / inactif --}}
<div class="bg-white shadow rounded-lg p-6 flex items-center justify-between"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 flex items-center justify-between">
<div> <div>
<p class="text-sm font-medium text-gray-900">Statut du compte</p> <p class="text-sm font-medium text-gray-900 dark:text-white">Statut du compte</p>
<p class="text-sm text-gray-500 mt-0.5"> <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
@if($user->is_active) @if($user->is_active)
Le compte est <span class="text-green-600 font-medium">actif</span> l'utilisateur peut se connecter et être assigné à des sources. Le compte est <span class="text-green-600 font-medium">actif</span> l'utilisateur peut se connecter et être assigné à des sources.
@else @else
@@ -59,8 +59,8 @@
<button type="submit" <button type="submit"
class="px-4 py-2 text-sm font-medium rounded-md class="px-4 py-2 text-sm font-medium rounded-md
{{ $user->is_active {{ $user->is_active
? 'bg-red-50 text-red-700 border border-red-200 hover:bg-red-100' ? 'bg-red-50 dark:bg-red-900/30 text-red-700 border border-red-200 dark:border-red-700 hover:bg-red-100'
: 'bg-green-50 text-green-700 border border-green-200 hover:bg-green-100' }}"> : 'bg-green-50 dark:bg-green-900/30 text-green-700 border border-green-200 dark:border-green-700 hover:bg-green-100' }}">
{{ $user->is_active ? 'Désactiver le compte' : 'Activer le compte' }} {{ $user->is_active ? 'Désactiver le compte' : 'Activer le compte' }}
</button> </button>
</form> </form>
@@ -68,20 +68,20 @@
</div> </div>
{{-- Modifier le rôle --}} {{-- Modifier le rôle --}}
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-4">Rôle</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-4">Rôle</h3>
<form method="POST" action="{{ route('admin.utilisateurs.update', $user) }}"> <form method="POST" action="{{ route('admin.utilisateurs.update', $user) }}">
@csrf @method('PUT') @csrf @method('PUT')
<div class="space-y-3"> <div class="space-y-3">
@foreach(\App\Enums\UserRole::cases() as $role) @foreach(\App\Enums\UserRole::cases() as $role)
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50 <label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700
{{ $user->role === $role ? 'border-indigo-400 bg-indigo-50' : 'border-gray-200' }}"> {{ $user->role === $role ? 'border-indigo-400 bg-indigo-50 dark:bg-indigo-900/30' : 'border-gray-200 dark:border-gray-700' }}">
<input type="radio" name="role" value="{{ $role->value }}" <input type="radio" name="role" value="{{ $role->value }}"
{{ $user->role === $role ? 'checked' : '' }} {{ $user->role === $role ? 'checked' : '' }}
class="mt-0.5 text-indigo-600"> class="mt-0.5 text-indigo-600">
<div> <div>
<p class="text-sm font-medium text-gray-900">{{ $role->label() }}</p> <p class="text-sm font-medium text-gray-900 dark:text-white">{{ $role->label() }}</p>
<p class="text-xs text-gray-500 mt-0.5"> <p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
@if($role === \App\Enums\UserRole::Admin) @if($role === \App\Enums\UserRole::Admin)
Accès complet : gestion des utilisateurs, sections, dépôts, types de sources et statistiques. Accès complet : gestion des utilisateurs, sections, dépôts, types de sources et statistiques.
@elseif($role === \App\Enums\UserRole::SectionManager) @elseif($role === \App\Enums\UserRole::SectionManager)
@@ -100,7 +100,7 @@
Enregistrer Enregistrer
</button> </button>
<a href="{{ route('admin.utilisateurs.index') }}" <a href="{{ route('admin.utilisateurs.index') }}"
class="text-sm text-gray-500 self-center hover:text-gray-700"> class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">
Annuler Annuler
</a> </a>
</div> </div>
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Importer des utilisateurs</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Importer des utilisateurs</h2>
<a href="{{ route('admin.utilisateurs.index') }}" <a href="{{ route('admin.utilisateurs.index') }}"
class="text-sm text-indigo-600 hover:underline"> Retour à la liste</a> class="text-sm text-indigo-600 hover:underline"> Retour à la liste</a>
</div> </div>
@@ -11,7 +11,7 @@
{{-- Résultats d'import --}} {{-- Résultats d'import --}}
@if(isset($results)) @if(isset($results))
<div class="bg-white shadow rounded-lg p-6 space-y-4"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
@if($created > 0) @if($created > 0)
<span class="text-green-700 font-semibold text-sm"> <span class="text-green-700 font-semibold text-sm">
@@ -26,31 +26,31 @@
</div> </div>
@if($created > 0) @if($created > 0)
<div class="p-3 bg-amber-50 border border-amber-200 rounded-lg text-xs text-amber-800"> <div class="p-3 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 rounded-lg text-xs text-amber-800">
Les mots de passe temporaires ci-dessous sont affichés <strong>une seule fois</strong>. Les mots de passe temporaires ci-dessous sont affichés <strong>une seule fois</strong>.
Notez-les et communiquez-les aux utilisateurs concernés. Ils pourront les changer via leur profil. Notez-les et communiquez-les aux utilisateurs concernés. Ils pourront les changer via leur profil.
</div> </div>
@endif @endif
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full text-sm divide-y divide-gray-200"> <table class="min-w-full text-sm divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Ligne</th> <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Ligne</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">E-mail</th> <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">E-mail</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Rôle</th> <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Rôle</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Mot de passe temporaire</th> <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Mot de passe temporaire</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Statut</th> <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Statut</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@foreach($results as $r) @foreach($results as $r)
<tr class="{{ $r['ok'] ? '' : 'bg-red-50' }}"> <tr class="{{ $r['ok'] ? '' : 'bg-red-50 dark:bg-red-900/30' }}">
<td class="px-4 py-2 text-gray-400">{{ $r['line'] }}</td> <td class="px-4 py-2 text-gray-400 dark:text-gray-500">{{ $r['line'] }}</td>
<td class="px-4 py-2 font-medium text-gray-900">{{ $r['name'] }}</td> <td class="px-4 py-2 font-medium text-gray-900 dark:text-white">{{ $r['name'] }}</td>
<td class="px-4 py-2 text-gray-600">{{ $r['email'] }}</td> <td class="px-4 py-2 text-gray-600 dark:text-gray-400">{{ $r['email'] }}</td>
<td class="px-4 py-2 text-gray-500">{{ $r['role'] ?? '' }}</td> <td class="px-4 py-2 text-gray-500 dark:text-gray-400">{{ $r['role'] ?? '' }}</td>
<td class="px-4 py-2 font-mono text-indigo-700"> <td class="px-4 py-2 font-mono text-indigo-700">
{{ $r['ok'] ? $r['password'] : '' }} {{ $r['ok'] ? $r['password'] : '' }}
</td> </td>
@@ -77,26 +77,26 @@
@endif @endif
{{-- Formulaire d'import --}} {{-- Formulaire d'import --}}
<div class="bg-white shadow rounded-lg p-6 space-y-5"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-5">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Importer un fichier CSV</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Importer un fichier CSV</h3>
@if($errors->any()) @if($errors->any())
<div class="p-4 bg-red-50 border border-red-200 text-red-700 text-sm rounded-md space-y-1"> <div class="p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-700 text-sm rounded-md space-y-1">
@foreach($errors->all() as $e)<p>{{ $e }}</p>@endforeach @foreach($errors->all() as $e)<p>{{ $e }}</p>@endforeach
</div> </div>
@endif @endif
<div class="p-4 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800 space-y-1"> <div class="p-4 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800 dark:text-blue-200 space-y-1">
<p class="font-semibold">Format attendu du fichier CSV :</p> <p class="font-semibold">Format attendu du fichier CSV :</p>
<ul class="list-disc list-inside text-xs space-y-0.5"> <ul class="list-disc list-inside text-xs space-y-0.5">
<li>Séparateur : <code class="bg-blue-100 px-1 rounded">;</code> ou <code class="bg-blue-100 px-1 rounded">,</code></li> <li>Séparateur : <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">;</code> ou <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">,</code></li>
<li>Encodage : UTF-8 (avec ou sans BOM)</li> <li>Encodage : UTF-8 (avec ou sans BOM)</li>
<li>Colonnes obligatoires : <code class="bg-blue-100 px-1 rounded">name</code>, <code class="bg-blue-100 px-1 rounded">email</code>, <code class="bg-blue-100 px-1 rounded">role</code></li> <li>Colonnes obligatoires : <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">name</code>, <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">email</code>, <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">role</code></li>
<li>Colonne optionnelle : <code class="bg-blue-100 px-1 rounded">is_active</code> (1 ou 0, défaut : 1)</li> <li>Colonne optionnelle : <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">is_active</code> (1 ou 0, défaut : 1)</li>
<li>Valeurs acceptées pour <code class="bg-blue-100 px-1 rounded">role</code> : <li>Valeurs acceptées pour <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">role</code> :
<code class="bg-blue-100 px-1 rounded">member</code>, <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">member</code>,
<code class="bg-blue-100 px-1 rounded">section_manager</code>, <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">section_manager</code>,
<code class="bg-blue-100 px-1 rounded">admin</code> <code class="bg-blue-100 dark:bg-blue-900/50 px-1 rounded">admin</code>
</li> </li>
<li>Un mot de passe temporaire aléatoire sera généré pour chaque compte importé.</li> <li>Un mot de passe temporaire aléatoire sera généré pour chaque compte importé.</li>
</ul> </ul>
@@ -110,15 +110,15 @@
enctype="multipart/form-data" class="space-y-4"> enctype="multipart/form-data" class="space-y-4">
@csrf @csrf
<div> <div>
<label for="file" class="block text-sm font-medium text-gray-700 mb-1"> <label for="file" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Fichier CSV <span class="text-red-500">*</span> Fichier CSV <span class="text-red-500">*</span>
</label> </label>
<input type="file" id="file" name="file" accept=".csv,.txt" required <input type="file" id="file" name="file" accept=".csv,.txt" required
class="block w-full text-sm text-gray-500 class="block w-full text-sm text-gray-500 dark:text-gray-400
file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0
file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700
hover:file:bg-indigo-100"> hover:file:bg-indigo-100">
<p class="mt-1 text-xs text-gray-400">Taille maximale : 2 Mo.</p> <p class="mt-1 text-xs text-gray-400 dark:text-gray-500">Taille maximale : 2 Mo.</p>
@error('file') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('file') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
@@ -1,10 +1,10 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Gestion des utilisateurs</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Gestion des utilisateurs</h2>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<a href="{{ route('admin.utilisateurs.import') }}" <a href="{{ route('admin.utilisateurs.import') }}"
class="flex items-center gap-1.5 px-4 py-2 border border-gray-300 text-sm text-gray-700 rounded-md hover:bg-gray-50 transition-colors"> class="flex items-center gap-1.5 px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/> d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
@@ -12,7 +12,7 @@
Importer CSV Importer CSV
</a> </a>
<a href="{{ route('admin.utilisateurs.export', request()->only(['role', 'status', 'q'])) }}" <a href="{{ route('admin.utilisateurs.export', request()->only(['role', 'status', 'q'])) }}"
class="flex items-center gap-1.5 px-4 py-2 border border-gray-300 text-sm text-gray-700 rounded-md hover:bg-gray-50 transition-colors"> class="flex items-center gap-1.5 px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4 4m0 0l4 4m-4-4h12"/> d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4 4m0 0l4 4m-4-4h12"/>
@@ -26,33 +26,33 @@
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@if(session('success')) @if(session('success'))
<div class="p-4 bg-green-50 border border-green-200 text-green-800 text-sm rounded-md"> <div class="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 text-sm rounded-md">
{{ session('success') }} {{ session('success') }}
</div> </div>
@endif @endif
@if(session('error')) @if(session('error'))
<div class="p-4 bg-red-50 border border-red-200 text-red-800 text-sm rounded-md"> <div class="p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200 text-sm rounded-md">
{{ session('error') }} {{ session('error') }}
</div> </div>
@endif @endif
{{-- Filtres --}} {{-- Filtres --}}
@php $hasFilters = request()->anyFilled(['role', 'q', 'status']); @endphp @php $hasFilters = request()->anyFilled(['role', 'q', 'status']); @endphp
<div class="bg-white shadow rounded-lg p-5"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-5">
<form method="GET" action="{{ route('admin.utilisateurs.index') }}" <form method="GET" action="{{ route('admin.utilisateurs.index') }}"
class="flex flex-wrap items-end gap-4"> class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-[180px]"> <div class="flex-1 min-w-[180px]">
<label class="block text-xs font-medium text-gray-600 mb-1">Recherche</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Recherche</label>
<input type="text" name="q" value="{{ request('q') }}" <input type="text" name="q" value="{{ request('q') }}"
placeholder="Nom ou e-mail…" placeholder="Nom ou e-mail…"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm
focus:border-indigo-500 focus:ring-indigo-500"> focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div class="w-44"> <div class="w-44">
<label class="block text-xs font-medium text-gray-600 mb-1">Rôle</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Rôle</label>
<select name="role" <select name="role"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm
focus:border-indigo-500 focus:ring-indigo-500"> focus:border-indigo-500 focus:ring-indigo-500">
<option value=""> Tous </option> <option value=""> Tous </option>
@foreach(\App\Enums\UserRole::cases() as $r) @foreach(\App\Enums\UserRole::cases() as $r)
@@ -64,9 +64,9 @@
</div> </div>
<div class="w-40"> <div class="w-40">
<label class="block text-xs font-medium text-gray-600 mb-1">Statut</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Statut</label>
<select name="status" <select name="status"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm
focus:border-indigo-500 focus:ring-indigo-500"> focus:border-indigo-500 focus:ring-indigo-500">
<option value=""> Tous </option> <option value=""> Tous </option>
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>Actifs</option> <option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>Actifs</option>
@@ -81,7 +81,7 @@
</button> </button>
@if($hasFilters) @if($hasFilters)
<a href="{{ route('admin.utilisateurs.index') }}" <a href="{{ route('admin.utilisateurs.index') }}"
class="px-4 py-2 border border-gray-300 text-gray-600 text-sm rounded-md hover:bg-gray-50"> class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 text-sm rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
Effacer Effacer
</a> </a>
@endif @endif
@@ -90,52 +90,52 @@
</div> </div>
{{-- Tableau --}} {{-- Tableau --}}
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">E-mail</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">E-mail</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Rôle</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Rôle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Sections</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Sections</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Sources</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Sources</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Inscrit le</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Inscrit le</th>
<th class="px-6 py-3"></th> <th class="px-6 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($users as $user) @forelse($users as $user)
@php @php
$roleColors = [ $roleColors = [
'admin' => 'bg-red-100 text-red-700', 'admin' => 'bg-red-100 dark:bg-red-900/50 text-red-700',
'section_manager' => 'bg-blue-100 text-blue-700', 'section_manager' => 'bg-blue-100 dark:bg-blue-900/50 text-blue-700',
'member' => 'bg-gray-100 text-gray-600', 'member' => 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400',
]; ];
$color = $roleColors[$user->role->value] ?? 'bg-gray-100 text-gray-600'; $color = $roleColors[$user->role->value] ?? 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
@endphp @endphp
<tr class="hover:bg-gray-50 {{ ! $user->is_active ? 'opacity-60' : '' }}"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 {{ ! $user->is_active ? 'opacity-60' : '' }}">
<td class="px-6 py-4 font-medium text-gray-900"> <td class="px-6 py-4 font-medium text-gray-900 dark:text-white">
{{ $user->name }} {{ $user->name }}
@if($user->id === auth()->id()) @if($user->id === auth()->id())
<span class="ml-1 text-xs text-gray-400">(vous)</span> <span class="ml-1 text-xs text-gray-400 dark:text-gray-500">(vous)</span>
@endif @endif
@if(! $user->is_active) @if(! $user->is_active)
<span class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-600"> <span class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900/50 text-red-600">
Inactif Inactif
</span> </span>
@endif @endif
</td> </td>
<td class="px-6 py-4 text-gray-500">{{ $user->email }}</td> <td class="px-6 py-4 text-gray-500 dark:text-gray-400">{{ $user->email }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $color }}"> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $color }}">
{{ $user->role->label() }} {{ $user->role->label() }}
</span> </span>
</td> </td>
<td class="px-6 py-4 text-gray-500"> <td class="px-6 py-4 text-gray-500 dark:text-gray-400">
{{ $user->sections->isNotEmpty() ? $user->sections->pluck('nom')->join(', ') : '—' }} {{ $user->sections->isNotEmpty() ? $user->sections->pluck('nom')->join(', ') : '—' }}
</td> </td>
<td class="px-6 py-4 text-gray-500">{{ $user->sources_assignees_count ?: '—' }}</td> <td class="px-6 py-4 text-gray-500 dark:text-gray-400">{{ $user->sources_assignees_count ?: '—' }}</td>
<td class="px-6 py-4 text-gray-500 whitespace-nowrap"> <td class="px-6 py-4 text-gray-500 dark:text-gray-400 whitespace-nowrap">
{{ $user->created_at->format('d/m/Y') }} {{ $user->created_at->format('d/m/Y') }}
</td> </td>
<td class="px-6 py-4 text-right space-x-3"> <td class="px-6 py-4 text-right space-x-3">
@@ -157,7 +157,7 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="7" class="px-6 py-10 text-center text-gray-400"> <td colspan="7" class="px-6 py-10 text-center text-gray-400 dark:text-gray-500">
Aucun utilisateur trouvé. Aucun utilisateur trouvé.
</td> </td>
</tr> </tr>
@@ -1,5 +1,5 @@
<x-guest-layout> <x-guest-layout>
<div class="mb-4 text-sm text-gray-600"> <div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }} {{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div> </div>
@@ -1,5 +1,5 @@
<x-guest-layout> <x-guest-layout>
<div class="mb-4 text-sm text-gray-600"> <div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
</div> </div>
+3 -3
View File
@@ -27,14 +27,14 @@
<!-- Remember Me --> <!-- Remember Me -->
<div class="block mt-4"> <div class="block mt-4">
<label for="remember_me" class="inline-flex items-center"> <label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember"> <input id="remember_me" type="checkbox" class="rounded border-gray-300 dark:border-gray-600 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
<span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span> <span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
</label> </label>
</div> </div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
@if (Route::has('password.request')) @if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}"> <a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }} {{ __('Forgot your password?') }}
</a> </a>
@endif @endif
+1 -1
View File
@@ -40,7 +40,7 @@
</div> </div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}"> <a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
{{ __('Already registered?') }} {{ __('Already registered?') }}
</a> </a>
+7 -7
View File
@@ -1,20 +1,20 @@
<x-guest-layout> <x-guest-layout>
<div class="mb-6 text-center"> <div class="mb-6 text-center">
<div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-indigo-100 mb-3"> <div class="inline-flex items-center justify-center w-14 h-14 rounded-full bg-indigo-100 dark:bg-indigo-900/50 mb-3">
<svg class="w-7 h-7 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-7 h-7 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg> </svg>
</div> </div>
<h2 class="text-lg font-semibold text-gray-900">Vérification en deux étapes</h2> <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Vérification en deux étapes</h2>
<p class="text-sm text-gray-500 mt-1"> <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
Un code à 6 chiffres a été envoyé à<br> Un code à 6 chiffres a été envoyé à<br>
<span class="font-medium text-gray-700">{{ $maskedEmail }}</span> <span class="font-medium text-gray-700 dark:text-gray-300">{{ $maskedEmail }}</span>
</p> </p>
</div> </div>
@if(session('resent')) @if(session('resent'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 text-green-800 text-sm rounded-md text-center"> <div class="mb-4 p-3 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 text-sm rounded-md text-center">
Un nouveau code a été envoyé. Un nouveau code a été envoyé.
</div> </div>
@endif @endif
@@ -28,7 +28,7 @@
inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code"
autofocus required autofocus required
class="mt-1 block w-full text-center text-3xl tracking-[.5em] font-mono class="mt-1 block w-full text-center text-3xl tracking-[.5em] font-mono
border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" border-gray-300 dark:border-gray-600 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
placeholder="——————"> placeholder="——————">
<x-input-error :messages="$errors->get('pin')" class="mt-2"/> <x-input-error :messages="$errors->get('pin')" class="mt-2"/>
</div> </div>
@@ -45,7 +45,7 @@
Renvoyer le code Renvoyer le code
</button> </button>
</form> </form>
<a href="{{ route('login') }}" class="text-sm text-gray-400 hover:text-gray-600"> <a href="{{ route('login') }}" class="text-sm text-gray-400 dark:text-gray-500 hover:text-gray-600">
Retour à la connexion Retour à la connexion
</a> </a>
</div> </div>
+2 -2
View File
@@ -1,5 +1,5 @@
<x-guest-layout> <x-guest-layout>
<div class="mb-4 text-sm text-gray-600"> <div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
</div> </div>
@@ -23,7 +23,7 @@
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
@csrf @csrf
<button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
{{ __('Log Out') }} {{ __('Log Out') }}
</button> </button>
</form> </form>
+170
View File
@@ -0,0 +1,170 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Carte des relevés</h2>
<p class="text-sm text-gray-500 dark:text-gray-400" id="carte-stats"></p>
</div>
</x-slot>
@push('head')
{{-- Leaflet CSS --}}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
<style>
#carte-map {
height: calc(100vh - 120px);
min-height: 400px;
}
/* Popup dark mode */
.dark .leaflet-popup-content-wrapper,
.dark .leaflet-popup-tip {
background-color: #1f2937;
color: #f3f4f6;
}
.dark .leaflet-popup-content-wrapper {
border: 1px solid #374151;
}
/* Contrôles dark mode */
.dark .leaflet-control-zoom a,
.dark .leaflet-control-attribution {
background-color: #1f2937;
color: #d1d5db;
border-color: #374151;
}
.dark .leaflet-control-zoom a:hover {
background-color: #374151;
}
/* Tuiles légèrement assombries en dark mode */
.dark .leaflet-tile {
filter: brightness(0.7) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7);
}
</style>
@endpush
{{-- La carte occupe toute la hauteur disponible --}}
<div id="carte-map" class="w-full"></div>
@push('head')
{{-- Leaflet JS (chargé en fin de head pour ne pas bloquer le rendu) --}}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV/XN2GqM8=" crossorigin=""
defer></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// ── Initialisation de la carte ──────────────────────────────────────
const map = L.map('carte-map', {
center: [46.5, 2.2], // centre de la France
zoom: 6,
zoomControl: true,
});
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
// ── Icône personnalisée ────────────────────────────────────────────
function makeIcon(count) {
const size = count > 10 ? 40 : count > 3 ? 34 : 28;
const color = count > 10 ? '#4f46e5' : count > 3 ? '#6366f1' : '#818cf8';
return L.divIcon({
className: '',
html: `<div style="
width:${size}px; height:${size}px;
background:${color};
border:3px solid white;
border-radius:50%;
box-shadow:0 2px 6px rgba(0,0,0,.35);
display:flex; align-items:center; justify-content:center;
font-size:${size > 30 ? 13 : 11}px;
font-weight:700; color:white; font-family:sans-serif;
">${count}</div>`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
popupAnchor: [0, -(size / 2 + 4)],
});
}
// ── Statut → badge couleur ─────────────────────────────────────────
const statusColors = {
'a_faire': 'background:#f3f4f6;color:#374151',
'en_cours': 'background:#dbeafe;color:#1d4ed8',
'a_valider': 'background:#fef9c3;color:#92400e',
'termine': 'background:#d1fae5;color:#065f46',
};
// ── Chargement des données ─────────────────────────────────────────
fetch('{{ route('carte.data') }}')
.then(r => r.json())
.then(lieux => {
if (lieux.length === 0) {
document.getElementById('carte-stats').textContent =
'Aucun lieu géolocalisé avec des relevés.';
return;
}
const totalReleves = lieux.reduce((s, l) => s + l.releves_count, 0);
document.getElementById('carte-stats').textContent =
`${lieux.length} lieu${lieux.length > 1 ? 'x' : ''} · ${totalReleves.toLocaleString('fr-FR')} relevé${totalReleves > 1 ? 's' : ''}`;
const bounds = [];
lieux.forEach(lieu => {
bounds.push([lieu.lat, lieu.lng]);
// ── Contenu du popup ───────────────────────────────────
const sourcesHtml = lieu.sources.map(s => {
const style = statusColors[s.status_value] || statusColors['a_faire'];
const annees = s.annees ? ` <span style="color:#6b7280;font-size:11px">(${s.annees})</span>` : '';
return `<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:3px 0;border-bottom:1px solid #f3f4f6">
<div style="min-width:0">
<a href="/sources/${s.id}" style="color:#4f46e5;font-size:13px;font-weight:500;text-decoration:none"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">${s.nom}</a>${annees}
</div>
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
<span style="font-size:11px;${style};padding:1px 6px;border-radius:9999px;white-space:nowrap">${s.status}</span>
<span style="font-size:12px;color:#6b7280;white-space:nowrap">${s.releves_count.toLocaleString('fr-FR')} rel.</span>
</div>
</div>`;
}).join('');
const popup = `
<div style="min-width:260px;max-width:320px;font-family:sans-serif">
<div style="font-size:15px;font-weight:700;margin-bottom:6px;padding-bottom:6px;border-bottom:2px solid #e5e7eb">
📍 ${lieu.nom}
</div>
<div style="font-size:12px;color:#6b7280;margin-bottom:8px">
${lieu.sources_count} source${lieu.sources_count > 1 ? 's' : ''} ·
<strong>${lieu.releves_count.toLocaleString('fr-FR')}</strong> relevé${lieu.releves_count > 1 ? 's' : ''}
</div>
<div>${sourcesHtml}</div>
<div style="margin-top:8px;text-align:right">
<a href="/recherche?lieu_id=${lieu.id}"
style="font-size:12px;color:#4f46e5;text-decoration:none"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">
Rechercher dans ces relevés
</a>
</div>
</div>`;
L.marker([lieu.lat, lieu.lng], { icon: makeIcon(lieu.sources_count) })
.addTo(map)
.bindPopup(popup, { maxWidth: 340 });
});
// Ajuster la vue sur tous les marqueurs
if (bounds.length > 0) {
map.fitBounds(bounds, { padding: [40, 40], maxZoom: 12 });
}
})
.catch(() => {
document.getElementById('carte-stats').textContent = 'Erreur lors du chargement des données.';
});
});
</script>
@endpush
</x-app-layout>
@@ -1 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a> <a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>
@@ -1,4 +1,4 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white']) @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-800'])
@php @php
$alignmentClasses = match ($align) { $alignmentClasses = match ($align) {
@@ -28,7 +28,7 @@ $width = match ($width) {
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}" class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;" style="display: none;"
@click="open = false"> @click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}"> <div class="rounded-md ring-1 ring-black ring-opacity-5 dark:ring-gray-700 {{ $contentClasses }}">
{{ $content }} {{ $content }}
</div> </div>
</div> </div>
@@ -1,5 +1,5 @@
@props(['value']) @props(['value'])
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}> <label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700 dark:text-gray-300']) }}>
{{ $value ?? $slot }} {{ $value ?? $slot }}
</label> </label>
@@ -69,7 +69,7 @@
}" }"
@keydown.escape.window="open = false" @keydown.escape.window="open = false"
> >
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{{ $label }} {{ $label }}
@if($required) <span class="text-red-500">*</span> @endif @if($required) <span class="text-red-500">*</span> @endif
</label> </label>
@@ -82,12 +82,17 @@
<button <button
type="button" type="button"
@click="openModal()" @click="openModal()"
class="flex-1 text-left px-3 py-2 border border-gray-300 rounded-md bg-white text-sm shadow-sm class="flex-1 text-left px-3 py-2 border border-gray-300 dark:border-gray-600
hover:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors rounded-md bg-white dark:bg-gray-700 text-sm shadow-sm
hover:border-indigo-400 dark:hover:border-indigo-500
focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors
@error($name) border-red-500 @enderror" @error($name) border-red-500 @enderror"
> >
<span x-show="selected.id" class="text-gray-900" x-text="selected.name"></span> <span x-show="selected.id"
<span x-show="!selected.id" class="text-gray-400">{{ $placeholder }}</span> class="text-gray-900 dark:text-gray-100"
x-text="selected.name"></span>
<span x-show="!selected.id"
class="text-gray-400 dark:text-gray-500">{{ $placeholder }}</span>
</button> </button>
<button <button
@@ -95,7 +100,7 @@
x-show="selected.id" x-show="selected.id"
@click="clear()" @click="clear()"
title="Effacer" title="Effacer"
class="px-2 py-2 text-gray-400 hover:text-red-500 transition-colors" class="px-2 py-2 text-gray-400 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition-colors"
> >
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
@@ -104,7 +109,7 @@
</div> </div>
@error($name) @error($name)
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
{{-- Modale de recherche --}} {{-- Modale de recherche --}}
@@ -115,13 +120,15 @@
@click.self="open = false" @click.self="open = false"
> >
{{-- Fond semi-transparent --}} {{-- Fond semi-transparent --}}
<div class="absolute inset-0 bg-black/40" @click="open = false"></div> <div class="absolute inset-0 bg-black/40 dark:bg-black/60" @click="open = false"></div>
{{-- Panneau --}} {{-- Panneau --}}
<div class="relative bg-white rounded-xl shadow-2xl w-full max-w-lg z-10 overflow-hidden"> <div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl dark:shadow-gray-900/50
{{-- En-tête --}} w-full max-w-lg z-10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-100 flex items-center gap-3">
<svg class="w-5 h-5 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> {{-- En-tête avec champ de recherche --}}
<div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700 flex items-center gap-3">
<svg class="w-5 h-5 text-gray-400 dark:text-gray-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/> d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
</svg> </svg>
@@ -131,10 +138,13 @@
x-model="search" x-model="search"
@input="onSearchInput()" @input="onSearchInput()"
placeholder="Nom, code INSEE…" placeholder="Nom, code INSEE…"
class="flex-1 text-sm outline-none placeholder-gray-400" class="flex-1 text-sm outline-none bg-transparent
text-gray-900 dark:text-gray-100
placeholder-gray-400 dark:placeholder-gray-500
focus:ring-0 border-none p-0"
> >
<button type="button" @click="open = false" <button type="button" @click="open = false"
class="text-gray-400 hover:text-gray-600"> class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg> </svg>
@@ -144,39 +154,44 @@
{{-- Résultats --}} {{-- Résultats --}}
<div class="max-h-80 overflow-y-auto"> <div class="max-h-80 overflow-y-auto">
{{-- Chargement --}} {{-- Chargement --}}
<div x-show="loading" class="px-4 py-6 text-center text-sm text-gray-400"> <div x-show="loading"
class="px-4 py-6 text-center text-sm text-gray-400 dark:text-gray-500">
Recherche… Recherche…
</div> </div>
{{-- Aucun résultat --}} {{-- Aucun résultat --}}
<div x-show="!loading && search.length > 0 && results.length === 0" <div x-show="!loading && search.length > 0 && results.length === 0"
class="px-4 py-6 text-center text-sm text-gray-400"> class="px-4 py-6 text-center text-sm text-gray-400 dark:text-gray-500">
Aucun lieu trouvé pour « <span x-text="search"></span> » Aucun lieu trouvé pour « <span x-text="search"></span> »
</div> </div>
{{-- Invite initiale --}} {{-- Invite initiale --}}
<div x-show="!loading && search.length === 0" <div x-show="!loading && search.length === 0"
class="px-4 py-6 text-center text-sm text-gray-400"> class="px-4 py-6 text-center text-sm text-gray-400 dark:text-gray-500">
Saisissez au moins une lettre pour rechercher Saisissez au moins une lettre pour rechercher
</div> </div>
{{-- Liste --}} {{-- Liste des résultats --}}
<ul x-show="results.length > 0"> <ul x-show="results.length > 0">
<template x-for="lieu in results" :key="lieu.id"> <template x-for="lieu in results" :key="lieu.id">
<li> <li>
<button <button
type="button" type="button"
@click="select(lieu)" @click="select(lieu)"
class="w-full text-left px-4 py-3 hover:bg-indigo-50 flex items-center justify-between gap-3 transition-colors" class="w-full text-left px-4 py-3 flex items-center justify-between gap-3 transition-colors
hover:bg-indigo-50 dark:hover:bg-indigo-900/30"
> >
<div> <div>
<span class="text-sm font-medium text-gray-900" x-text="lieu.nom_long"></span> <span class="text-sm font-medium text-gray-900 dark:text-gray-100"
x-text="lieu.nom_long"></span>
<span x-show="lieu.code" <span x-show="lieu.code"
class="ml-2 text-xs text-gray-400" class="ml-2 text-xs text-gray-400 dark:text-gray-500"
x-text="lieu.code"></span> x-text="lieu.code"></span>
</div> </div>
<span x-show="lieu.type" <span x-show="lieu.type"
class="shrink-0 text-xs px-2 py-0.5 bg-gray-100 text-gray-500 rounded-full" class="shrink-0 text-xs px-2 py-0.5 rounded-full
bg-gray-100 dark:bg-gray-700
text-gray-500 dark:text-gray-400"
x-text="lieu.type"></span> x-text="lieu.type"></span>
</button> </button>
</li> </li>
@@ -186,9 +201,9 @@
@can('create', App\Models\Lieu::class) @can('create', App\Models\Lieu::class)
{{-- Pied : créer un nouveau lieu --}} {{-- Pied : créer un nouveau lieu --}}
<div class="border-t border-gray-100 px-4 py-2.5"> <div class="border-t border-gray-100 dark:border-gray-700 px-4 py-2.5">
<a href="{{ route('lieux.create') }}" target="_blank" <a href="{{ route('lieux.create') }}" target="_blank"
class="text-xs text-indigo-600 hover:underline"> class="text-xs text-indigo-600 dark:text-indigo-400 hover:underline">
+ Créer un nouveau lieu + Créer un nouveau lieu
</a> </a>
</div> </div>
+2 -4
View File
@@ -18,10 +18,8 @@ $maxWidth = [
x-data="{ x-data="{
show: @js($show), show: @js($show),
focusables() { focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])' let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)] return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => ! el.hasAttribute('disabled')) .filter(el => ! el.hasAttribute('disabled'))
}, },
firstFocusable() { return this.focusables()[0] }, firstFocusable() { return this.focusables()[0] },
@@ -60,12 +58,12 @@ $maxWidth = [
x-transition:leave-start="opacity-100" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0" x-transition:leave-end="opacity-0"
> >
<div class="absolute inset-0 bg-gray-500 opacity-75"></div> <div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
</div> </div>
<div <div
x-show="show" x-show="show"
class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto" class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl dark:shadow-gray-900/50 transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300" x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100" x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
@@ -2,8 +2,8 @@
@php @php
$classes = ($active ?? false) $classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 dark:text-white focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-700 dark:focus:text-gray-200 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out';
@endphp @endphp
<a {{ $attributes->merge(['class' => $classes]) }}> <a {{ $attributes->merge(['class' => $classes]) }}>
@@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}> <button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-gray-300 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150']) }}>
{{ $slot }} {{ $slot }}
</button> </button>
@@ -2,8 +2,8 @@
@php @php
$classes = ($active ?? false) $classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/30 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp @endphp
<a {{ $attributes->merge(['class' => $classes]) }}> <a {{ $attributes->merge(['class' => $classes]) }}>
@@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}> <button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }} {{ $slot }}
</button> </button>
@@ -1,3 +1,3 @@
@props(['disabled' => false]) @props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}> <input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:placeholder-gray-400 focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-indigo-500 dark:focus:ring-indigo-400 rounded-md shadow-sm']) }}>
@@ -0,0 +1,159 @@
{{--
Sélecteur d'utilisateur avec recherche (fenêtre modale).
Props :
name nom du champ hidden (défaut : user_id)
users collection/array d'objets avec {id, name, email}
placeholder texte affiché avant sélection
required ajoute l'attribut required sur le champ hidden (pour validation formulaire)
--}}
@props([
'name' => 'user_id',
'users' => [],
'placeholder' => 'Sélectionner un utilisateur…',
'required' => false,
])
@php
$usersJson = collect($users)->map(fn ($u) => [
'id' => $u->id,
'name' => $u->name,
'email' => $u->email,
])->values()->toJson();
@endphp
<div x-data="{
open: false,
search: '',
selectedId: null,
selectedLabel: '',
users: {{ $usersJson }},
get filtered() {
if (! this.search.trim()) return this.users;
const q = this.search.toLowerCase();
return this.users.filter(u =>
u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
);
},
select(u) {
this.selectedId = u.id;
this.selectedLabel = u.name + ' (' + u.email + ')';
this.open = false;
this.search = '';
},
clear() {
this.selectedId = null;
this.selectedLabel = '';
}
}" @keydown.escape.window="open = false">
<input type="hidden" name="{{ $name }}" :value="selectedId" {{ $required ? 'required' : '' }}>
{{-- Bouton déclencheur --}}
<button type="button" @click="open = true"
class="w-full flex items-center justify-between gap-2 px-3 py-2
bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600
rounded-md shadow-sm text-sm text-left
hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 dark:focus:ring-offset-gray-800">
<span :class="selectedId ? 'text-gray-900 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500'"
x-text="selectedLabel || '{{ $placeholder }}'"></span>
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</button>
{{-- Fenêtre modale --}}
<div x-show="open" x-cloak
class="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
{{-- Fond semi-transparent --}}
<div class="absolute inset-0 bg-gray-900/50 dark:bg-gray-950/70" @click="open = false"></div>
{{-- Panneau --}}
<div class="relative w-full max-w-md bg-white dark:bg-gray-800 rounded-xl shadow-xl dark:shadow-gray-900/50 overflow-hidden"
@click.stop
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0">
{{-- Champ de recherche --}}
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<svg class="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="text" x-model="search" x-ref="searchInput"
x-init="$watch('open', v => v && $nextTick(() => $refs.searchInput && $refs.searchInput.focus()))"
placeholder="Rechercher par nom ou email…"
class="flex-1 bg-transparent border-none outline-none p-0 text-sm
text-gray-900 dark:text-gray-100
placeholder-gray-400 dark:placeholder-gray-500
focus:ring-0">
<button type="button" @click="open = false"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
{{-- Liste des utilisateurs --}}
<div class="overflow-y-auto" style="max-height: 18rem;">
<template x-if="filtered.length === 0">
<p class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500">
Aucun résultat pour « <span x-text="search"></span> »
</p>
</template>
<template x-for="u in filtered" :key="u.id">
<button type="button" @click="select(u)"
class="w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors
hover:bg-gray-50 dark:hover:bg-gray-700"
:class="selectedId === u.id
? 'bg-indigo-50 dark:bg-indigo-900/30'
: ''">
{{-- Avatar initiale --}}
<div class="w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-900/50
flex items-center justify-center shrink-0">
<span class="text-xs font-semibold text-indigo-700 dark:text-indigo-300"
x-text="u.name.charAt(0).toUpperCase()"></span>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate"
x-text="u.name"></p>
<p class="text-xs text-gray-500 dark:text-gray-400 truncate"
x-text="u.email"></p>
</div>
<template x-if="selectedId === u.id">
<svg class="w-4 h-4 text-indigo-600 dark:text-indigo-400 shrink-0"
fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"/>
</svg>
</template>
</button>
</template>
</div>
{{-- Pied : compteur --}}
<div class="px-4 py-2 border-t border-gray-100 dark:border-gray-700
text-xs text-gray-400 dark:text-gray-500 flex items-center justify-between">
<span>
<span x-text="filtered.length"></span> /
<span x-text="users.length"></span> utilisateur<span x-show="users.length !== 1">s</span>
</span>
<button x-show="selectedId" type="button" @click="clear()"
class="text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors">
Effacer la sélection
</button>
</div>
</div>
</div>
</div>
+47 -47
View File
@@ -1,6 +1,6 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold text-gray-800">Tableau de bord</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Tableau de bord</h2>
</x-slot> </x-slot>
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
@@ -9,9 +9,9 @@
{{-- Bloc admin : lien vers le dashboard admin --}} {{-- Bloc admin : lien vers le dashboard admin --}}
@if($user->isAdmin()) @if($user->isAdmin())
<div class="bg-indigo-50 border border-indigo-200 rounded-xl p-5 flex items-center justify-between"> <div class="bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-700 rounded-xl p-5 flex items-center justify-between">
<div> <div>
<p class="text-sm font-semibold text-indigo-800">Accès administrateur</p> <p class="text-sm font-semibold text-indigo-800 dark:text-indigo-200">Accès administrateur</p>
<p class="text-xs text-indigo-600 mt-0.5">Statistiques globales, gestion des utilisateurs et des sections.</p> <p class="text-xs text-indigo-600 mt-0.5">Statistiques globales, gestion des utilisateurs et des sections.</p>
</div> </div>
<a href="{{ route('admin.dashboard') }}" <a href="{{ route('admin.dashboard') }}"
@@ -26,10 +26,10 @@
@foreach($sectionsStats as $stat) @foreach($sectionsStats as $stat)
@php @php
$statusColors = [ $statusColors = [
'a_faire' => ['bg' => 'bg-gray-100', 'text' => 'text-gray-700'], 'a_faire' => ['bg' => 'bg-gray-100 dark:bg-gray-700', 'text' => 'text-gray-700 dark:text-gray-300'],
'en_cours' => ['bg' => 'bg-blue-100', 'text' => 'text-blue-700'], 'en_cours' => ['bg' => 'bg-blue-100 dark:bg-blue-900/50', 'text' => 'text-blue-700'],
'a_valider' => ['bg' => 'bg-yellow-100', 'text' => 'text-yellow-700'], 'a_valider' => ['bg' => 'bg-yellow-100 dark:bg-yellow-900/50', 'text' => 'text-yellow-700'],
'termine' => ['bg' => 'bg-green-100', 'text' => 'text-green-700'], 'termine' => ['bg' => 'bg-green-100 dark:bg-green-900/50', 'text' => 'text-green-700'],
]; ];
$statusLabels = [ $statusLabels = [
'a_faire' => 'À faire', 'a_faire' => 'À faire',
@@ -39,17 +39,17 @@
]; ];
@endphp @endphp
<div class="space-y-4"> <div class="space-y-4">
<h3 class="text-base font-semibold text-gray-800 flex items-center gap-2"> <h3 class="text-base font-semibold text-gray-800 dark:text-gray-200 flex items-center gap-2">
<span>Section {{ $stat['section']->nom }}</span> <span>Section {{ $stat['section']->nom }}</span>
@if($user->isManagerOfSection($stat['section'])) @if($user->isManagerOfSection($stat['section']))
<span class="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded-full">Responsable</span> <span class="text-xs px-2 py-0.5 bg-blue-100 dark:bg-blue-900/50 text-blue-700 rounded-full">Responsable</span>
@endif @endif
</h3> </h3>
{{-- Compteurs par statut --}} {{-- Compteurs par statut --}}
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
@foreach($stat['by_status'] as $statusVal => $count) @foreach($stat['by_status'] as $statusVal => $count)
@php $c = $statusColors[$statusVal] ?? ['bg' => 'bg-gray-100', 'text' => 'text-gray-700']; @endphp @php $c = $statusColors[$statusVal] ?? ['bg' => 'bg-gray-100 dark:bg-gray-700', 'text' => 'text-gray-700 dark:text-gray-300']; @endphp
<a href="{{ route('sources.index', ['status' => $statusVal]) }}" <a href="{{ route('sources.index', ['status' => $statusVal]) }}"
class="rounded-xl border p-4 flex flex-col gap-1 hover:shadow-md transition-shadow class="rounded-xl border p-4 flex flex-col gap-1 hover:shadow-md transition-shadow
{{ $c['bg'] }} border-transparent"> {{ $c['bg'] }} border-transparent">
@@ -62,38 +62,38 @@
{{-- Métriques globales section --}} {{-- Métriques globales section --}}
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="bg-white border border-gray-200 rounded-xl px-5 py-4 flex items-center gap-4"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl px-5 py-4 flex items-center gap-4">
<div class="p-2 bg-indigo-100 rounded-lg"> <div class="p-2 bg-indigo-100 dark:bg-indigo-900/50 rounded-lg">
<svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8"/>
</svg> </svg>
</div> </div>
<div> <div>
<p class="text-xl font-bold text-gray-900">{{ $stat['total_sources'] }}</p> <p class="text-xl font-bold text-gray-900 dark:text-white">{{ $stat['total_sources'] }}</p>
<p class="text-xs text-gray-500">source{{ $stat['total_sources'] > 1 ? 's' : '' }} au total</p> <p class="text-xs text-gray-500 dark:text-gray-400">source{{ $stat['total_sources'] > 1 ? 's' : '' }} au total</p>
</div> </div>
</div> </div>
<div class="bg-white border border-gray-200 rounded-xl px-5 py-4 flex items-center gap-4"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl px-5 py-4 flex items-center gap-4">
<div class="p-2 bg-green-100 rounded-lg"> <div class="p-2 bg-green-100 dark:bg-green-900/50 rounded-lg">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg> </svg>
</div> </div>
<div> <div>
<p class="text-xl font-bold text-gray-900">{{ number_format($stat['total_releves']) }}</p> <p class="text-xl font-bold text-gray-900 dark:text-white">{{ number_format($stat['total_releves']) }}</p>
<p class="text-xs text-gray-500">relevé{{ $stat['total_releves'] > 1 ? 's' : '' }} saisi{{ $stat['total_releves'] > 1 ? 's' : '' }}</p> <p class="text-xs text-gray-500 dark:text-gray-400">relevé{{ $stat['total_releves'] > 1 ? 's' : '' }} saisi{{ $stat['total_releves'] > 1 ? 's' : '' }}</p>
</div> </div>
</div> </div>
</div> </div>
{{-- Sources récentes de la section --}} {{-- Sources récentes de la section --}}
@if($stat['sources_recentes']->isNotEmpty()) @if($stat['sources_recentes']->isNotEmpty())
<div class="bg-white border border-gray-200 rounded-xl p-5"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-5">
<p class="text-xs font-semibold text-gray-500 uppercase mb-3">Sources récentes</p> <p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-3">Sources récentes</p>
<div class="divide-y divide-gray-100"> <div class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($stat['sources_recentes'] as $src) @foreach($stat['sources_recentes'] as $src)
@php @php
$sc = $statusColors[$src->status->value] ?? ['bg' => 'bg-gray-100', 'text' => 'text-gray-600']; $sc = $statusColors[$src->status->value] ?? ['bg' => 'bg-gray-100 dark:bg-gray-700', 'text' => 'text-gray-600 dark:text-gray-400'];
@endphp @endphp
<div class="flex items-center justify-between py-2.5"> <div class="flex items-center justify-between py-2.5">
<div class="min-w-0"> <div class="min-w-0">
@@ -101,7 +101,7 @@
class="text-sm font-medium text-indigo-600 hover:underline truncate block"> class="text-sm font-medium text-indigo-600 hover:underline truncate block">
{{ $src->nom }} {{ $src->nom }}
</a> </a>
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400 dark:text-gray-500">
{{ $src->sourceType->nom }} · {{ $src->releves_count }} relevé{{ $src->releves_count > 1 ? 's' : '' }} {{ $src->sourceType->nom }} · {{ $src->releves_count }} relevé{{ $src->releves_count > 1 ? 's' : '' }}
</p> </p>
</div> </div>
@@ -119,45 +119,45 @@
{{-- ── Mes sources assignées ────────────────────────────────────────── --}} {{-- ── Mes sources assignées ────────────────────────────────────────── --}}
@if($mesSources->isNotEmpty()) @if($mesSources->isNotEmpty())
<div class="bg-white border border-gray-200 rounded-xl p-6"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Mes sources assignées</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Mes sources assignées</h3>
<a href="{{ route('sources.index') }}" class="text-xs text-indigo-600 hover:underline">Voir toutes</a> <a href="{{ route('sources.index') }}" class="text-xs text-indigo-600 hover:underline">Voir toutes</a>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full text-sm divide-y divide-gray-100"> <table class="min-w-full text-sm divide-y divide-gray-100 dark:divide-gray-700">
<thead> <thead>
<tr> <tr>
<th class="pb-2 text-left text-xs font-medium text-gray-500 uppercase">Source</th> <th class="pb-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Source</th>
<th class="pb-2 text-left text-xs font-medium text-gray-500 uppercase">Type</th> <th class="pb-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Type</th>
<th class="pb-2 text-left text-xs font-medium text-gray-500 uppercase">Statut</th> <th class="pb-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Statut</th>
<th class="pb-2 text-left text-xs font-medium text-gray-500 uppercase">Relevés</th> <th class="pb-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Relevés</th>
<th class="pb-2"></th> <th class="pb-2"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-50"> <tbody class="divide-y divide-gray-50 dark:divide-gray-700">
@foreach($mesSources as $source) @foreach($mesSources as $source)
@php @php
$colors = [ $colors = [
'a_faire' => 'bg-gray-100 text-gray-600', 'a_faire' => 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400',
'en_cours' => 'bg-blue-100 text-blue-700', 'en_cours' => 'bg-blue-100 dark:bg-blue-900/50 text-blue-700',
'a_valider' => 'bg-yellow-100 text-yellow-700', 'a_valider' => 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700',
'termine' => 'bg-green-100 text-green-700', 'termine' => 'bg-green-100 dark:bg-green-900/50 text-green-700',
]; ];
$c = $colors[$source->status->value] ?? 'bg-gray-100 text-gray-600'; $c = $colors[$source->status->value] ?? 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
@endphp @endphp
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="py-2.5 pr-4"> <td class="py-2.5 pr-4">
<a href="{{ route('sources.show', $source) }}" <a href="{{ route('sources.show', $source) }}"
class="font-medium text-indigo-600 hover:underline">{{ $source->nom }}</a> class="font-medium text-indigo-600 hover:underline">{{ $source->nom }}</a>
</td> </td>
<td class="py-2.5 pr-4 text-gray-500">{{ $source->sourceType->nom }}</td> <td class="py-2.5 pr-4 text-gray-500 dark:text-gray-400">{{ $source->sourceType->nom }}</td>
<td class="py-2.5 pr-4"> <td class="py-2.5 pr-4">
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium {{ $c }}"> <span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium {{ $c }}">
{{ $source->status->label() }} {{ $source->status->label() }}
</span> </span>
</td> </td>
<td class="py-2.5 pr-4 text-gray-500">{{ $source->releves_count }}</td> <td class="py-2.5 pr-4 text-gray-500 dark:text-gray-400">{{ $source->releves_count }}</td>
<td class="py-2.5 text-right"> <td class="py-2.5 text-right">
<a href="{{ route('sources.releves.index', $source) }}" <a href="{{ route('sources.releves.index', $source) }}"
class="text-xs text-indigo-600 hover:underline">Saisir </a> class="text-xs text-indigo-600 hover:underline">Saisir </a>
@@ -169,8 +169,8 @@
</div> </div>
</div> </div>
@elseif(! $sectionsStats || $sectionsStats->isEmpty()) @elseif(! $sectionsStats || $sectionsStats->isEmpty())
<div class="bg-white border border-gray-200 rounded-xl p-10 text-center text-gray-400"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-10 text-center text-gray-400 dark:text-gray-500">
<svg class="mx-auto w-10 h-10 mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto w-10 h-10 mb-3 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8"/> d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8"/>
</svg> </svg>
@@ -183,22 +183,22 @@
{{-- ── Mes derniers relevés ─────────────────────────────────────────── --}} {{-- ── Mes derniers relevés ─────────────────────────────────────────── --}}
@if($mesReleves->isNotEmpty()) @if($mesReleves->isNotEmpty())
<div class="bg-white border border-gray-200 rounded-xl p-6"> <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-gray-700 uppercase tracking-wide">Mes derniers relevés</h3> <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Mes derniers relevés</h3>
<a href="{{ route('recherche') }}" class="text-xs text-indigo-600 hover:underline">Recherche</a> <a href="{{ route('recherche') }}" class="text-xs text-indigo-600 hover:underline">Recherche</a>
</div> </div>
<div class="space-y-0 divide-y divide-gray-100"> <div class="space-y-0 divide-y divide-gray-100 dark:divide-gray-700">
@foreach($mesReleves as $releve) @foreach($mesReleves as $releve)
<div class="flex items-center justify-between py-2.5"> <div class="flex items-center justify-between py-2.5">
<div class="min-w-0"> <div class="min-w-0">
<a href="{{ route('releves.show', $releve) }}" <a href="{{ route('releves.show', $releve) }}"
class="text-sm font-medium text-gray-900 hover:text-indigo-600"> class="text-sm font-medium text-gray-900 dark:text-white hover:text-indigo-600">
{{ $releve->nom ?? '—' }}{{ $releve->prenom ? ', ' . $releve->prenom : '' }} {{ $releve->nom ?? '—' }}{{ $releve->prenom ? ', ' . $releve->prenom : '' }}
</a> </a>
<p class="text-xs text-gray-400">{{ $releve->source->nom }}</p> <p class="text-xs text-gray-400 dark:text-gray-500">{{ $releve->source->nom }}</p>
</div> </div>
<span class="text-xs text-gray-400 whitespace-nowrap ml-4"> <span class="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap ml-4">
{{ $releve->created_at->diffForHumans() }} {{ $releve->created_at->diffForHumans() }}
</span> </span>
</div> </div>
+19 -4
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -11,20 +11,35 @@
<link rel="icon" href="{{ $siteLogoUrl }}"> <link rel="icon" href="{{ $siteLogoUrl }}">
@endif @endif
<!-- Dark mode init must run before first paint to avoid flash -->
<script>
(function () {
var t = localStorage.getItem('colorTheme') || 'auto';
var dark = t === 'dark' || (t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
if ((localStorage.getItem('colorTheme') || 'auto') === 'auto') {
document.documentElement.classList.toggle('dark', e.matches);
}
});
})();
</script>
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net"> <link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts --> <!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js']) @vite(['resources/css/app.css', 'resources/js/app.js'])
@stack('head')
</head> </head>
<body class="font-sans antialiased"> <body class="font-sans antialiased bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<div class="min-h-screen bg-gray-100"> <div class="min-h-screen">
@include('layouts.navigation') @include('layouts.navigation')
<!-- Page Heading --> <!-- Page Heading -->
@isset($header) @isset($header)
<header class="bg-white shadow"> <header class="bg-white dark:bg-gray-800 shadow dark:shadow-gray-900/50">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }} {{ $header }}
</div> </div>
+19 -5
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -11,6 +11,20 @@
<link rel="icon" href="{{ $siteLogoUrl }}"> <link rel="icon" href="{{ $siteLogoUrl }}">
@endif @endif
<!-- Dark mode init -->
<script>
(function () {
var t = localStorage.getItem('colorTheme') || 'auto';
var dark = t === 'dark' || (t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
if ((localStorage.getItem('colorTheme') || 'auto') === 'auto') {
document.documentElement.classList.toggle('dark', e.matches);
}
});
})();
</script>
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net"> <link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
@@ -18,19 +32,19 @@
<!-- Scripts --> <!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js']) @vite(['resources/css/app.css', 'resources/js/app.js'])
</head> </head>
<body class="font-sans text-gray-900 antialiased"> <body class="font-sans text-gray-900 dark:text-gray-100 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100"> <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div class="mb-2"> <div class="mb-2">
<a href="/"> <a href="/">
@if($siteLogoUrl) @if($siteLogoUrl)
<img src="{{ $siteLogoUrl }}" alt="{{ config('app.name') }}" class="h-16 w-auto object-contain"> <img src="{{ $siteLogoUrl }}" alt="{{ config('app.name') }}" class="h-16 w-auto object-contain">
@else @else
<x-application-logo class="w-16 h-16 fill-current text-gray-400" /> <x-application-logo class="w-16 h-16 fill-current text-gray-400 dark:text-gray-500" />
@endif @endif
</a> </a>
</div> </div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg"> <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md dark:shadow-gray-900/50 overflow-hidden sm:rounded-lg">
{{ $slot }} {{ $slot }}
</div> </div>
</div> </div>
+105 -19
View File
@@ -1,8 +1,8 @@
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100"> <nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16"> <div class="flex justify-between h-16">
<div class="flex"> <div class="flex">
<!-- Logo max-h contraint sur l'image directement, sans chaîne h-full --> <!-- Logo -->
<div class="shrink-0 flex items-center"> <div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}" class="flex items-center"> <a href="{{ route('dashboard') }}" class="flex items-center">
@if($siteLogoUrl) @if($siteLogoUrl)
@@ -10,7 +10,7 @@
class="block w-auto object-contain" class="block w-auto object-contain"
style="max-height: 40px; max-width: 200px;"> style="max-height: 40px; max-width: 200px;">
@else @else
<span class="font-semibold text-gray-800 text-lg">{{ config('app.name') }}</span> <span class="font-semibold text-gray-800 dark:text-gray-200 text-lg">{{ config('app.name') }}</span>
@endif @endif
</a> </a>
</div> </div>
@@ -33,13 +33,16 @@
Recherche Recherche
</x-nav-link> </x-nav-link>
<x-nav-link :href="route('carte')" :active="request()->routeIs('carte*')">
Carte
</x-nav-link>
@if(auth()->user()->isSectionManager()) @if(auth()->user()->isSectionManager())
<!-- Menu Administration utilise le composant x-dropdown (même positionnement que le menu utilisateur) -->
<div class="hidden sm:flex sm:items-center"> <div class="hidden sm:flex sm:items-center">
<x-dropdown align="left" width="w-56"> <x-dropdown align="left" width="w-56">
<x-slot name="trigger"> <x-slot name="trigger">
<button class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out <button class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out
{{ request()->routeIs('admin.*') ? 'border-indigo-400 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}"> {{ request()->routeIs('admin.*') ? 'border-indigo-400 text-gray-900 dark:text-white' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600' }}">
Administration Administration
<svg class="ms-1 h-4 w-4 fill-current" viewBox="0 0 20 20"> <svg class="ms-1 h-4 w-4 fill-current" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
@@ -70,7 +73,7 @@
<x-dropdown-link :href="route('admin.lieu-types.index')"> <x-dropdown-link :href="route('admin.lieu-types.index')">
Types de lieux Types de lieux
</x-dropdown-link> </x-dropdown-link>
<div class="border-t border-gray-100 my-1"></div> <div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
<x-dropdown-link :href="route('admin.parametres')"> <x-dropdown-link :href="route('admin.parametres')">
Paramètres du site Paramètres du site
</x-dropdown-link> </x-dropdown-link>
@@ -82,11 +85,53 @@
</div> </div>
</div> </div>
<!-- Cloche notifications --> <!-- Right side: theme toggle + notifications + user menu -->
<div class="hidden sm:flex sm:items-center sm:ms-4"> <div class="hidden sm:flex sm:items-center sm:gap-1">
<!-- Sélecteur de thème -->
<div x-data="{
theme: localStorage.getItem('colorTheme') || 'auto',
apply(t) {
this.theme = t;
localStorage.setItem('colorTheme', t);
var dark = t === 'dark' || (t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
}
}" class="flex items-center">
<div class="flex items-center bg-gray-100 dark:bg-gray-700 rounded-full p-0.5 gap-0.5">
<!-- Clair -->
<button @click="apply('light')"
:class="theme === 'light' ? 'bg-white dark:bg-gray-600 shadow text-gray-800 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'"
class="p-1.5 rounded-full transition-all" title="Mode clair">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M12 7a5 5 0 100 10A5 5 0 0012 7z"/>
</svg>
</button>
<!-- Automatique -->
<button @click="apply('auto')"
:class="theme === 'auto' ? 'bg-white dark:bg-gray-600 shadow text-gray-800 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'"
class="p-1.5 rounded-full transition-all" title="Automatique (système)">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</button>
<!-- Sombre -->
<button @click="apply('dark')"
:class="theme === 'dark' ? 'bg-white dark:bg-gray-600 shadow text-gray-800 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'"
class="p-1.5 rounded-full transition-all" title="Mode sombre">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
</svg>
</button>
</div>
</div>
<!-- Notifications -->
@php $unreadCount = auth()->user()->unreadNotifications->count(); @endphp @php $unreadCount = auth()->user()->unreadNotifications->count(); @endphp
<a href="{{ route('notifications.index') }}" <a href="{{ route('notifications.index') }}"
class="relative p-2 text-gray-500 hover:text-indigo-600 transition-colors" class="relative p-2 text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
title="Notifications"> title="Notifications">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -98,13 +143,11 @@
</span> </span>
@endif @endif
</a> </a>
</div>
<!-- Menu utilisateur (Profil / Déconnexion) --> <!-- Menu utilisateur -->
<div class="hidden sm:flex sm:items-center sm:ms-2">
<x-dropdown align="right" width="48"> <x-dropdown align="right" width="48">
<x-slot name="trigger"> <x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"> <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div> <div>{{ Auth::user()->name }}</div>
<div class="ms-1"> <div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
@@ -115,7 +158,7 @@
</x-slot> </x-slot>
<x-slot name="content"> <x-slot name="content">
<div class="px-4 py-2 text-xs text-gray-400 border-b border-gray-100"> <div class="px-4 py-2 text-xs text-gray-400 dark:text-gray-500 border-b border-gray-100 dark:border-gray-700">
{{ Auth::user()->role->label() }} {{ Auth::user()->role->label() }}
</div> </div>
<x-dropdown-link :href="route('profile.edit')"> <x-dropdown-link :href="route('profile.edit')">
@@ -134,7 +177,7 @@
<!-- Hamburger (mobile) --> <!-- Hamburger (mobile) -->
<div class="-me-2 flex items-center sm:hidden"> <div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none transition duration-150 ease-in-out"> <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
@@ -159,6 +202,9 @@
<x-responsive-nav-link :href="route('recherche')" :active="request()->routeIs('recherche')"> <x-responsive-nav-link :href="route('recherche')" :active="request()->routeIs('recherche')">
Recherche Recherche
</x-responsive-nav-link> </x-responsive-nav-link>
<x-responsive-nav-link :href="route('carte')" :active="request()->routeIs('carte*')">
Carte
</x-responsive-nav-link>
@if(auth()->user()->isSectionManager()) @if(auth()->user()->isSectionManager())
@if(auth()->user()->isAdmin()) @if(auth()->user()->isAdmin())
<x-responsive-nav-link :href="route('admin.dashboard')" :active="request()->routeIs('admin.dashboard')"> <x-responsive-nav-link :href="route('admin.dashboard')" :active="request()->routeIs('admin.dashboard')">
@@ -189,12 +235,52 @@
</div> </div>
<!-- Options utilisateur (mobile) --> <!-- Options utilisateur (mobile) -->
<div class="pt-4 pb-1 border-t border-gray-200"> <div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
<div class="px-4"> <div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div> <div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div> <div class="font-medium text-sm text-gray-500 dark:text-gray-400">{{ Auth::user()->email }}</div>
<div class="text-xs text-gray-400">{{ Auth::user()->role->label() }}</div> <div class="text-xs text-gray-400 dark:text-gray-500">{{ Auth::user()->role->label() }}</div>
</div> </div>
<!-- Sélecteur de thème mobile -->
<div x-data="{
theme: localStorage.getItem('colorTheme') || 'auto',
apply(t) {
this.theme = t;
localStorage.setItem('colorTheme', t);
var dark = t === 'dark' || (t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
}
}" class="px-4 pt-3 pb-1">
<p class="text-xs text-gray-400 dark:text-gray-500 mb-2">Apparence</p>
<div class="flex gap-2">
<button @click="apply('light')"
:class="theme === 'light' ? 'bg-indigo-50 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 border-indigo-300 dark:border-indigo-600' : 'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400'"
class="flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs border rounded-md transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M12 7a5 5 0 100 10A5 5 0 0012 7z"/>
</svg>
Clair
</button>
<button @click="apply('auto')"
:class="theme === 'auto' ? 'bg-indigo-50 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 border-indigo-300 dark:border-indigo-600' : 'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400'"
class="flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs border rounded-md transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Auto
</button>
<button @click="apply('dark')"
:class="theme === 'dark' ? 'bg-indigo-50 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 border-indigo-300 dark:border-indigo-600' : 'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400'"
class="flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs border rounded-md transition-colors">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
</svg>
Sombre
</button>
</div>
</div>
<div class="mt-3 space-y-1"> <div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">Mon profil</x-responsive-nav-link> <x-responsive-nav-link :href="route('profile.edit')">Mon profil</x-responsive-nav-link>
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
+12 -12
View File
@@ -1,19 +1,19 @@
<div class="space-y-5"> <div class="space-y-5">
{{-- Nom --}} {{-- Nom --}}
<div> <div>
<label for="nom" class="block text-sm font-medium text-gray-700">Nom <span class="text-red-500">*</span></label> <label for="nom" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nom <span class="text-red-500">*</span></label>
<input type="text" id="nom" name="nom" <input type="text" id="nom" name="nom"
value="{{ old('nom', $lieu?->nom) }}" value="{{ old('nom', $lieu?->nom) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror"
required> required>
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
{{-- Type de lieu --}} {{-- Type de lieu --}}
<div> <div>
<label for="lieu_type_id" class="block text-sm font-medium text-gray-700">Type <span class="text-red-500">*</span></label> <label for="lieu_type_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Type <span class="text-red-500">*</span></label>
<select id="lieu_type_id" name="lieu_type_id" required <select id="lieu_type_id" name="lieu_type_id" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('lieu_type_id') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('lieu_type_id') border-red-500 @enderror">
<option value=""> Choisir un type </option> <option value=""> Choisir un type </option>
@foreach($lieuTypes as $lt) @foreach($lieuTypes as $lt)
<option value="{{ $lt->id }}" {{ old('lieu_type_id', $lieu?->lieu_type_id) == $lt->id ? 'selected' : '' }}> <option value="{{ $lt->id }}" {{ old('lieu_type_id', $lieu?->lieu_type_id) == $lt->id ? 'selected' : '' }}>
@@ -32,10 +32,10 @@
{{-- Code --}} {{-- Code --}}
<div> <div>
<label for="code" class="block text-sm font-medium text-gray-700">Code (INSEE, postal…)</label> <label for="code" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Code (INSEE, postal…)</label>
<input type="text" id="code" name="code" <input type="text" id="code" name="code"
value="{{ old('code', $lieu?->code) }}" value="{{ old('code', $lieu?->code) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
maxlength="20"> maxlength="20">
</div> </div>
@@ -53,18 +53,18 @@
{{-- Coordonnées --}} {{-- Coordonnées --}}
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label for="latitude" class="block text-sm font-medium text-gray-700">Latitude</label> <label for="latitude" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Latitude</label>
<input type="number" id="latitude" name="latitude" step="0.0000001" <input type="number" id="latitude" name="latitude" step="0.0000001"
value="{{ old('latitude', $lieu?->latitude) }}" value="{{ old('latitude', $lieu?->latitude) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="48.8566"> placeholder="48.8566">
@error('latitude') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('latitude') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label for="longitude" class="block text-sm font-medium text-gray-700">Longitude</label> <label for="longitude" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Longitude</label>
<input type="number" id="longitude" name="longitude" step="0.0000001" <input type="number" id="longitude" name="longitude" step="0.0000001"
value="{{ old('longitude', $lieu?->longitude) }}" value="{{ old('longitude', $lieu?->longitude) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
placeholder="2.3522"> placeholder="2.3522">
@error('longitude') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('longitude') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
@@ -72,8 +72,8 @@
{{-- Note --}} {{-- Note --}}
<div> <div>
<label for="note" class="block text-sm font-medium text-gray-700">Note</label> <label for="note" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Note</label>
<textarea id="note" name="note" rows="3" <textarea id="note" name="note" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('note', $lieu?->note) }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('note', $lieu?->note) }}</textarea>
</div> </div>
</div> </div>
+3 -3
View File
@@ -1,10 +1,10 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold text-gray-800">Nouveau lieu</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Nouveau lieu</h2>
</x-slot> </x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('lieux.store') }}"> <form method="POST" action="{{ route('lieux.store') }}">
@csrf @csrf
@include('lieux._form', ['lieu' => null]) @include('lieux._form', ['lieu' => null])
@@ -14,7 +14,7 @@
class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"> class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
Créer Créer
</button> </button>
<a href="{{ route('lieux.index') }}" class="text-sm text-gray-500 hover:text-gray-700">Annuler</a> <a href="{{ route('lieux.index') }}" class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+3 -3
View File
@@ -1,10 +1,10 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold text-gray-800">Modifier : {{ $lieu->nom }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Modifier : {{ $lieu->nom }}</h2>
</x-slot> </x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('lieux.update', $lieu) }}"> <form method="POST" action="{{ route('lieux.update', $lieu) }}">
@csrf @method('PUT') @csrf @method('PUT')
@include('lieux._form', ['lieu' => $lieu]) @include('lieux._form', ['lieu' => $lieu])
@@ -14,7 +14,7 @@
class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"> class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
Enregistrer Enregistrer
</button> </button>
<a href="{{ route('lieux.show', $lieu) }}" class="text-sm text-gray-500 hover:text-gray-700">Annuler</a> <a href="{{ route('lieux.show', $lieu) }}" class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+27 -27
View File
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Lieux</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Lieux</h2>
@can('create', App\Models\Lieu::class) @can('create', App\Models\Lieu::class)
<a href="{{ route('lieux.create') }}" <a href="{{ route('lieux.create') }}"
class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700"> class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">
@@ -14,29 +14,29 @@
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@if(session('success')) @if(session('success'))
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> <div class="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div>
@endif @endif
@if(session('error')) @if(session('error'))
<div class="p-4 bg-red-50 border border-red-200 text-red-800 rounded-md">{{ session('error') }}</div> <div class="p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200 rounded-md">{{ session('error') }}</div>
@endif @endif
{{-- Filtres --}} {{-- Filtres --}}
@php $hasFilters = request()->anyFilled(['lieu_type_id', 'q', 'lieu_id']); @endphp @php $hasFilters = request()->anyFilled(['lieu_type_id', 'q', 'lieu_id']); @endphp
<div class="bg-white shadow rounded-lg p-5"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-5">
<form method="GET" action="{{ route('lieux.index') }}"> <form method="GET" action="{{ route('lieux.index') }}">
<div class="flex flex-wrap items-end gap-4"> <div class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
<label class="block text-xs font-medium text-gray-600 mb-1">Recherche</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Recherche</label>
<input type="text" name="q" value="{{ request('q') }}" <input type="text" name="q" value="{{ request('q') }}"
placeholder="Nom, code INSEE…" placeholder="Nom, code INSEE…"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div class="w-52"> <div class="w-52">
<label class="block text-xs font-medium text-gray-600 mb-1">Type de lieu</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Type de lieu</label>
<select name="lieu_type_id" <select name="lieu_type_id"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value=""> Tous les types </option> <option value=""> Tous les types </option>
@foreach($lieuTypes as $lt) @foreach($lieuTypes as $lt)
<option value="{{ $lt->id }}" {{ request('lieu_type_id') == $lt->id ? 'selected' : '' }}> <option value="{{ $lt->id }}" {{ request('lieu_type_id') == $lt->id ? 'selected' : '' }}>
@@ -53,10 +53,10 @@
</button> </button>
@if($hasFilters) @if($hasFilters)
<a href="{{ route('lieux.index') }}" <a href="{{ route('lieux.index') }}"
class="px-4 py-2 border border-gray-300 text-gray-600 text-sm rounded-md hover:bg-gray-50"> class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 text-sm rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
Effacer Effacer
</a> </a>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-indigo-100 text-indigo-700"> <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700">
filtres actifs filtres actifs
</span> </span>
@endif @endif
@@ -77,29 +77,29 @@
</div> </div>
{{-- Tableau --}} {{-- Tableau --}}
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lieu</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Lieu</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Code</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Code</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Parent</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Parent</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Coordonnées</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Coordonnées</th>
<th class="px-6 py-3"></th> <th class="px-6 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($lieux as $lieu) @forelse($lieux as $lieu)
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4"> <td class="px-6 py-4">
<a href="{{ route('lieux.show', $lieu) }}" class="font-medium text-indigo-600 hover:underline"> <a href="{{ route('lieux.show', $lieu) }}" class="font-medium text-indigo-600 hover:underline">
{{ $lieu->nom }} {{ $lieu->nom }}
</a> </a>
</td> </td>
<td class="px-6 py-4 text-sm text-gray-500">{{ $lieu->lieuType?->nom ?? '—' }}</td> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $lieu->lieuType?->nom ?? '—' }}</td>
<td class="px-6 py-4 text-sm text-gray-500">{{ $lieu->code ?? '—' }}</td> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $lieu->code ?? '—' }}</td>
<td class="px-6 py-4 text-sm text-gray-500"> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
@if($lieu->parent) @if($lieu->parent)
<a href="{{ route('lieux.show', $lieu->parent) }}" class="text-indigo-600 hover:underline"> <a href="{{ route('lieux.show', $lieu->parent) }}" class="text-indigo-600 hover:underline">
{{ $lieu->parent->nom }} {{ $lieu->parent->nom }}
@@ -108,7 +108,7 @@
@endif @endif
</td> </td>
<td class="px-6 py-4 text-sm text-gray-500"> <td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
@if($lieu->latitude && $lieu->longitude) @if($lieu->latitude && $lieu->longitude)
{{ number_format($lieu->latitude, 4) }}, {{ number_format($lieu->longitude, 4) }} {{ number_format($lieu->latitude, 4) }}, {{ number_format($lieu->longitude, 4) }}
@else @else
@@ -117,7 +117,7 @@
</td> </td>
<td class="px-6 py-4 text-right text-sm space-x-3"> <td class="px-6 py-4 text-right text-sm space-x-3">
@can('update', $lieu) @can('update', $lieu)
<a href="{{ route('lieux.edit', $lieu) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a> <a href="{{ route('lieux.edit', $lieu) }}" class="text-gray-600 dark:text-gray-400 hover:text-indigo-600">Modifier</a>
@endcan @endcan
@can('delete', $lieu) @can('delete', $lieu)
<form method="POST" action="{{ route('lieux.destroy', $lieu) }}" class="inline" <form method="POST" action="{{ route('lieux.destroy', $lieu) }}" class="inline"
@@ -131,7 +131,7 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="6" class="px-6 py-10 text-center text-gray-400"> <td colspan="6" class="px-6 py-10 text-center text-gray-400 dark:text-gray-500">
@if($hasFilters) Aucun lieu ne correspond aux filtres. @if($hasFilters) Aucun lieu ne correspond aux filtres.
@else Aucun lieu enregistré. @endif @else Aucun lieu enregistré. @endif
</td> </td>
@@ -141,7 +141,7 @@
</table> </table>
@if($lieux->hasPages()) @if($lieux->hasPages())
<div class="px-6 py-4 border-t border-gray-200"> <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
{{ $lieux->links() }} {{ $lieux->links() }}
</div> </div>
@endif @endif
+20 -20
View File
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">{{ $lieu->nom }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">{{ $lieu->nom }}</h2>
<div class="flex gap-3"> <div class="flex gap-3">
@can('update', $lieu) @can('update', $lieu)
<a href="{{ route('lieux.edit', $lieu) }}" <a href="{{ route('lieux.edit', $lieu) }}"
@@ -26,67 +26,67 @@
<div class="py-8 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@if(session('success')) @if(session('success'))
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md"> <div class="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">
{{ session('success') }} {{ session('success') }}
</div> </div>
@endif @endif
{{-- Fiche --}} {{-- Fiche --}}
<div class="bg-white shadow rounded-lg divide-y divide-gray-100"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg divide-y divide-gray-100 dark:divide-gray-700">
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm"> <div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
<dt class="font-medium text-gray-500">Nom complet</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">Nom complet</dt>
<dd class="col-span-2 text-gray-900">{{ $lieu->nom_long ?? $lieu->nom }}</dd> <dd class="col-span-2 text-gray-900 dark:text-white">{{ $lieu->nom_long ?? $lieu->nom }}</dd>
</div> </div>
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm"> <div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
<dt class="font-medium text-gray-500">Type</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">Type</dt>
<dd class="col-span-2 text-gray-900">{{ $lieu->lieuType?->nom ?? '—' }}</dd> <dd class="col-span-2 text-gray-900 dark:text-white">{{ $lieu->lieuType?->nom ?? '—' }}</dd>
</div> </div>
@if($lieu->code) @if($lieu->code)
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm"> <div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
<dt class="font-medium text-gray-500">Code</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">Code</dt>
<dd class="col-span-2 text-gray-900">{{ $lieu->code }}</dd> <dd class="col-span-2 text-gray-900 dark:text-white">{{ $lieu->code }}</dd>
</div> </div>
@endif @endif
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm"> <div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
<dt class="font-medium text-gray-500">Lieu parent</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">Lieu parent</dt>
<dd class="col-span-2"> <dd class="col-span-2">
@if($lieu->parent) @if($lieu->parent)
<a href="{{ route('lieux.show', $lieu->parent) }}" class="text-indigo-600 hover:underline"> <a href="{{ route('lieux.show', $lieu->parent) }}" class="text-indigo-600 hover:underline">
{{ $lieu->parent->nom_long ?? $lieu->parent->nom }} {{ $lieu->parent->nom_long ?? $lieu->parent->nom }}
</a> </a>
@else @else
<span class="text-gray-400">Lieu racine</span> <span class="text-gray-400 dark:text-gray-500">Lieu racine</span>
@endif @endif
</dd> </dd>
</div> </div>
@if($lieu->latitude && $lieu->longitude) @if($lieu->latitude && $lieu->longitude)
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm"> <div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
<dt class="font-medium text-gray-500">Coordonnées</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">Coordonnées</dt>
<dd class="col-span-2 text-gray-900">{{ $lieu->latitude }}, {{ $lieu->longitude }}</dd> <dd class="col-span-2 text-gray-900 dark:text-white">{{ $lieu->latitude }}, {{ $lieu->longitude }}</dd>
</div> </div>
@endif @endif
@if($lieu->note) @if($lieu->note)
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm"> <div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
<dt class="font-medium text-gray-500">Note</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">Note</dt>
<dd class="col-span-2 text-gray-900 whitespace-pre-line">{{ $lieu->note }}</dd> <dd class="col-span-2 text-gray-900 dark:text-white whitespace-pre-line">{{ $lieu->note }}</dd>
</div> </div>
@endif @endif
</div> </div>
{{-- Enfants --}} {{-- Enfants --}}
@if($lieu->enfants->isNotEmpty()) @if($lieu->enfants->isNotEmpty())
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200"> <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="font-medium text-gray-900">Subdivisions ({{ $lieu->enfants->count() }})</h3> <h3 class="font-medium text-gray-900 dark:text-white">Subdivisions ({{ $lieu->enfants->count() }})</h3>
</div> </div>
<ul class="divide-y divide-gray-100"> <ul class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($lieu->enfants->sortBy('nom') as $enfant) @foreach($lieu->enfants->sortBy('nom') as $enfant)
<li class="px-6 py-3"> <li class="px-6 py-3">
<a href="{{ route('lieux.show', $enfant) }}" class="text-indigo-600 hover:underline"> <a href="{{ route('lieux.show', $enfant) }}" class="text-indigo-600 hover:underline">
{{ $enfant->nom }} {{ $enfant->nom }}
</a> </a>
@if($enfant->code) @if($enfant->code)
<span class="ml-2 text-xs text-gray-400">({{ $enfant->code }})</span> <span class="ml-2 text-xs text-gray-400 dark:text-gray-500">({{ $enfant->code }})</span>
@endif @endif
</li> </li>
@endforeach @endforeach
+11 -11
View File
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Notifications</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Notifications</h2>
@if(auth()->user()->unreadNotifications->isNotEmpty()) @if(auth()->user()->unreadNotifications->isNotEmpty())
<form method="POST" action="{{ route('notifications.read-all') }}"> <form method="POST" action="{{ route('notifications.read-all') }}">
@csrf @csrf
@@ -16,12 +16,12 @@
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
@if(session('success')) @if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md"> <div class="mb-4 p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">
{{ session('success') }} {{ session('success') }}
</div> </div>
@endif @endif
<div class="bg-white shadow rounded-lg divide-y divide-gray-100"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg divide-y divide-gray-100 dark:divide-gray-700">
@forelse($notifications as $notification) @forelse($notifications as $notification)
@php @php
$data = $notification->data; $data = $notification->data;
@@ -32,13 +32,13 @@
{{-- Icône --}} {{-- Icône --}}
<div class="shrink-0 mt-0.5"> <div class="shrink-0 mt-0.5">
@if($isRejet) @if($isRejet)
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-red-100 text-red-600"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-red-100 dark:bg-red-900/50 text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
</span> </span>
@else @else
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-yellow-100 text-yellow-600"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
@@ -48,7 +48,7 @@
{{-- Contenu --}} {{-- Contenu --}}
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<p class="text-sm text-gray-900"> <p class="text-sm text-gray-900 dark:text-white">
@if($isRejet) @if($isRejet)
<strong>{{ $data['rejete_par'] }}</strong> a renvoyé la source <strong>{{ $data['rejete_par'] }}</strong> a renvoyé la source
<strong>{{ $data['source_nom'] }}</strong> en cours de saisie. <strong>{{ $data['source_nom'] }}</strong> en cours de saisie.
@@ -57,10 +57,10 @@
<strong>{{ $data['source_nom'] }}</strong> pour validation. <strong>{{ $data['source_nom'] }}</strong> pour validation.
@endif @endif
</p> </p>
<div class="mt-1 flex items-center gap-3 text-xs text-gray-400"> <div class="mt-1 flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
<span>{{ $notification->created_at->diffForHumans() }}</span> <span>{{ $notification->created_at->diffForHumans() }}</span>
@if(!$isRead) @if(!$isRead)
<span class="inline-block w-2 h-2 rounded-full bg-indigo-500"></span> <span class="inline-block w-2 h-2 rounded-full bg-indigo-50 dark:bg-indigo-900/300"></span>
@endif @endif
</div> </div>
</div> </div>
@@ -74,14 +74,14 @@
@if(!$isRead) @if(!$isRead)
<form method="POST" action="{{ route('notifications.read', $notification->id) }}"> <form method="POST" action="{{ route('notifications.read', $notification->id) }}">
@csrf @csrf
<button type="submit" class="text-xs text-gray-400 hover:text-gray-600" title="Marquer comme lu"></button> <button type="submit" class="text-xs text-gray-400 dark:text-gray-500 hover:text-gray-600" title="Marquer comme lu"></button>
</form> </form>
@endif @endif
</div> </div>
</div> </div>
@empty @empty
<div class="px-6 py-16 text-center text-gray-400"> <div class="px-6 py-16 text-center text-gray-400 dark:text-gray-500">
<svg class="mx-auto w-10 h-10 mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto w-10 h-10 mb-3 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg> </svg>
<p>Aucune notification</p> <p>Aucune notification</p>
+4 -4
View File
@@ -1,25 +1,25 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Mon profil Mon profil
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl"> <div class="max-w-xl">
@include('profile.partials.update-profile-information-form') @include('profile.partials.update-profile-information-form')
</div> </div>
</div> </div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl"> <div class="max-w-xl">
@include('profile.partials.update-password-form') @include('profile.partials.update-password-form')
</div> </div>
</div> </div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg"> <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl"> <div class="max-w-xl">
@include('profile.partials.delete-user-form') @include('profile.partials.delete-user-form')
</div> </div>
@@ -1,7 +1,7 @@
<section class="space-y-6"> <section class="space-y-6">
<header> <header>
<h2 class="text-lg font-medium text-gray-900">Supprimer le compte</h2> <h2 class="text-lg font-medium text-gray-900 dark:text-white">Supprimer le compte</h2>
<p class="mt-1 text-sm text-gray-600"> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Une fois votre compte supprimé, toutes ses données seront définitivement effacées. Une fois votre compte supprimé, toutes ses données seront définitivement effacées.
Téléchargez toute information que vous souhaitez conserver avant de procéder. Téléchargez toute information que vous souhaitez conserver avant de procéder.
</p> </p>
@@ -17,11 +17,11 @@
@csrf @csrf
@method('delete') @method('delete')
<h2 class="text-lg font-medium text-gray-900"> <h2 class="text-lg font-medium text-gray-900 dark:text-white">
Êtes-vous sûr de vouloir supprimer votre compte ? Êtes-vous sûr de vouloir supprimer votre compte ?
</h2> </h2>
<p class="mt-1 text-sm text-gray-600"> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Cette action est irréversible. Toutes vos données seront définitivement supprimées. Cette action est irréversible. Toutes vos données seront définitivement supprimées.
Saisissez votre mot de passe pour confirmer. Saisissez votre mot de passe pour confirmer.
</p> </p>
@@ -1,7 +1,7 @@
<section> <section>
<header> <header>
<h2 class="text-lg font-medium text-gray-900">Changer le mot de passe</h2> <h2 class="text-lg font-medium text-gray-900 dark:text-white">Changer le mot de passe</h2>
<p class="mt-1 text-sm text-gray-600"> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Utilisez un mot de passe long et aléatoire pour sécuriser votre compte. Utilisez un mot de passe long et aléatoire pour sécuriser votre compte.
</p> </p>
</header> </header>
@@ -37,7 +37,7 @@
@if (session('status') === 'password-updated') @if (session('status') === 'password-updated')
<p x-data="{ show: true }" x-show="show" x-transition <p x-data="{ show: true }" x-show="show" x-transition
x-init="setTimeout(() => show = false, 2000)" x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"> class="text-sm text-gray-600 dark:text-gray-400">
Enregistré. Enregistré.
</p> </p>
@endif @endif
@@ -1,7 +1,7 @@
<section> <section>
<header> <header>
<h2 class="text-lg font-medium text-gray-900">Informations du profil</h2> <h2 class="text-lg font-medium text-gray-900 dark:text-white">Informations du profil</h2>
<p class="mt-1 text-sm text-gray-600"> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Mettez à jour votre nom et votre adresse e-mail. Mettez à jour votre nom et votre adresse e-mail.
</p> </p>
</header> </header>
@@ -29,10 +29,10 @@
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail()) @if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
<div> <div>
<p class="text-sm mt-2 text-gray-800"> <p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
Votre adresse e-mail n'est pas vérifiée. Votre adresse e-mail n'est pas vérifiée.
<button form="send-verification" <button form="send-verification"
class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cliquez ici pour renvoyer l'e-mail de vérification. Cliquez ici pour renvoyer l'e-mail de vérification.
</button> </button>
</p> </p>
@@ -51,7 +51,7 @@
@if (session('status') === 'profile-updated') @if (session('status') === 'profile-updated')
<p x-data="{ show: true }" x-show="show" x-transition <p x-data="{ show: true }" x-show="show" x-transition
x-init="setTimeout(() => show = false, 2000)" x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"> class="text-sm text-gray-600 dark:text-gray-400">
Enregistré. Enregistré.
</p> </p>
@endif @endif
+34 -34
View File
@@ -1,19 +1,19 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold text-gray-800">Recherche dans les relevés</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Recherche dans les relevés</h2>
</x-slot> </x-slot>
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
{{-- Formulaire de recherche --}} {{-- Formulaire de recherche --}}
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="GET" action="{{ route('recherche') }}" class="space-y-4"> <form method="GET" action="{{ route('recherche') }}" class="space-y-4">
{{-- Barre principale --}} {{-- Barre principale --}}
<div class="flex gap-3"> <div class="flex gap-3">
<div class="flex-1 relative"> <div class="flex-1 relative">
<div class="absolute inset-y-0 left-3 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-3 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/> d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
</svg> </svg>
@@ -21,7 +21,7 @@
<input type="text" name="q" value="{{ request('q') }}" <input type="text" name="q" value="{{ request('q') }}"
placeholder="Nom, prénom, lieu, note…" placeholder="Nom, prénom, lieu, note…"
autofocus autofocus
class="block w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-md shadow-sm class="block w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm
text-sm focus:border-indigo-500 focus:ring-indigo-500"> text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<button type="submit" <button type="submit"
@@ -30,7 +30,7 @@
</button> </button>
@if(request()->anyFilled(['q', 'source_type_id', 'annee_debut', 'annee_fin'])) @if(request()->anyFilled(['q', 'source_type_id', 'annee_debut', 'annee_fin']))
<a href="{{ route('recherche') }}" <a href="{{ route('recherche') }}"
class="px-4 py-2.5 border border-gray-300 text-gray-600 text-sm rounded-md hover:bg-gray-50"> class="px-4 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 text-sm rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
Effacer Effacer
</a> </a>
@endif @endif
@@ -45,7 +45,7 @@
class="text-sm text-indigo-600 hover:underline flex items-center gap-1"> class="text-sm text-indigo-600 hover:underline flex items-center gap-1">
<span x-text="open ? '▲ Masquer les filtres' : '▼ Filtres avancés'"></span> <span x-text="open ? '▲ Masquer les filtres' : '▼ Filtres avancés'"></span>
@if($hasAdvanced) @if($hasAdvanced)
<span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-indigo-100 text-indigo-700"> <span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700">
actifs actifs
</span> </span>
@endif @endif
@@ -54,9 +54,9 @@
<div x-show="open" x-cloak class="mt-4 space-y-4"> <div x-show="open" x-cloak class="mt-4 space-y-4">
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Type de source</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Type de source</label>
<select name="source_type_id" <select name="source_type_id"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value=""> Tous les types </option> <option value=""> Tous les types </option>
@foreach($sourceTypes as $st) @foreach($sourceTypes as $st)
<option value="{{ $st->id }}" {{ request('source_type_id') == $st->id ? 'selected' : '' }}> <option value="{{ $st->id }}" {{ request('source_type_id') == $st->id ? 'selected' : '' }}>
@@ -66,16 +66,16 @@
</select> </select>
</div> </div>
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Année de début</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Année de début</label>
<input type="number" name="annee_debut" value="{{ request('annee_debut') }}" <input type="number" name="annee_debut" value="{{ request('annee_debut') }}"
min="1000" max="2100" placeholder="ex : 1820" min="1000" max="2100" placeholder="ex : 1820"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Année de fin</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Année de fin</label>
<input type="number" name="annee_fin" value="{{ request('annee_fin') }}" <input type="number" name="annee_fin" value="{{ request('annee_fin') }}"
min="1000" max="2100" placeholder="ex : 1830" min="1000" max="2100" placeholder="ex : 1830"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
</div> </div>
@@ -89,7 +89,7 @@
placeholder="— Tous les lieux —" placeholder="— Tous les lieux —"
/> />
@if($lieuSelectionne) @if($lieuSelectionne)
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
Inclut toutes les subdivisions de {{ $lieuSelectionne->nom_long ?? $lieuSelectionne->nom }}. Inclut toutes les subdivisions de {{ $lieuSelectionne->nom_long ?? $lieuSelectionne->nom }}.
</p> </p>
@endif @endif
@@ -102,7 +102,7 @@
{{-- Résultats --}} {{-- Résultats --}}
@if($resultats !== null) @if($resultats !== null)
<div> <div>
<p class="text-sm text-gray-500 mb-3"> <p class="text-sm text-gray-500 dark:text-gray-400 mb-3">
@if($total === 0) @if($total === 0)
Aucun relevé trouvé. Aucun relevé trouvé.
@else @else
@@ -117,20 +117,20 @@
</p> </p>
@if($resultats->isNotEmpty()) @if($resultats->isNotEmpty())
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Prénom</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Prénom</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Date</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Source</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Source</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Type</th>
<th class="px-4 py-3"></th> <th class="px-4 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($resultats as $releve) @foreach($resultats as $releve)
@php @php
$data = $releve->data; $data = $releve->data;
@@ -139,32 +139,32 @@
? ($dateEvt['valeur'] ?? '—') . ($dateEvt['calendrier'] !== 'gregorien' ? ' (' . $dateEvt['calendrier'] . ')' : '') ? ($dateEvt['valeur'] ?? '—') . ($dateEvt['calendrier'] !== 'gregorien' ? ' (' . $dateEvt['calendrier'] . ')' : '')
: ($releve->date_evenement ?? '—'); : ($releve->date_evenement ?? '—');
@endphp @endphp
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-4 py-3 font-medium text-gray-900"> <td class="px-4 py-3 font-medium text-gray-900 dark:text-white">
@if(request('q') && $releve->nom) @if(request('q') && $releve->nom)
{!! preg_replace('/(' . preg_quote(request('q'), '/') . ')/i', '<mark class="bg-yellow-100 rounded px-0.5">$1</mark>', e($releve->nom)) !!} {!! preg_replace('/(' . preg_quote(request('q'), '/') . ')/i', '<mark class="bg-yellow-100 dark:bg-yellow-900/50 rounded px-0.5">$1</mark>', e($releve->nom)) !!}
@else @else
{{ $releve->nom ?? '—' }} {{ $releve->nom ?? '—' }}
@endif @endif
</td> </td>
<td class="px-4 py-3 text-gray-700"> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">
@if(request('q') && $releve->prenom) @if(request('q') && $releve->prenom)
{!! preg_replace('/(' . preg_quote(request('q'), '/') . ')/i', '<mark class="bg-yellow-100 rounded px-0.5">$1</mark>', e($releve->prenom)) !!} {!! preg_replace('/(' . preg_quote(request('q'), '/') . ')/i', '<mark class="bg-yellow-100 dark:bg-yellow-900/50 rounded px-0.5">$1</mark>', e($releve->prenom)) !!}
@else @else
{{ $releve->prenom ?? '—' }} {{ $releve->prenom ?? '—' }}
@endif @endif
</td> </td>
<td class="px-4 py-3 text-gray-600 whitespace-nowrap"> <td class="px-4 py-3 text-gray-600 dark:text-gray-400 whitespace-nowrap">
{{ $dateAffichee }} {{ $dateAffichee }}
</td> </td>
<td class="px-4 py-3 text-gray-600"> <td class="px-4 py-3 text-gray-600 dark:text-gray-400">
<a href="{{ route('sources.show', $releve->source) }}" <a href="{{ route('sources.show', $releve->source) }}"
class="hover:text-indigo-600 hover:underline"> class="hover:text-indigo-600 hover:underline">
{{ $releve->source->nom }} {{ $releve->source->nom }}
</a> </a>
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600"> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
{{ $releve->source->sourceType->nom }} {{ $releve->source->sourceType->nom }}
</span> </span>
</td> </td>
@@ -181,7 +181,7 @@
</div> </div>
@if($resultats->hasPages()) @if($resultats->hasPages())
<div class="px-6 py-4 border-t border-gray-200"> <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
{{ $resultats->links() }} {{ $resultats->links() }}
</div> </div>
@endif @endif
@@ -190,8 +190,8 @@
</div> </div>
@else @else
{{-- État initial --}} {{-- État initial --}}
<div class="text-center py-16 text-gray-400"> <div class="text-center py-16 text-gray-400 dark:text-gray-500">
<svg class="mx-auto w-12 h-12 mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto w-12 h-12 mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/> d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
</svg> </svg>
+10 -10
View File
@@ -10,7 +10,7 @@
@endphp @endphp
<div class="space-y-1"> <div class="space-y-1">
<label for="{{ $inputId }}" class="block text-sm font-medium text-gray-700"> <label for="{{ $inputId }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ $field->label }} {{ $field->label }}
@if($field->required) <span class="text-red-500">*</span> @endif @if($field->required) <span class="text-red-500">*</span> @endif
</label> </label>
@@ -21,20 +21,20 @@
<input type="text" id="{{ $inputId }}" name="{{ $name }}" <input type="text" id="{{ $inputId }}" name="{{ $name }}"
value="{{ $oldValue }}" value="{{ $oldValue }}"
{{ $field->required ? 'required' : '' }} {{ $field->required ? 'required' : '' }}
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">
@break @break
@case(FieldType::Textarea) @case(FieldType::Textarea)
<textarea id="{{ $inputId }}" name="{{ $name }}" rows="3" <textarea id="{{ $inputId }}" name="{{ $name }}" rows="3"
{{ $field->required ? 'required' : '' }} {{ $field->required ? 'required' : '' }}
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">{{ $oldValue }}</textarea> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">{{ $oldValue }}</textarea>
@break @break
@case(FieldType::Number) @case(FieldType::Number)
<input type="number" id="{{ $inputId }}" name="{{ $name }}" <input type="number" id="{{ $inputId }}" name="{{ $name }}"
value="{{ $oldValue }}" step="any" value="{{ $oldValue }}" step="any"
{{ $field->required ? 'required' : '' }} {{ $field->required ? 'required' : '' }}
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">
@break @break
@case(FieldType::Boolean) @case(FieldType::Boolean)
@@ -43,15 +43,15 @@
<input type="hidden" name="{{ $name }}" value="0"> <input type="hidden" name="{{ $name }}" value="0">
<input type="checkbox" id="{{ $inputId }}" name="{{ $name }}" value="1" <input type="checkbox" id="{{ $inputId }}" name="{{ $name }}" value="1"
{{ $checked ? 'checked' : '' }} {{ $checked ? 'checked' : '' }}
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"> class="rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500">
<span class="text-sm text-gray-600">{{ $field->label }}</span> <span class="text-sm text-gray-600 dark:text-gray-400">{{ $field->label }}</span>
</div> </div>
@break @break
@case(FieldType::Select) @case(FieldType::Select)
<select id="{{ $inputId }}" name="{{ $name }}" <select id="{{ $inputId }}" name="{{ $name }}"
{{ $field->required ? 'required' : '' }} {{ $field->required ? 'required' : '' }}
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">
@if(!$field->required) <option value=""> Choisir </option> @endif @if(!$field->required) <option value=""> Choisir </option> @endif
@foreach($field->options ?? [] as $opt) @foreach($field->options ?? [] as $opt)
<option value="{{ $opt }}" {{ $oldValue === $opt ? 'selected' : '' }}>{{ $opt }}</option> <option value="{{ $opt }}" {{ $oldValue === $opt ? 'selected' : '' }}>{{ $opt }}</option>
@@ -67,7 +67,7 @@
<div x-data="{ cal: '{{ $dateCal }}' }" class="flex gap-2"> <div x-data="{ cal: '{{ $dateCal }}' }" class="flex gap-2">
{{-- Sélecteur de calendrier --}} {{-- Sélecteur de calendrier --}}
<select name="{{ $name }}[calendrier]" x-model="cal" <select name="{{ $name }}[calendrier]" x-model="cal"
class="w-40 rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="w-40 rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="gregorien">Grégorien</option> <option value="gregorien">Grégorien</option>
<option value="julien">Julien</option> <option value="julien">Julien</option>
<option value="republicain">Républicain</option> <option value="republicain">Républicain</option>
@@ -78,14 +78,14 @@
type="date" name="{{ $name }}[valeur]" type="date" name="{{ $name }}[valeur]"
value="{{ $dateCal !== 'republicain' ? $dateVal : '' }}" value="{{ $dateCal !== 'republicain' ? $dateVal : '' }}"
{{ $field->required ? 'required' : '' }} {{ $field->required ? 'required' : '' }}
class="flex-1 rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="flex-1 rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
{{-- Date républicaine : saisie texte libre (ex: "15 Vendémiaire An III") --}} {{-- Date républicaine : saisie texte libre (ex: "15 Vendémiaire An III") --}}
<input x-show="cal === 'republicain'" x-cloak <input x-show="cal === 'republicain'" x-cloak
type="text" name="{{ $name }}[valeur]" type="text" name="{{ $name }}[valeur]"
value="{{ $dateCal === 'republicain' ? $dateVal : '' }}" value="{{ $dateCal === 'republicain' ? $dateVal : '' }}"
placeholder="ex : 15 Vendémiaire An III" placeholder="ex : 15 Vendémiaire An III"
class="flex-1 rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="flex-1 rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
@error("data.{$field->name}.valeur") @error("data.{$field->name}.valeur")
<p class="text-sm text-red-600">{{ $message }}</p> <p class="text-sm text-red-600">{{ $message }}</p>
+1 -1
View File
@@ -7,7 +7,7 @@
@endphp @endphp
@include('releves._field', ['field' => $field, 'value' => $rawValue]) @include('releves._field', ['field' => $field, 'value' => $rawValue])
@empty @empty
<p class="text-sm text-gray-400 italic"> <p class="text-sm text-gray-400 dark:text-gray-500 italic">
Ce type de source n'a aucun champ défini. Ce type de source n'a aucun champ défini.
<a href="{{ route('admin.source-types.show', $source->sourceType) }}" class="text-indigo-600 hover:underline">Configurer les champs </a> <a href="{{ route('admin.source-types.show', $source->sourceType) }}" class="text-indigo-600 hover:underline">Configurer les champs </a>
</p> </p>
+5 -5
View File
@@ -1,8 +1,8 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div> <div>
<h2 class="text-xl font-semibold text-gray-800">Nouveau relevé</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Nouveau relevé</h2>
<p class="text-sm text-gray-500 mt-0.5"> <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a> Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
· Type : {{ $source->sourceType->nom }} · Type : {{ $source->sourceType->nom }}
</p> </p>
@@ -10,17 +10,17 @@
</x-slot> </x-slot>
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('sources.releves.store', $source) }}"> <form method="POST" action="{{ route('sources.releves.store', $source) }}">
@csrf @csrf
@include('releves._form', ['releve' => null]) @include('releves._form', ['releve' => null])
<div class="mt-8 pt-6 border-t border-gray-200 flex items-center gap-4"> <div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 flex items-center gap-4">
<button type="submit" <button type="submit"
class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"> class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
Enregistrer le relevé Enregistrer le relevé
</button> </button>
<a href="{{ route('sources.releves.index', $source) }}" <a href="{{ route('sources.releves.index', $source) }}"
class="text-sm text-gray-500 hover:text-gray-700">Annuler</a> class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+5 -5
View File
@@ -1,25 +1,25 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div> <div>
<h2 class="text-xl font-semibold text-gray-800">Modifier le relevé #{{ $releve->id }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Modifier le relevé #{{ $releve->id }}</h2>
<p class="text-sm text-gray-500 mt-0.5"> <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a> Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
</p> </p>
</div> </div>
</x-slot> </x-slot>
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('releves.update', $releve) }}"> <form method="POST" action="{{ route('releves.update', $releve) }}">
@csrf @method('PUT') @csrf @method('PUT')
@include('releves._form', ['releve' => $releve]) @include('releves._form', ['releve' => $releve])
<div class="mt-8 pt-6 border-t border-gray-200 flex items-center gap-4"> <div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 flex items-center gap-4">
<button type="submit" <button type="submit"
class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"> class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
Enregistrer Enregistrer
</button> </button>
<a href="{{ route('releves.show', $releve) }}" <a href="{{ route('releves.show', $releve) }}"
class="text-sm text-gray-500 hover:text-gray-700">Annuler</a> class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+21 -21
View File
@@ -2,8 +2,8 @@
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h2 class="text-xl font-semibold text-gray-800">Relevés {{ $source->nom }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Relevés {{ $source->nom }}</h2>
<p class="text-sm text-gray-500 mt-0.5"> <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Type : {{ $source->sourceType->nom }} Type : {{ $source->sourceType->nom }}
@if($source->cote) · Cote : {{ $source->cote }} @endif @if($source->cote) · Cote : {{ $source->cote }} @endif
</p> </p>
@@ -11,7 +11,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<a href="{{ route('sources.show', $source) }}" class="text-sm text-indigo-600 hover:underline"> Source</a> <a href="{{ route('sources.show', $source) }}" class="text-sm text-indigo-600 hover:underline"> Source</a>
<a href="{{ route('export.source', $source) }}" <a href="{{ route('export.source', $source) }}"
class="px-4 py-2 border border-gray-300 text-gray-700 text-sm rounded-md hover:bg-gray-50" class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-sm rounded-md hover:bg-gray-50 dark:hover:bg-gray-700"
title="Télécharger au format GEDCOM 5.5.1"> title="Télécharger au format GEDCOM 5.5.1">
GEDCOM GEDCOM
</a> </a>
@@ -27,7 +27,7 @@
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@if(session('success')) @if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> <div class="mb-4 p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div>
@endif @endif
@php @php
@@ -35,31 +35,31 @@
$colonnes = $source->sourceType->fields->take(5); $colonnes = $source->sourceType->fields->take(5);
@endphp @endphp
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
@foreach($colonnes as $col) @foreach($colonnes as $col)
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase whitespace-nowrap"> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase whitespace-nowrap">
{{ $col->label }} {{ $col->label }}
</th> </th>
@endforeach @endforeach
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Saisi par</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Saisi par</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Date</th>
<th class="px-4 py-3"></th> <th class="px-4 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($releves as $releve) @forelse($releves as $releve)
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
@foreach($colonnes as $col) @foreach($colonnes as $col)
<td class="px-4 py-3 text-gray-700"> <td class="px-4 py-3 text-gray-700 dark:text-gray-300">
@php $val = $releve->data[$col->name] ?? null; @endphp @php $val = $releve->data[$col->name] ?? null; @endphp
@if(is_array($val)) @if(is_array($val))
{{ $val['valeur'] ?? '' }} {{ $val['valeur'] ?? '' }}
@if(!empty($val['calendrier']) && $val['calendrier'] !== 'gregorien') @if(!empty($val['calendrier']) && $val['calendrier'] !== 'gregorien')
<span class="text-xs text-gray-400">({{ $val['calendrier'] }})</span> <span class="text-xs text-gray-400 dark:text-gray-500">({{ $val['calendrier'] }})</span>
@endif @endif
@elseif(is_bool($val)) @elseif(is_bool($val))
{{ $val ? 'Oui' : 'Non' }} {{ $val ? 'Oui' : 'Non' }}
@@ -68,12 +68,12 @@
@endif @endif
</td> </td>
@endforeach @endforeach
<td class="px-4 py-3 text-gray-500">{{ $releve->createur?->name ?? '—' }}</td> <td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ $releve->createur?->name ?? '—' }}</td>
<td class="px-4 py-3 text-gray-500 whitespace-nowrap">{{ $releve->created_at->format('d/m/Y') }}</td> <td class="px-4 py-3 text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ $releve->created_at->format('d/m/Y') }}</td>
<td class="px-4 py-3 text-right whitespace-nowrap space-x-3"> <td class="px-4 py-3 text-right whitespace-nowrap space-x-3">
<a href="{{ route('releves.show', $releve) }}" class="text-indigo-600 hover:underline">Voir</a> <a href="{{ route('releves.show', $releve) }}" class="text-indigo-600 hover:underline">Voir</a>
@can('update', $releve) @can('update', $releve)
<a href="{{ route('releves.edit', $releve) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a> <a href="{{ route('releves.edit', $releve) }}" class="text-gray-600 dark:text-gray-400 hover:text-indigo-600">Modifier</a>
@endcan @endcan
@can('delete', $releve) @can('delete', $releve)
<form method="POST" action="{{ route('releves.destroy', $releve) }}" class="inline" <form method="POST" action="{{ route('releves.destroy', $releve) }}" class="inline"
@@ -87,7 +87,7 @@
@empty @empty
<tr> <tr>
<td colspan="{{ $colonnes->count() + 3 }}" <td colspan="{{ $colonnes->count() + 3 }}"
class="px-6 py-10 text-center text-gray-400"> class="px-6 py-10 text-center text-gray-400 dark:text-gray-500">
Aucun relevé pour cette source. Aucun relevé pour cette source.
</td> </td>
</tr> </tr>
@@ -98,10 +98,10 @@
{{-- Navigation curseur (keyset pagination) --}} {{-- Navigation curseur (keyset pagination) --}}
@if($releves->hasPages()) @if($releves->hasPages())
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between text-sm"> <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between text-sm">
<div> <div>
@if($releves->onFirstPage()) @if($releves->onFirstPage())
<span class="text-gray-400"> Précédent</span> <span class="text-gray-400 dark:text-gray-500"> Précédent</span>
@else @else
<a href="{{ $releves->previousPageUrl() }}" class="text-indigo-600 hover:underline"> Précédent</a> <a href="{{ $releves->previousPageUrl() }}" class="text-indigo-600 hover:underline"> Précédent</a>
@endif @endif
@@ -110,7 +110,7 @@
@if($releves->hasMorePages()) @if($releves->hasMorePages())
<a href="{{ $releves->nextPageUrl() }}" class="text-indigo-600 hover:underline">Suivant </a> <a href="{{ $releves->nextPageUrl() }}" class="text-indigo-600 hover:underline">Suivant </a>
@else @else
<span class="text-gray-400">Suivant </span> <span class="text-gray-400 dark:text-gray-500">Suivant </span>
@endif @endif
</div> </div>
</div> </div>
+10 -10
View File
@@ -2,8 +2,8 @@
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h2 class="text-xl font-semibold text-gray-800">Relevé #{{ $releve->id }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Relevé #{{ $releve->id }}</h2>
<p class="text-sm text-gray-500 mt-0.5"> <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a> Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
</p> </p>
</div> </div>
@@ -30,25 +30,25 @@
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@if(session('success')) @if(session('success'))
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> <div class="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div>
@endif @endif
{{-- Champs du relevé --}} {{-- Champs du relevé --}}
<div class="bg-white shadow rounded-lg divide-y divide-gray-100"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg divide-y divide-gray-100 dark:divide-gray-700">
@foreach($source->sourceType->fields as $field) @foreach($source->sourceType->fields as $field)
@php $val = $releve->data[$field->name] ?? null; @endphp @php $val = $releve->data[$field->name] ?? null; @endphp
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm"> <div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
<dt class="font-medium text-gray-500">{{ $field->label }}</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">{{ $field->label }}</dt>
<dd class="col-span-2 text-gray-900"> <dd class="col-span-2 text-gray-900 dark:text-white">
@if($val === null || $val === '') @if($val === null || $val === '')
<span class="text-gray-400"></span> <span class="text-gray-400 dark:text-gray-500"></span>
@elseif(is_array($val)) @elseif(is_array($val))
{{ $val['valeur'] ?? '—' }} {{ $val['valeur'] ?? '—' }}
@if(!empty($val['calendrier']) && $val['calendrier'] !== 'gregorien') @if(!empty($val['calendrier']) && $val['calendrier'] !== 'gregorien')
<span class="ml-1 text-xs text-gray-400 capitalize">({{ $val['calendrier'] }})</span> <span class="ml-1 text-xs text-gray-400 dark:text-gray-500 capitalize">({{ $val['calendrier'] }})</span>
@endif @endif
@elseif(is_bool($val)) @elseif(is_bool($val))
<span class="{{ $val ? 'text-green-700' : 'text-gray-400' }}"> <span class="{{ $val ? 'text-green-700' : 'text-gray-400 dark:text-gray-500' }}">
{{ $val ? 'Oui' : 'Non' }} {{ $val ? 'Oui' : 'Non' }}
</span> </span>
@else @else
@@ -60,7 +60,7 @@
</div> </div>
{{-- Méta-données de saisie --}} {{-- Méta-données de saisie --}}
<div class="bg-gray-50 rounded-lg px-6 py-4 text-xs text-gray-500 space-y-1"> <div class="bg-gray-50 dark:bg-gray-700 rounded-lg px-6 py-4 text-xs text-gray-500 dark:text-gray-400 space-y-1">
<p>Saisi par <strong>{{ $releve->createur?->name ?? '?' }}</strong> le {{ $releve->created_at->format('d/m/Y à H:i') }}</p> <p>Saisi par <strong>{{ $releve->createur?->name ?? '?' }}</strong> le {{ $releve->created_at->format('d/m/Y à H:i') }}</p>
@if($releve->updated_at != $releve->created_at) @if($releve->updated_at != $releve->created_at)
<p>Modifié par <strong>{{ $releve->modificateur?->name ?? '?' }}</strong> le {{ $releve->updated_at->format('d/m/Y à H:i') }}</p> <p>Modifié par <strong>{{ $releve->modificateur?->name ?? '?' }}</strong> le {{ $releve->updated_at->format('d/m/Y à H:i') }}</p>
+18 -18
View File
@@ -1,23 +1,23 @@
<div class="space-y-5"> <div class="space-y-5">
<div> <div>
<label for="nom" class="block text-sm font-medium text-gray-700">Nom <span class="text-red-500">*</span></label> <label for="nom" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Nom <span class="text-red-500">*</span></label>
<input type="text" id="nom" name="nom" value="{{ old('nom', $source?->nom) }}" required <input type="text" id="nom" name="nom" value="{{ old('nom', $source?->nom) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror">
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $source?->description) }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $source?->description) }}</textarea>
</div> </div>
{{-- Section propriétaire --}} {{-- Section propriétaire --}}
@if($sections->isNotEmpty()) @if($sections->isNotEmpty())
<div> <div>
<label for="section_id" class="block text-sm font-medium text-gray-700">Section</label> <label for="section_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Section</label>
<select id="section_id" name="section_id" <select id="section_id" name="section_id"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm">
<option value=""> Aucune (globale) </option> <option value=""> Aucune (globale) </option>
@foreach($sections as $sec) @foreach($sections as $sec)
<option value="{{ $sec->id }}" {{ old('section_id', $source?->section_id) == $sec->id ? 'selected' : '' }}> <option value="{{ $sec->id }}" {{ old('section_id', $source?->section_id) == $sec->id ? 'selected' : '' }}>
@@ -31,9 +31,9 @@
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label for="source_type_id" class="block text-sm font-medium text-gray-700">Type de source <span class="text-red-500">*</span></label> <label for="source_type_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Type de source <span class="text-red-500">*</span></label>
<select id="source_type_id" name="source_type_id" required <select id="source_type_id" name="source_type_id" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('source_type_id') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('source_type_id') border-red-500 @enderror">
<option value=""> Choisir </option> <option value=""> Choisir </option>
@foreach($sourceTypes as $st) @foreach($sourceTypes as $st)
<option value="{{ $st->id }}" {{ old('source_type_id', $source?->source_type_id) == $st->id ? 'selected' : '' }}> <option value="{{ $st->id }}" {{ old('source_type_id', $source?->source_type_id) == $st->id ? 'selected' : '' }}>
@@ -45,9 +45,9 @@
</div> </div>
<div> <div>
<label for="depot_id" class="block text-sm font-medium text-gray-700">Dépôt d'archives</label> <label for="depot_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Dépôt d'archives</label>
<select id="depot_id" name="depot_id" <select id="depot_id" name="depot_id"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value=""> Aucun </option> <option value=""> Aucun </option>
@foreach($depots as $depot) @foreach($depots as $depot)
<option value="{{ $depot->id }}" {{ old('depot_id', $source?->depot_id) == $depot->id ? 'selected' : '' }}> <option value="{{ $depot->id }}" {{ old('depot_id', $source?->depot_id) == $depot->id ? 'selected' : '' }}>
@@ -83,33 +83,33 @@
{{-- Période couverte --}} {{-- Période couverte --}}
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label for="annee_debut" class="block text-sm font-medium text-gray-700">Année de début</label> <label for="annee_debut" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Année de début</label>
<input type="number" id="annee_debut" name="annee_debut" <input type="number" id="annee_debut" name="annee_debut"
value="{{ old('annee_debut', $source?->annee_debut) }}" value="{{ old('annee_debut', $source?->annee_debut) }}"
min="1000" max="2100" placeholder="ex : 1820" min="1000" max="2100" placeholder="ex : 1820"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('annee_debut') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('annee_debut') border-red-500 @enderror">
@error('annee_debut') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('annee_debut') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
<div> <div>
<label for="annee_fin" class="block text-sm font-medium text-gray-700">Année de fin</label> <label for="annee_fin" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Année de fin</label>
<input type="number" id="annee_fin" name="annee_fin" <input type="number" id="annee_fin" name="annee_fin"
value="{{ old('annee_fin', $source?->annee_fin) }}" value="{{ old('annee_fin', $source?->annee_fin) }}"
min="1000" max="2100" placeholder="ex : 1870" min="1000" max="2100" placeholder="ex : 1870"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('annee_fin') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('annee_fin') border-red-500 @enderror">
@error('annee_fin') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('annee_fin') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<label for="cote" class="block text-sm font-medium text-gray-700">Cote</label> <label for="cote" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Cote</label>
<input type="text" id="cote" name="cote" value="{{ old('cote', $source?->cote) }}" <input type="text" id="cote" name="cote" value="{{ old('cote', $source?->cote) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
<div> <div>
<label for="auteur" class="block text-sm font-medium text-gray-700">Auteur</label> <label for="auteur" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Auteur</label>
<input type="text" id="auteur" name="auteur" value="{{ old('auteur', $source?->auteur) }}" <input type="text" id="auteur" name="auteur" value="{{ old('auteur', $source?->auteur) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
</div> </div>
</div> </div>
+3 -3
View File
@@ -1,13 +1,13 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Nouvelle source</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Nouvelle source</h2></x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('sources.store') }}"> <form method="POST" action="{{ route('sources.store') }}">
@csrf @csrf
@include('sources._form', ['source' => null]) @include('sources._form', ['source' => null])
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button>
<a href="{{ route('sources.index') }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a> <a href="{{ route('sources.index') }}" class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+3 -3
View File
@@ -1,13 +1,13 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Modifier : {{ $source->nom }}</h2></x-slot> <x-slot name="header"><h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Modifier : {{ $source->nom }}</h2></x-slot>
<div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="py-8 max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<form method="POST" action="{{ route('sources.update', $source) }}"> <form method="POST" action="{{ route('sources.update', $source) }}">
@csrf @method('PUT') @csrf @method('PUT')
@include('sources._form', ['source' => $source]) @include('sources._form', ['source' => $source])
<div class="mt-6 flex gap-4"> <div class="mt-6 flex gap-4">
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button> <button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button>
<a href="{{ route('sources.show', $source) }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a> <a href="{{ route('sources.show', $source) }}" class="text-sm text-gray-500 dark:text-gray-400 self-center hover:text-gray-700 dark:hover:text-gray-300">Annuler</a>
</div> </div>
</form> </form>
</div> </div>
+38 -38
View File
@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-800">Sources</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">Sources</h2>
@can('create', App\Models\Source::class) @can('create', App\Models\Source::class)
<a href="{{ route('sources.create') }}" <a href="{{ route('sources.create') }}"
class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouvelle source</a> class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouvelle source</a>
@@ -11,22 +11,22 @@
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@if(session('success')) @if(session('success'))
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> <div class="p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200 rounded-md">{{ session('success') }}</div>
@endif @endif
{{-- Filtres --}} {{-- Filtres --}}
@php @php
$hasFilters = request()->anyFilled(['status', 'source_type_id', 'lieu_id', 'annee_debut', 'annee_fin']); $hasFilters = request()->anyFilled(['status', 'source_type_id', 'lieu_id', 'annee_debut', 'annee_fin']);
@endphp @endphp
<div class="bg-white shadow rounded-lg p-5"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-5">
<form method="GET" action="{{ route('sources.index') }}"> <form method="GET" action="{{ route('sources.index') }}">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
{{-- Statut --}} {{-- Statut --}}
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Statut</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Statut</label>
<select name="status" <select name="status"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value=""> Tous </option> <option value=""> Tous </option>
@foreach(\App\Enums\SourceStatus::cases() as $s) @foreach(\App\Enums\SourceStatus::cases() as $s)
<option value="{{ $s->value }}" {{ request('status') === $s->value ? 'selected' : '' }}> <option value="{{ $s->value }}" {{ request('status') === $s->value ? 'selected' : '' }}>
@@ -38,9 +38,9 @@
{{-- Type de source --}} {{-- Type de source --}}
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Type de source</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Type de source</label>
<select name="source_type_id" <select name="source_type_id"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value=""> Tous </option> <option value=""> Tous </option>
@foreach($sourceTypes as $st) @foreach($sourceTypes as $st)
<option value="{{ $st->id }}" {{ request('source_type_id') == $st->id ? 'selected' : '' }}> <option value="{{ $st->id }}" {{ request('source_type_id') == $st->id ? 'selected' : '' }}>
@@ -52,18 +52,18 @@
{{-- Année de début --}} {{-- Année de début --}}
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Période de</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Période de</label>
<input type="number" name="annee_debut" value="{{ request('annee_debut') }}" <input type="number" name="annee_debut" value="{{ request('annee_debut') }}"
min="1000" max="2100" placeholder="ex : 1820" min="1000" max="2100" placeholder="ex : 1820"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
{{-- Année de fin --}} {{-- Année de fin --}}
<div> <div>
<label class="block text-xs font-medium text-gray-600 mb-1">Période à</label> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Période à</label>
<input type="number" name="annee_fin" value="{{ request('annee_fin') }}" <input type="number" name="annee_fin" value="{{ request('annee_fin') }}"
min="1000" max="2100" placeholder="ex : 1870" min="1000" max="2100" placeholder="ex : 1870"
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> </div>
</div> </div>
@@ -85,10 +85,10 @@
</button> </button>
@if($hasFilters) @if($hasFilters)
<a href="{{ route('sources.index') }}" <a href="{{ route('sources.index') }}"
class="px-4 py-2 border border-gray-300 text-gray-600 text-sm rounded-md hover:bg-gray-50"> class="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 text-sm rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
Effacer les filtres Effacer les filtres
</a> </a>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-indigo-100 text-indigo-700"> <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700">
filtres actifs filtres actifs
</span> </span>
@endif @endif
@@ -97,30 +97,30 @@
</div> </div>
{{-- Tableau --}} {{-- Tableau --}}
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Statut</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Statut</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Lieu</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Lieu</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Période</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Période</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Relevés</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Relevés</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Dépôt</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Dépôt</th>
<th class="px-6 py-3"></th> <th class="px-6 py-3"></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($sources as $source) @forelse($sources as $source)
@php @php
$statusColors = [ $statusColors = [
'a_faire' => 'bg-gray-100 text-gray-600', 'a_faire' => 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400',
'en_cours' => 'bg-blue-100 text-blue-700', 'en_cours' => 'bg-blue-100 dark:bg-blue-900/50 text-blue-700',
'a_valider' => 'bg-yellow-100 text-yellow-700', 'a_valider' => 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700',
'termine' => 'bg-green-100 text-green-700', 'termine' => 'bg-green-100 dark:bg-green-900/50 text-green-700',
]; ];
$color = $statusColors[$source->status->value] ?? 'bg-gray-100 text-gray-600'; $color = $statusColors[$source->status->value] ?? 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
$periode = match(true) { $periode = match(true) {
$source->annee_debut && $source->annee_fin => $source->annee_debut . ' ' . $source->annee_fin, $source->annee_debut && $source->annee_fin => $source->annee_debut . ' ' . $source->annee_fin,
(bool)$source->annee_debut => 'depuis ' . $source->annee_debut, (bool)$source->annee_debut => 'depuis ' . $source->annee_debut,
@@ -128,32 +128,32 @@
default => '—', default => '—',
}; };
@endphp @endphp
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 font-medium"> <td class="px-6 py-4 font-medium">
<a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a> <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
@if($source->cote) <span class="ml-2 text-xs text-gray-400">{{ $source->cote }}</span> @endif @if($source->cote) <span class="ml-2 text-xs text-gray-400 dark:text-gray-500">{{ $source->cote }}</span> @endif
</td> </td>
<td class="px-6 py-4 text-gray-500">{{ $source->sourceType->nom }}</td> <td class="px-6 py-4 text-gray-500 dark:text-gray-400">{{ $source->sourceType->nom }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $color }}"> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $color }}">
{{ $source->status->label() }} {{ $source->status->label() }}
</span> </span>
</td> </td>
<td class="px-6 py-4 text-gray-500 max-w-[180px] truncate" title="{{ $source->lieu?->nom_long ?? $source->lieu?->nom }}"> <td class="px-6 py-4 text-gray-500 dark:text-gray-400 max-w-[180px] truncate" title="{{ $source->lieu?->nom_long ?? $source->lieu?->nom }}">
{{ $source->lieu?->nom ?? '—' }} {{ $source->lieu?->nom ?? '—' }}
</td> </td>
<td class="px-6 py-4 text-gray-500 whitespace-nowrap">{{ $periode }}</td> <td class="px-6 py-4 text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ $periode }}</td>
<td class="px-6 py-4 text-gray-500">{{ $source->releves_count }}</td> <td class="px-6 py-4 text-gray-500 dark:text-gray-400">{{ $source->releves_count }}</td>
<td class="px-6 py-4 text-gray-500">{{ $source->depot?->nom ?? '—' }}</td> <td class="px-6 py-4 text-gray-500 dark:text-gray-400">{{ $source->depot?->nom ?? '—' }}</td>
<td class="px-6 py-4 text-right text-sm space-x-3"> <td class="px-6 py-4 text-right text-sm space-x-3">
@can('update', $source) @can('update', $source)
<a href="{{ route('sources.edit', $source) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a> <a href="{{ route('sources.edit', $source) }}" class="text-gray-600 dark:text-gray-400 hover:text-indigo-600">Modifier</a>
@endcan @endcan
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="8" class="px-6 py-10 text-center text-gray-400"> <td colspan="8" class="px-6 py-10 text-center text-gray-400 dark:text-gray-500">
@if($hasFilters) Aucune source ne correspond aux filtres. @if($hasFilters) Aucune source ne correspond aux filtres.
@else Aucune source disponible. @endif @else Aucune source disponible. @endif
</td> </td>
+32 -33
View File
@@ -2,15 +2,15 @@
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h2 class="text-xl font-semibold text-gray-800">{{ $source->nom }}</h2> <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">{{ $source->nom }}</h2>
@if($source->cote) @if($source->cote)
<p class="text-sm text-gray-500 mt-0.5">Cote : {{ $source->cote }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Cote : {{ $source->cote }}</p>
@endif @endif
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@can('update', $source) @can('update', $source)
<a href="{{ route('sources.edit', $source) }}" <a href="{{ route('sources.edit', $source) }}"
class="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm rounded-md hover:bg-gray-50">Modifier</a> class="px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-sm rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">Modifier</a>
@endcan @endcan
@can('delete', $source) @can('delete', $source)
<form method="POST" action="{{ route('sources.destroy', $source) }}" <form method="POST" action="{{ route('sources.destroy', $source) }}"
@@ -26,7 +26,7 @@
<div class="py-8 max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6"> <div class="py-8 max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
@foreach(['success','error'] as $flash) @foreach(['success','error'] as $flash)
@if(session($flash)) @if(session($flash))
<div class="p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 border border-green-200 text-green-800' : 'bg-red-50 border border-red-200 text-red-800' }}"> <div class="p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-700 text-green-800 dark:text-green-200' : 'bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200' }}">
{{ session($flash) }} {{ session($flash) }}
</div> </div>
@endif @endif
@@ -34,33 +34,33 @@
<div class="grid grid-cols-3 gap-6"> <div class="grid grid-cols-3 gap-6">
{{-- Fiche source --}} {{-- Fiche source --}}
<div class="col-span-2 bg-white shadow rounded-lg divide-y divide-gray-100 text-sm"> <div class="col-span-2 bg-white dark:bg-gray-800 shadow rounded-lg divide-y divide-gray-100 dark:divide-gray-700 text-sm">
@foreach([ @foreach([
['Type', $source->sourceType->nom], ['Type', $source->sourceType->nom],
['Dépôt', $source->depot?->nom ?? '—'], ['Dépôt', $source->depot?->nom ?? '—'],
['Auteur', $source->auteur ?? '—'], ['Auteur', $source->auteur ?? '—'],
] as [$label, $val]) ] as [$label, $val])
<div class="px-6 py-4 grid grid-cols-3 gap-4"> <div class="px-6 py-4 grid grid-cols-3 gap-4">
<dt class="font-medium text-gray-500">{{ $label }}</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">{{ $label }}</dt>
<dd class="col-span-2 text-gray-900">{{ $val }}</dd> <dd class="col-span-2 text-gray-900 dark:text-white">{{ $val }}</dd>
</div> </div>
@endforeach @endforeach
@if($source->description) @if($source->description)
<div class="px-6 py-4 grid grid-cols-3 gap-4"> <div class="px-6 py-4 grid grid-cols-3 gap-4">
<dt class="font-medium text-gray-500">Description</dt> <dt class="font-medium text-gray-500 dark:text-gray-400">Description</dt>
<dd class="col-span-2 text-gray-900 whitespace-pre-line">{{ $source->description }}</dd> <dd class="col-span-2 text-gray-900 dark:text-white whitespace-pre-line">{{ $source->description }}</dd>
</div> </div>
@endif @endif
</div> </div>
{{-- Statut + transitions --}} {{-- Statut + transitions --}}
<div class="bg-white shadow rounded-lg p-6 space-y-4"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 space-y-4">
@php @php
$statusColors = ['a_faire'=>'gray','en_cours'=>'blue','a_valider'=>'yellow','termine'=>'green']; $statusColors = ['a_faire'=>'gray','en_cours'=>'blue','a_valider'=>'yellow','termine'=>'green'];
$color = $statusColors[$source->status->value] ?? 'gray'; $color = $statusColors[$source->status->value] ?? 'gray';
@endphp @endphp
<div> <div>
<p class="text-xs font-medium text-gray-500 uppercase mb-2">Statut</p> <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">Statut</p>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-{{ $color }}-100 text-{{ $color }}-700"> <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-{{ $color }}-100 text-{{ $color }}-700">
{{ $source->status->label() }} {{ $source->status->label() }}
</span> </span>
@@ -70,14 +70,14 @@
@php $transitions = $source->status->transitions(); @endphp @php $transitions = $source->status->transitions(); @endphp
@if(count($transitions)) @if(count($transitions))
<div class="space-y-2"> <div class="space-y-2">
<p class="text-xs font-medium text-gray-500 uppercase">Changer le statut</p> <p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Changer le statut</p>
@foreach($transitions as $next) @foreach($transitions as $next)
@if($source->canTransitionTo($next, auth()->user())) @if($source->canTransitionTo($next, auth()->user()))
<form method="POST" action="{{ route('sources.transition', $source) }}"> <form method="POST" action="{{ route('sources.transition', $source) }}">
@csrf @csrf
<input type="hidden" name="status" value="{{ $next->value }}"> <input type="hidden" name="status" value="{{ $next->value }}">
<button type="submit" <button type="submit"
class="w-full text-left px-3 py-2 text-sm border border-gray-200 rounded-md hover:bg-gray-50 hover:border-indigo-300 transition-colors"> class="w-full text-left px-3 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-indigo-300 transition-colors">
{{ $next->label() }} {{ $next->label() }}
</button> </button>
</form> </form>
@@ -91,17 +91,17 @@
{{-- Membres assignés --}} {{-- Membres assignés --}}
@can('assignMembre', $source) @can('assignMembre', $source)
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 font-medium text-gray-900"> <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 font-medium text-gray-900 dark:text-white">
Membres assignés ({{ $source->membres->count() }}) Membres assignés ({{ $source->membres->count() }})
</div> </div>
@if($source->membres->isNotEmpty()) @if($source->membres->isNotEmpty())
<table class="min-w-full divide-y divide-gray-200 text-sm"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($source->membres as $membre) @foreach($source->membres as $membre)
<tr> <tr>
<td class="px-6 py-3">{{ $membre->name }}</td> <td class="px-6 py-3">{{ $membre->name }}</td>
<td class="px-6 py-3 text-gray-500">{{ $membre->email }}</td> <td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ $membre->email }}</td>
<td class="px-6 py-3 text-right"> <td class="px-6 py-3 text-right">
<form method="POST" action="{{ route('sources.membres.remove', [$source, $membre]) }}" <form method="POST" action="{{ route('sources.membres.remove', [$source, $membre]) }}"
x-data @submit.prevent="if(confirm('Retirer ce membre ?')) $el.submit()"> x-data @submit.prevent="if(confirm('Retirer ce membre ?')) $el.submit()">
@@ -114,27 +114,26 @@
</tbody> </tbody>
</table> </table>
@endif @endif
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200"> <div class="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-700">
<form method="POST" action="{{ route('sources.membres.add', $source) }}" class="flex gap-3 items-end"> <form method="POST" action="{{ route('sources.membres.add', $source) }}"
@submit.prevent="if ($el.querySelector('[name=user_id]').value) $el.submit()">
@csrf @csrf
<div class="flex-1"> <div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-end">
<label class="block text-xs font-medium text-gray-600 mb-1">Ajouter un membre</label> <div class="flex-1">
<select name="user_id" class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500"> <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Ajouter un membre</label>
@foreach($availableUsers as $u) <x-user-picker :users="$availableUsers" placeholder="Rechercher un membre à assigner…" required />
<option value="{{ $u->id }}">{{ $u->name }} ({{ $u->email }})</option> </div>
@endforeach <button type="submit" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700 shrink-0">Assigner</button>
</select>
</div> </div>
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">Assigner</button>
</form> </form>
</div> </div>
</div> </div>
@endcan @endcan
{{-- Liste des relevés (aperçu) --}} {{-- Liste des relevés (aperçu) --}}
<div class="bg-white shadow rounded-lg overflow-hidden"> <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between"> <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 class="font-medium text-gray-900">Relevés ({{ $source->releves->count() }})</h3> <h3 class="font-medium text-gray-900 dark:text-white">Relevés ({{ $source->releves->count() }})</h3>
@can('create', [App\Models\Releve::class, $source]) @can('create', [App\Models\Releve::class, $source])
<a href="{{ route('sources.releves.create', $source) }}" <a href="{{ route('sources.releves.create', $source) }}"
class="px-3 py-1.5 bg-indigo-600 text-white text-xs rounded-md hover:bg-indigo-700"> class="px-3 py-1.5 bg-indigo-600 text-white text-xs rounded-md hover:bg-indigo-700">
@@ -143,9 +142,9 @@
@endcan @endcan
</div> </div>
@if($source->releves->isEmpty()) @if($source->releves->isEmpty())
<p class="px-6 py-8 text-center text-gray-400 text-sm">Aucun relevé pour cette source.</p> <p class="px-6 py-8 text-center text-gray-400 dark:text-gray-500 text-sm">Aucun relevé pour cette source.</p>
@else @else
<p class="px-6 py-4 text-sm text-gray-500"> <p class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
<a href="{{ route('sources.releves.index', $source) }}" class="text-indigo-600 hover:underline"> <a href="{{ route('sources.releves.index', $source) }}" class="text-indigo-600 hover:underline">
Voir les {{ $source->releves->count() }} relevé(s) Voir les {{ $source->releves->count() }} relevé(s)
</a> </a>
+5 -5
View File
@@ -14,7 +14,7 @@
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet"/> <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet"/>
@endif @endif
</head> </head>
<body class="font-sans antialiased bg-gray-50 min-h-screen flex flex-col items-center justify-center px-4"> <body class="font-sans antialiased bg-gray-50 dark:bg-gray-700 min-h-screen flex flex-col items-center justify-center px-4">
<div class="text-center max-w-md w-full"> <div class="text-center max-w-md w-full">
@@ -23,10 +23,10 @@
<img src="{{ $siteLogoUrl }}" alt="{{ config('app.name') }}" <img src="{{ $siteLogoUrl }}" alt="{{ config('app.name') }}"
class="h-28 w-auto object-contain mx-auto mb-8"> class="h-28 w-auto object-contain mx-auto mb-8">
@else @else
<h1 class="text-3xl font-bold text-gray-900 mb-4">{{ config('app.name') }}</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">{{ config('app.name') }}</h1>
@endif @endif
<p class="text-gray-500 text-sm mb-10"> <p class="text-gray-500 dark:text-gray-400 text-sm mb-10">
Application de relevés généalogiques Application de relevés généalogiques
</p> </p>
@@ -43,7 +43,7 @@
</a> </a>
@if($registrationEnabled && Route::has('register')) @if($registrationEnabled && Route::has('register'))
<a href="{{ route('register') }}" <a href="{{ route('register') }}"
class="px-8 py-3 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-100 transition-colors"> class="px-8 py-3 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
Créer un compte Créer un compte
</a> </a>
@endif @endif
@@ -51,7 +51,7 @@
@endauth @endauth
</div> </div>
<footer class="absolute bottom-6 text-xs text-gray-300"> <footer class="absolute bottom-6 text-xs text-gray-300 dark:text-gray-600">
{{ config('app.name') }} {{ config('app.name') }}
</footer> </footer>
+1
View File
@@ -20,6 +20,7 @@ Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->grou
Route::post('parametres/smtp', [SettingController::class, 'updateSmtp'])->name('parametres.smtp.update'); Route::post('parametres/smtp', [SettingController::class, 'updateSmtp'])->name('parametres.smtp.update');
Route::delete('parametres/smtp', [SettingController::class, 'deleteSmtp'])->name('parametres.smtp.delete'); Route::delete('parametres/smtp', [SettingController::class, 'deleteSmtp'])->name('parametres.smtp.delete');
Route::post('parametres/smtp/test', [SettingController::class, 'testSmtp'])->name('parametres.smtp.test'); Route::post('parametres/smtp/test', [SettingController::class, 'testSmtp'])->name('parametres.smtp.test');
Route::post('parametres/updates', [SettingController::class, 'updateUpdates'])->name('parametres.updates');
// Routes spécifiques avant la resource pour éviter les conflits de paramètre // Routes spécifiques avant la resource pour éviter les conflits de paramètre
Route::get('utilisateurs/export', [UserController::class, 'export'])->name('utilisateurs.export'); Route::get('utilisateurs/export', [UserController::class, 'export'])->name('utilisateurs.export');
+3
View File
@@ -1,5 +1,6 @@
<?php <?php
use App\Http\Controllers\CarteController;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ExportController; use App\Http\Controllers\ExportController;
use App\Http\Controllers\LieuController; use App\Http\Controllers\LieuController;
@@ -57,6 +58,8 @@ Route::middleware('auth')->group(function () {
->parameters(['releves' => 'releve']); ->parameters(['releves' => 'releve']);
Route::get('recherche', [RechercheController::class, 'index'])->name('recherche'); Route::get('recherche', [RechercheController::class, 'index'])->name('recherche');
Route::get('carte', [CarteController::class, 'index'])->name('carte');
Route::get('carte/data', [CarteController::class, 'data'])->name('carte.data');
Route::get('export/source/{source}', [ExportController::class, 'source'])->name('export.source'); Route::get('export/source/{source}', [ExportController::class, 'source'])->name('export.source');
Route::get('export/recherche', [ExportController::class, 'recherche'])->name('export.recherche'); Route::get('export/recherche', [ExportController::class, 'recherche'])->name('export.recherche');
+1
View File
@@ -3,6 +3,7 @@ import forms from '@tailwindcss/forms';
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: 'class',
content: [ content: [
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php', './storage/framework/views/*.php',