merge: frontend borrow api
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import "materiel_item.dart";
|
||||
|
||||
/// Réponse renvoyée par l'API après création d'un emprunt.
|
||||
class EmpruntResponse {
|
||||
const EmpruntResponse({
|
||||
required this.id,
|
||||
required this.dateEmprunt,
|
||||
required this.dateRetourPrevue,
|
||||
required this.statut,
|
||||
required this.materiel,
|
||||
});
|
||||
|
||||
factory EmpruntResponse.fromJson(Map<String, dynamic> json) {
|
||||
return EmpruntResponse(
|
||||
id: json["id"] as int,
|
||||
dateEmprunt: DateTime.parse(json["dateEmprunt"] as String),
|
||||
dateRetourPrevue: DateTime.parse(json["dateRetourPrevue"] as String),
|
||||
statut: json["statut"] as String,
|
||||
materiel: MaterielItem.fromJson(json["materiel"] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
final int id;
|
||||
final DateTime dateEmprunt;
|
||||
final DateTime dateRetourPrevue;
|
||||
final String statut;
|
||||
final MaterielItem materiel;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
|
||||
import "package:google_fonts/google_fonts.dart";
|
||||
import "../theme/ensup_colors.dart";
|
||||
import "../models/materiel_item.dart";
|
||||
import "../services/emprunt_service.dart";
|
||||
import "../widgets/ensup_top_bar.dart";
|
||||
import "../widgets/brand_corners.dart";
|
||||
import "../widgets/etat_checklist.dart";
|
||||
@@ -19,27 +20,79 @@ class ChecklistDepartScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
|
||||
late final List<String> _elements = widget.item.accessoires.isEmpty
|
||||
? <String>[widget.item.nom]
|
||||
: widget.item.accessoires;
|
||||
late final List<EtatChecklist> _etats = List<EtatChecklist>.filled(
|
||||
widget.item.accessoires.length,
|
||||
_elements.length,
|
||||
EtatChecklist.present,
|
||||
);
|
||||
final TextEditingController _commentaireController = TextEditingController();
|
||||
bool _envoiEnCours = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_commentaireController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _valider() async {
|
||||
setState(() => _envoiEnCours = true);
|
||||
|
||||
void _valider() {
|
||||
final anomalies = <String>[];
|
||||
for (var i = 0; i < _etats.length; i++) {
|
||||
if (_etats[i] == EtatChecklist.absent) {
|
||||
anomalies.add("${widget.item.accessoires[i]} absent au départ");
|
||||
anomalies.add("${_elements[i]} absent au départ");
|
||||
} else if (_etats[i] == EtatChecklist.deteriore) {
|
||||
anomalies.add("${widget.item.accessoires[i]} détérioré au départ");
|
||||
anomalies.add("${_elements[i]} détérioré au départ");
|
||||
}
|
||||
}
|
||||
final note = anomalies.isEmpty ? null : anomalies.join(", ");
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ConfirmationScreen(item: widget.item, note: note),
|
||||
),
|
||||
);
|
||||
try {
|
||||
final emprunt = await EmpruntService.creerEmprunt(
|
||||
materielId: widget.item.id,
|
||||
commentaireDepart: _commentaireController.text,
|
||||
elements: _elements.asMap().entries.map((entry) {
|
||||
return {
|
||||
"nomElement": entry.value,
|
||||
"etat": _etatApi(_etats[entry.key]),
|
||||
"quantiteConstatee": _etats[entry.key] == EtatChecklist.absent ? 0 : 1,
|
||||
};
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ConfirmationScreen(emprunt: emprunt, note: note),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error.toString())),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _envoiEnCours = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _etatApi(EtatChecklist etat) {
|
||||
switch (etat) {
|
||||
case EtatChecklist.present:
|
||||
return "PRESENT";
|
||||
case EtatChecklist.absent:
|
||||
return "ABSENT";
|
||||
case EtatChecklist.deteriore:
|
||||
return "DETERIORE";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -141,7 +194,7 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ChecklistTableau(
|
||||
accessoires: widget.item.accessoires,
|
||||
accessoires: _elements,
|
||||
etats: _etats,
|
||||
onChanged: (index, etat) => setState(() => _etats[index] = etat),
|
||||
),
|
||||
@@ -161,15 +214,23 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
|
||||
"endommagé pour éviter tout litige au retour.",
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const ChecklistCommentaire(),
|
||||
ChecklistCommentaire(controller: _commentaireController),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _valider,
|
||||
icon: const Icon(Icons.check, size: 18),
|
||||
onPressed: _envoiEnCours ? null : _valider,
|
||||
icon: _envoiEnCours
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Icon(Icons.check, size: 18),
|
||||
label: Text(
|
||||
"Valider la checklist et confirmer l'emprunt",
|
||||
_envoiEnCours
|
||||
? "Enregistrement..."
|
||||
: "Valider la checklist et confirmer l'emprunt",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 15),
|
||||
),
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:google_fonts/google_fonts.dart";
|
||||
import "../theme/ensup_colors.dart";
|
||||
import "../models/emprunt_response.dart";
|
||||
import "../models/materiel_item.dart";
|
||||
import "../widgets/ensup_top_bar.dart";
|
||||
import "../widgets/brand_corners.dart";
|
||||
|
||||
/// Écran de succès clôturant l'emprunt : confirmation + récapitulatif.
|
||||
class ConfirmationScreen extends StatelessWidget {
|
||||
const ConfirmationScreen({super.key, required this.item, this.note});
|
||||
const ConfirmationScreen({super.key, required this.emprunt, this.note});
|
||||
|
||||
final MaterielItem item;
|
||||
final EmpruntResponse emprunt;
|
||||
final String? note;
|
||||
|
||||
MaterielItem get item => emprunt.materiel;
|
||||
|
||||
static String _deuxChiffres(int valeur) => valeur.toString().padLeft(2, "0");
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
final date = "${_deuxChiffres(now.day)}/${_deuxChiffres(now.month)}/${now.year}";
|
||||
final heure = "${_deuxChiffres(now.hour)}:${_deuxChiffres(now.minute)}";
|
||||
final dateEmprunt = emprunt.dateEmprunt;
|
||||
final date =
|
||||
"${_deuxChiffres(dateEmprunt.day)}/${_deuxChiffres(dateEmprunt.month)}/${dateEmprunt.year}";
|
||||
final heure = "${_deuxChiffres(dateEmprunt.hour)}:${_deuxChiffres(dateEmprunt.minute)}";
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: EnsupColors.soft,
|
||||
@@ -181,6 +185,7 @@ class ConfirmationScreen extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
_rrow("Matériel", item.nom),
|
||||
_rrow("Référence", item.reference),
|
||||
_rrow("N° emprunt", "#${emprunt.id}"),
|
||||
_rrow("Emprunteur", "Lucas Martin"),
|
||||
_rrow("Date d'emprunt", date),
|
||||
_rrow("Heure", heure),
|
||||
|
||||
@@ -37,6 +37,31 @@ class ApiClient {
|
||||
throw ApiException(_messageErreur(reponse));
|
||||
}
|
||||
|
||||
static Future<dynamic> post(String chemin, Map<String, dynamic> body) async {
|
||||
final uri = Uri.parse("$_baseUrl$chemin");
|
||||
|
||||
late final http.Response reponse;
|
||||
try {
|
||||
reponse = await http.post(
|
||||
uri,
|
||||
headers: const {
|
||||
"content-type": "application/json",
|
||||
"x-user-email": _utilisateurEmail,
|
||||
},
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
} catch (_) {
|
||||
throw const ApiException(
|
||||
"Impossible de joindre le serveur. Vérifiez que le backend est démarré.",
|
||||
);
|
||||
}
|
||||
|
||||
if (reponse.statusCode >= 200 && reponse.statusCode < 300) {
|
||||
return jsonDecode(reponse.body);
|
||||
}
|
||||
throw ApiException(_messageErreur(reponse));
|
||||
}
|
||||
|
||||
static String _messageErreur(http.Response reponse) {
|
||||
try {
|
||||
final corps = jsonDecode(reponse.body);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import "../models/emprunt_response.dart";
|
||||
import "api_client.dart";
|
||||
|
||||
/// Appels liés aux emprunts.
|
||||
class EmpruntService {
|
||||
EmpruntService._();
|
||||
|
||||
static const int posteEmpruntId = 1;
|
||||
|
||||
static Future<EmpruntResponse> creerEmprunt({
|
||||
required int materielId,
|
||||
required String commentaireDepart,
|
||||
required List<Map<String, dynamic>> elements,
|
||||
}) async {
|
||||
final reponse = await ApiClient.post("/emprunts", {
|
||||
"materielId": materielId,
|
||||
"posteEmpruntId": posteEmpruntId,
|
||||
"modeIdentification": "MAIL_ENSUP",
|
||||
if (commentaireDepart.trim().isNotEmpty) "commentaireDepart": commentaireDepart.trim(),
|
||||
"elements": elements,
|
||||
});
|
||||
|
||||
final data = (reponse as Map<String, dynamic>)["data"] as Map<String, dynamic>;
|
||||
return EmpruntResponse.fromJson(data);
|
||||
}
|
||||
}
|
||||
@@ -191,13 +191,19 @@ class ChecklistInstructions extends StatelessWidget {
|
||||
|
||||
/// Zone de commentaire optionnel sous une checklist.
|
||||
class ChecklistCommentaire extends StatelessWidget {
|
||||
const ChecklistCommentaire({super.key, this.hint = "Commentaire ou observation optionnelle..."});
|
||||
const ChecklistCommentaire({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.hint = "Commentaire ou observation optionnelle...",
|
||||
});
|
||||
|
||||
final TextEditingController? controller;
|
||||
final String hint;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
maxLines: 4,
|
||||
style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.text),
|
||||
decoration: InputDecoration(
|
||||
|
||||
@@ -221,6 +221,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
||||
- Testé en réel côté backend : `/api/materiels/1` renvoie le PC avec accessoires (`Chargeur`, `Souris`) ; `/api/materiels/abc` renvoie 400.
|
||||
- Limite environnement : `flutter analyze`, `flutter analyze --no-pub` et `dart format` restent bloqués jusqu'au timeout dans le sandbox. À relancer dans un terminal Flutter local.
|
||||
|
||||
### Étape 28 — Connexion frontend ↔ API (création d'emprunt)
|
||||
- Branche dédiée `feat/frontend-emprunt-api` créée après merge de `feat/catalogue-detail-api` dans `develop`.
|
||||
- `ApiClient.post` ajouté et `EmpruntService.creerEmprunt` consomme `POST /api/emprunts` avec l'auth simulée.
|
||||
- Checklist de départ branchée sur l'API : construction du payload (`materielId`, `posteEmpruntId`, `modeIdentification`, commentaire, éléments), état de chargement, affichage d'erreur via snackbar.
|
||||
- Un matériel sans accessoire génère un élément de checklist portant le nom du matériel, afin de respecter RG11 côté backend (checklist obligatoire non vide).
|
||||
- Confirmation alimentée par la réponse API (`id`, `dateEmprunt`, matériel renvoyé).
|
||||
- Testé sans mutation de données : `POST /api/emprunts` avec checklist vide renvoie bien 400 `La checklist de depart est obligatoire (RG11)`.
|
||||
|
||||
---
|
||||
|
||||
*Dernière mise à jour : 2026-07-07 — Catalogue et détail matériel branchés sur l'API backend ; backend testé avec SQL Server Docker.*
|
||||
*Dernière mise à jour : 2026-07-07 — Catalogue, détail matériel et création d'emprunt frontend branchés sur l'API backend.*
|
||||
|
||||
Reference in New Issue
Block a user