Ajoute l'espace parent : PIN, grille de règles, historique et annulation.
Phase 2 du MVP Flutter : vérification PIN locale, application des règles par famille, suivi des mouvements et correction avec plancher à 0. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../data/database/app_database.dart';
|
||||
|
||||
class ChildSelector extends StatelessWidget {
|
||||
const ChildSelector({
|
||||
super.key,
|
||||
required this.children,
|
||||
required this.selectedId,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final List<Enfant> children;
|
||||
final int selectedId;
|
||||
final ValueChanged<int> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: children.map((child) {
|
||||
final selected = child.id == selectedId;
|
||||
final color = AppTheme.parseColor(child.couleur);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: InkWell(
|
||||
onTap: () => onSelected(child.id),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: selected ? color : Colors.transparent,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 32,
|
||||
backgroundColor: color,
|
||||
child: Text(
|
||||
child.prenom[0].toUpperCase(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: -4,
|
||||
bottom: -4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: color),
|
||||
),
|
||||
child: Text(
|
||||
'${child.score}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
child.prenom,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
selected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class PinEntryCard extends StatelessWidget {
|
||||
const PinEntryCard({
|
||||
super.key,
|
||||
required this.pinController,
|
||||
required this.obscurePin,
|
||||
required this.isLoading,
|
||||
required this.onToggleObscure,
|
||||
required this.onSubmit,
|
||||
this.error,
|
||||
});
|
||||
|
||||
final TextEditingController pinController;
|
||||
final bool obscurePin;
|
||||
final bool isLoading;
|
||||
final VoidCallback onToggleObscure;
|
||||
final VoidCallback onSubmit;
|
||||
final String? error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 36,
|
||||
backgroundColor: Color(0xFF667EEA),
|
||||
child: Icon(Icons.family_restroom, color: Colors.white, size: 36),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Entrez votre code PIN',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
controller: pinController,
|
||||
obscureText: obscurePin,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 24, letterSpacing: 8),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(8),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hintText: '••••',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
obscurePin ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: onToggleObscure,
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => onSubmit(),
|
||||
),
|
||||
if (error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
error!,
|
||||
style: TextStyle(color: Colors.red.shade700),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: isLoading ? null : onSubmit,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Connexion'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../data/database/app_database.dart';
|
||||
|
||||
class RulesGrid extends StatelessWidget {
|
||||
const RulesGrid({
|
||||
super.key,
|
||||
required this.rules,
|
||||
required this.isApplying,
|
||||
required this.onRuleTap,
|
||||
});
|
||||
|
||||
final List<Regle> rules;
|
||||
final bool isApplying;
|
||||
final void Function(int ruleId) onRuleTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final groups = <String, List<Regle>>{};
|
||||
for (final rule in rules) {
|
||||
groups.putIfAbsent(rule.famille, () => []).add(rule);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: groups.entries.map((entry) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
entry.key,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: entry.value.map((rule) {
|
||||
final positive = rule.points >= 0;
|
||||
return Material(
|
||||
color: positive
|
||||
? Colors.green.shade50
|
||||
: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: isApplying ? null : () => onRuleTap(rule.id),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
width: 160,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: positive
|
||||
? Colors.green.shade200
|
||||
: Colors.red.shade200,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${rule.icone} ${rule.libelle}',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${rule.points > 0 ? '+' : ''}${rule.points}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: positive
|
||||
? Colors.green.shade800
|
||||
: Colors.red.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user