feat(unionflow): ajout Spec-Kit, constitution, mission mutuelles

- Config Spec-Kit pour Spec-Driven Development
- CONSTITUTION.md + .specify/memory/constitution.md
- Commandes Cursor /speckit.*, règles projet
- Mission: associations + mutuelles d'épargne et de financement
- .gitignore: versionner config spec-kit unionflow

Made-with: Cursor
This commit is contained in:
dahoud
2026-02-27 14:41:07 +00:00
parent 144b68f8e7
commit b1957c1c81
631 changed files with 104070 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
/// Dialog de création d'une demande d'aide
library create_demande_aide_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../bloc/solidarity_bloc.dart';
import '../../data/models/demande_aide_model.dart';
import '../../../organizations/data/repositories/organization_repository.dart';
import '../../../organizations/data/models/organization_model.dart';
class CreateDemandeAideDialog extends StatefulWidget {
final VoidCallback onCreated;
const CreateDemandeAideDialog({super.key, required this.onCreated});
@override
State<CreateDemandeAideDialog> createState() => _CreateDemandeAideDialogState();
}
class _CreateDemandeAideDialogState extends State<CreateDemandeAideDialog> {
final _formKey = GlobalKey<FormState>();
final _titreController = TextEditingController();
final _descriptionController = TextEditingController();
final _justificationController = TextEditingController();
final _montantController = TextEditingController();
String? _organisationId;
String? _type;
List<OrganizationModel> _organisations = [];
bool _loading = false;
static const List<Map<String, String>> _types = [
{'value': 'FINANCIERE', 'label': 'Financière'},
{'value': 'MATERIELLE', 'label': 'Matérielle'},
{'value': 'ALIMENTAIRE', 'label': 'Alimentaire'},
{'value': 'MEDICALE', 'label': 'Médicale'},
{'value': 'SCOLAIRE', 'label': 'Scolaire'},
{'value': 'LOGEMENT', 'label': 'Logement'},
{'value': 'AUTRE', 'label': 'Autre'},
];
@override
void initState() {
super.initState();
_loadOrgs();
}
@override
void dispose() {
_titreController.dispose();
_descriptionController.dispose();
_justificationController.dispose();
_montantController.dispose();
super.dispose();
}
Future<void> _loadOrgs() async {
try {
final repo = GetIt.instance<OrganizationRepository>();
final list = await repo.getOrganizations(page: 0, size: 100);
if (mounted) setState(() => _organisations = list);
} catch (_) {
if (mounted) setState(() {});
}
}
void _submit() {
if (!_formKey.currentState!.validate()) return;
final titre = _titreController.text.trim();
final description = _descriptionController.text.trim();
if (titre.isEmpty || description.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Titre et description obligatoires')),
);
return;
}
final montant = double.tryParse(_montantController.text.replaceAll(',', '.'));
setState(() => _loading = true);
final demande = DemandeAideModel(
titre: titre,
description: description,
justification: _justificationController.text.trim().isEmpty
? null
: _justificationController.text.trim(),
type: _type,
montantDemande: montant,
organisationId: _organisationId,
dateDemande: DateTime.now(),
statut: 'BROUILLON',
);
context.read<SolidarityBloc>().add(CreateDemandeAide(demande));
widget.onCreated();
if (mounted) {
setState(() => _loading = false);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Nouvelle demande d\'aide'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _titreController,
decoration: const InputDecoration(
labelText: 'Titre *',
border: OutlineInputBorder(),
),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Obligatoire' : null,
enabled: !_loading,
),
const SizedBox(height: 12),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Description *',
border: OutlineInputBorder(),
),
maxLines: 3,
validator: (v) => (v == null || v.trim().isEmpty) ? 'Obligatoire' : null,
enabled: !_loading,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: _type,
decoration: const InputDecoration(
labelText: 'Type d\'aide',
border: OutlineInputBorder(),
),
items: _types
.map((e) => DropdownMenuItem(
value: e['value'],
child: Text(e['label']!),
))
.toList(),
onChanged: _loading ? null : (v) => setState(() => _type = v),
),
const SizedBox(height: 12),
TextFormField(
controller: _montantController,
decoration: const InputDecoration(
labelText: 'Montant demandé (FCFA, optionnel)',
border: OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
enabled: !_loading,
),
const SizedBox(height: 12),
TextFormField(
controller: _justificationController,
decoration: const InputDecoration(
labelText: 'Justification',
border: OutlineInputBorder(),
),
maxLines: 2,
enabled: !_loading,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: _organisationId,
decoration: const InputDecoration(
labelText: 'Organisation',
border: OutlineInputBorder(),
),
items: _organisations
.map((o) => DropdownMenuItem(
value: o.id,
child: Text(o.nom),
))
.toList(),
onChanged: _loading ? null : (v) => setState(() => _organisationId = v),
),
],
),
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
FilledButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Créer'),
),
],
);
}
}