Compare commits

..

2 Commits

Author SHA1 Message Date
5295e8ec72 Merge develop (squash): login mobile, formulaire sous slogan par ratio
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 18:54:16 +01:00
6bf0932da8 docs: Index, doc API Gitea et script fermeture issue
- docs/00_INDEX.md : entrée pour 26_GITEA-API
- docs/26_GITEA-API.md : procédure API Gitea (auth, issues, PR, branches, dépannage)
- scripts/gitea-close-issue.sh : fermer une issue via l'API (GITEA_TOKEN / .gitea-token)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 16:39:47 +01:00
7 changed files with 483 additions and 30 deletions

View File

@ -27,6 +27,7 @@ Ce fichier sert d'index pour naviguer dans toute la documentation du projet.
- [**23 - Liste des Tickets**](./23_LISTE-TICKETS.md) - 61 tickets Phase 1 détaillés
- [**24 - Décisions Projet**](./24_DECISIONS-PROJET.md) - Décisions architecturales et fonctionnelles
- [**25 - Backlog Phase 2**](./25_PHASE-2-BACKLOG.md) - Fonctionnalités techniques reportées
- [**26 - API Gitea**](./26_GITEA-API.md) - Procédure d'utilisation de l'API Gitea (issues, PR, branches, labels)
### Administration (À créer)
- [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs

View File

@ -1,12 +1,37 @@
# 🎫 Liste Complète des Tickets - Projet P'titsPas
**Version** : 1.1
**Version** : 1.2
**Date** : 27 Janvier 2026
**Auteur** : Équipe PtitsPas
**Estimation totale** : ~184h
---
## 🔗 Liste des tickets Gitea
Correspondance entre les numéros dissues Gitea et les tickets de ce document.
| Gitea # | Titre court | Priorité | Statut | Section doc |
|--------|--------------|----------|--------|-------------|
| 1 | BDD - Champs manquants CDC | P0 | Ouvert | § Ticket #1 |
| 2 | BDD - Table présentation dossier parent | P0 | Ouvert | § Ticket #2 |
| 3 | BDD - Tokens création MDP | P0 | ✅ Fermé | § Ticket #3 |
| 4 | BDD - Champ genre enfants | P0 | ✅ Fermé | § Ticket #4 |
| 5 | BDD - Supprimer champs obsolètes | P0 | Ouvert | § Ticket #5 |
| 6 | BDD - Table configuration système | P0 | Ouvert | § Ticket #6 |
| 68 | BDD - Documents légaux & acceptations | P0 | ✅ Fermé | § Ticket #7 |
| 73 | Frontend - Inscription Parent Étape 1 | P3 | ✅ Fermé (PR) | § Ticket #36 |
| 78 | Frontend - Infrastructure formulaires multi-modes | P3 | ✅ Fermé | § Ticket #78 |
| 79 | Frontend - Renommer Nanny en AM | P3 | ✅ Fermé | § Ticket #79 |
| 81 | Frontend - Corrections refactoring widgets | P3 | ✅ Fermé | § Ticket #81 |
| 83 | Frontend - RegisterChoiceScreen mobile | P3 | ✅ Fermé | § Ticket #83 |
*Les autres tickets (sans numéro Gitea dans ce tableau) sont décrits dans les sections par priorité cidessous ; les numéros de section (#1 à #83) sont les références internes du document.*
**Point API (tickets frontend)** 27/01/2026 : 20 issues avec le label `frontend` dans Gitea (12 ouvertes, 8 fermées). Numéros concernés : 3542, 4351, 54, 82, 83. Les #73, #78, #79, #81 sont fermés mais sans label dans lAPI. Détail : `docs/POINT_TICKETS_FRONT_API.txt`.
---
## 📊 Vue d'ensemble
### Répartition par priorité
@ -1218,6 +1243,6 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit
---
**Dernière mise à jour** : 27 Janvier 2026
**Version** : 1.1
**Version** : 1.2
**Statut** : ✅ À jour

176
docs/26_GITEA-API.md Normal file
View File

@ -0,0 +1,176 @@
# Procédure Utilisation de l'API Gitea
## 1. Contexte
- **Instance** : https://git.ptits-pas.fr
- **API de base** : `https://git.ptits-pas.fr/api/v1`
- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`)
## 2. Authentification
### 2.1 Token
Le token est défini dans l'environnement (ex. `~/.bashrc`) :
```bash
export GITEA_TOKEN="<votre_token>"
```
Pour l'utiliser dans les commandes :
```bash
source ~/.bashrc # ou : . ~/.bashrc
# Puis utiliser $GITEA_TOKEN dans les curl
```
### 2.2 En-tête HTTP
Toutes les requêtes API doivent envoyer le token :
```bash
-H "Authorization: token $GITEA_TOKEN"
```
Exemple :
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas"
```
## 3. Endpoints utiles
### 3.1 Dépôt (repository)
| Action | Méthode | URL |
|---------------|---------|-----|
| Infos dépôt | GET | `/repos/{owner}/{repo}` |
| Liste dépôts | GET | `/repos/search?q=petitspas` |
Exemple infos du dépôt :
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq .
```
### 3.2 Issues (tickets)
| Action | Méthode | URL |
|------------------|---------|-----|
| Liste des issues | GET | `/repos/{owner}/{repo}/issues` |
| Détail d'une issue | GET | `/repos/{owner}/{repo}/issues/{index}` |
| Créer une issue | POST | `/repos/{owner}/{repo}/issues` |
| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` |
| Fermer une issue | PATCH | (même URL, `state: "closed"`) |
**Paramètres GET utiles pour la liste :**
- `state` : `open` ou `closed`
- `labels` : filtre par label (ex. `frontend`)
- `page`, `limit` : pagination
Exemples :
```bash
# Toutes les issues ouvertes
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq .
# Issues ouvertes avec label "frontend"
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \
jq '.[] | select(.labels[].name == "frontend") | {number, title, state}'
# Détail de l'issue #47
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
# Fermer l'issue #31
curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31"
# Créer une issue
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
```
### 3.3 Pull requests
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` |
| Détail d'une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` |
| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` |
| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` |
Exemples :
```bash
# Liste des PR ouvertes
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq .
# Créer une PR (head = branche source, base = branche cible)
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"head":"develop","base":"master","title":"Titre de la PR"}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
```
### 3.4 Branches
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des branches | GET | `/repos/{owner}/{repo}/branches` |
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name'
```
### 3.5 Webhooks
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` |
| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` |
### 3.6 Labels
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des labels | GET | `/repos/{owner}/{repo}/labels` |
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}'
```
## 4. Résumé des URLs pour P'titsPas
Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` :
| Ressource | URL |
|------------------|-----|
| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` |
| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` |
| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` |
| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` |
| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` |
| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` |
## 5. Documentation officielle
- Swagger / OpenAPI : https://docs.gitea.com/api
- Référence selon la version de Gitea installée (ex. 1.21, 1.25).
## 6. Dépannage
- **401 Unauthorized** : vérifier le token et l'en-tête `Authorization: token <TOKEN>`.
- **404** : vérifier owner/repo et l'URL (sensible à la casse).
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

View File

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:url_launcher/url_launcher.dart';
import 'package:go_router/go_router.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
@ -17,7 +16,7 @@ class LoginScreen extends StatefulWidget {
State<LoginScreen> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginScreen> {
class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@ -25,13 +24,28 @@ class _LoginPageState extends State<LoginScreen> {
bool _isLoading = false;
String? _errorMessage;
static const double _mobileBreakpoint = 900.0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
if (mounted) setState(() {});
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
@ -122,16 +136,20 @@ class _LoginPageState extends State<LoginScreen> {
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < _mobileBreakpoint;
if (isMobile) {
return Scaffold(
backgroundColor: Colors.transparent,
body: _buildMobileLayout(context),
);
}
return Scaffold(
backgroundColor: Colors.transparent,
body: LayoutBuilder(
builder: (context, constraints) {
// Version desktop (web)
if (kIsWeb) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return FutureBuilder(
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return FutureBuilder(
future: _getImageDimensions(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
@ -289,18 +307,19 @@ class _LoginPageState extends State<LoginScreen> {
),
),
),
// Pied de page
// Pied de page (Wrap pour éviter overflow sur petite largeur)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: Colors.transparent,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
child: Wrap(
alignment: WrapAlignment.center,
runSpacing: 8,
children: [
_FooterLink(
text: 'Contact support',
@ -349,17 +368,207 @@ class _LoginPageState extends State<LoginScreen> {
);
},
);
}
// Version mobile (à implémenter)
return const Center(
child: Text('Version mobile à implémenter'),
);
},
),
);
}
/// Dimensions de river_logo_mobile.png (à mettre à jour si l'asset change).
static const int _riverLogoMobileWidth = 600;
static const int _riverLogoMobileHeight = 1080;
/// Fraction de la hauteur de l'image où se termine visuellement le slogan (0 = haut, 1 = bas).
static const double _sloganEndFraction = 0.42;
static const double _gapBelowSlogan = 12.0;
Widget _buildMobileLayout(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final h = constraints.maxHeight;
final w = constraints.maxWidth;
final imageAspectRatio = _riverLogoMobileHeight / _riverLogoMobileWidth;
final formTop = w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan;
return Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
Positioned(
top: 0,
left: 0,
right: 0,
height: h * 1.2,
child: OverflowBox(
alignment: Alignment.topCenter,
minWidth: w,
maxWidth: w,
minHeight: 0,
maxHeight: h * 2.5,
child: Image.asset(
'assets/images/river_logo_mobile.png',
width: w,
fit: BoxFit.fitWidth,
),
),
),
Positioned(
top: formTop,
left: 0,
right: 0,
bottom: 0,
child: SafeArea(
top: false,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
CustomAppTextField(
controller: _emailController,
labelText: 'Email',
showLabel: false,
hintText: 'Votre adresse email',
validator: _validateEmail,
style: CustomAppTextFieldStyle.lavande,
fieldHeight: 48,
fieldWidth: double.infinity,
),
const SizedBox(height: 12),
CustomAppTextField(
controller: _passwordController,
labelText: 'Mot de passe',
showLabel: false,
hintText: 'Votre mot de passe',
obscureText: true,
validator: _validatePassword,
style: CustomAppTextFieldStyle.jaune,
fieldHeight: 48,
fieldWidth: double.infinity,
),
if (_errorMessage != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.red.shade300),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red.shade700, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
_errorMessage!,
style: GoogleFonts.merienda(fontSize: 12, color: Colors.red.shade700),
),
),
],
),
),
],
const SizedBox(height: 12),
_isLoading
? const CircularProgressIndicator()
: ImageButton(
bg: 'assets/images/bg_green.png',
width: double.infinity,
height: 44,
text: 'Se connecter',
textColor: const Color(0xFF2D6A4F),
onPressed: _handleLogin,
),
const SizedBox(height: 12),
TextButton(
onPressed: () { /* TODO */ },
child: Text(
'Mot de passe oublié ?',
style: GoogleFonts.merienda(
fontSize: 14,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
TextButton(
onPressed: () => context.go('/register-choice'),
child: Text(
'Créer un compte',
style: GoogleFonts.merienda(
fontSize: 16,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 12, top: 8),
child: Wrap(
alignment: WrapAlignment.center,
runSpacing: 6,
spacing: 4,
children: [
_FooterLink(
text: 'Contact support',
fontSize: 11,
onTap: () async {
final uri = Uri(scheme: 'mailto', path: 'support@supernounou.local');
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Impossible d\'ouvrir le client mail', style: GoogleFonts.merienda())),
);
}
},
),
_FooterLink(
text: 'Signaler un bug',
fontSize: 11,
onTap: () => _showBugReportDialog(context),
),
_FooterLink(
text: 'Mentions légales',
fontSize: 11,
onTap: () => context.go('/legal'),
),
_FooterLink(
text: 'Politique de confidentialité',
fontSize: 11,
onTap: () => context.go('/privacy'),
),
],
),
),
],
),
),
),
],
);
},
);
}
void _showBugReportDialog(BuildContext context) {
final TextEditingController controller = TextEditingController();
@ -471,10 +680,12 @@ class ImageDimensions {
class _FooterLink extends StatelessWidget {
final String text;
final VoidCallback onTap;
final double fontSize;
const _FooterLink({
required this.text,
required this.onTap,
this.fontSize = 14.0,
});
@override
@ -482,11 +693,11 @@ class _FooterLink extends StatelessWidget {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: EdgeInsets.symmetric(horizontal: fontSize > 12 ? 8.0 : 4.0),
child: Text(
text,
style: GoogleFonts.merienda(
fontSize: 14,
fontSize: fontSize,
color: Colors.black87,
decoration: TextDecoration.underline,
),

View File

@ -25,11 +25,13 @@ class CustomAppTextField extends StatefulWidget {
final IconData? suffixIcon;
final double labelFontSize;
final double inputFontSize;
final bool showLabel;
const CustomAppTextField({
super.key,
required this.controller,
required this.labelText,
this.showLabel = true,
this.hintText = '',
this.fieldWidth = 300.0,
this.fieldHeight = 53.0,
@ -73,15 +75,17 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.labelText,
style: GoogleFonts.merienda(
fontSize: widget.labelFontSize,
color: Colors.black87,
fontWeight: FontWeight.w500,
if (widget.showLabel) ...[
Text(
widget.labelText,
style: GoogleFonts.merienda(
fontSize: widget.labelFontSize,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 6),
const SizedBox(height: 6),
],
SizedBox(
width: widget.fieldWidth,
height: dynamicFieldHeight,

View File

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Ferme une issue Gitea via l'API.
# Usage: GITEA_TOKEN=votre_token ./scripts/gitea-close-issue.sh [numéro]
# Exemple: GITEA_TOKEN=xxx ./scripts/gitea-close-issue.sh 83
set -e
ISSUE="${1:-83}"
BASE_URL="${GITEA_URL:-https://git.ptits-pas.fr/api/v1}"
REPO="jmartin/petitspas"
if [ -z "$GITEA_TOKEN" ]; then
if [ -f .gitea-token ]; then
GITEA_TOKEN=$(cat .gitea-token)
fi
fi
if [ -z "$GITEA_TOKEN" ]; then
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
exit 1
fi
echo "Fermeture de l'issue #$ISSUE..."
RESP=$(curl -s -w "\n%{http_code}" -X PATCH \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"$BASE_URL/repos/$REPO/issues/$ISSUE")
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | sed '$d')
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
echo "Issue #$ISSUE fermée."
else
echo "Erreur HTTP $HTTP_CODE: $BODY"
exit 1
fi