(Feat) Migration PocketBase

This commit is contained in:
2026-07-18 11:05:26 +02:00
parent 4d936d2ee3
commit 54091398a5
9 changed files with 473 additions and 41 deletions
+60 -1
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../services/produit_repository.dart';
import 'etiquettes_screen.dart';
import 'produits_screen.dart';
import 'scan_screen.dart';
@@ -23,7 +24,19 @@ class _HomeShellState extends State<HomeShell> {
const EtiquettesScreen(),
];
return Scaffold(
body: IndexedStack(index: _index, children: pages),
body: Column(
children: [
ListenableBuilder(
listenable: ProduitRepository.instance,
builder: (context, _) {
final repo = ProduitRepository.instance;
if (repo.connecte) return const SizedBox.shrink();
return _BanniereHorsLigne(onReessayer: () => repo.charger());
},
),
Expanded(child: IndexedStack(index: _index, children: pages)),
],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
height: 68,
@@ -50,3 +63,49 @@ class _HomeShellState extends State<HomeShell> {
);
}
}
/// Fin bandeau affiché en haut quand la tablette n'est pas synchronisée au serveur.
class _BanniereHorsLigne extends StatelessWidget {
final VoidCallback onReessayer;
const _BanniereHorsLigne({required this.onReessayer});
static const _ambre = Color(0xFFB26B00);
@override
Widget build(BuildContext context) {
return Material(
color: const Color(0xFFFFF3E0),
child: SafeArea(
bottom: false,
child: InkWell(
onTap: onReessayer,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
child: Row(
children: [
const Icon(Icons.cloud_off_rounded, size: 16, color: _ambre),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Hors ligne — non synchronisé',
style: TextStyle(
color: _ambre,
fontSize: 12.5,
fontWeight: FontWeight.w600),
),
),
const Text('Réessayer',
style: TextStyle(
color: _ambre,
fontSize: 12.5,
fontWeight: FontWeight.w700)),
const SizedBox(width: 4),
const Icon(Icons.refresh, size: 15, color: _ambre),
],
),
),
),
),
);
}
}
+156 -1
View File
@@ -60,7 +60,12 @@ class _ProduitsScreenState extends State<ProduitsScreen> {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(child: _entete(_repo.produits.length)),
if (_repo.enCours && _repo.produits.isEmpty)
if (_repo.erreur != null && _repo.produits.isEmpty)
SliverFillRemaining(
hasScrollBody: false,
child: _erreurServeur(),
)
else if (_repo.enCours && _repo.produits.isEmpty)
const SliverFillRemaining(
child: Center(child: CircularProgressIndicator()),
)
@@ -111,6 +116,12 @@ class _ProduitsScreenState extends State<ProduitsScreen> {
fontWeight: FontWeight.w600,
color: AppTheme.grisTexte)),
const Spacer(),
IconButton(
icon: const Icon(Icons.settings_outlined),
color: AppTheme.grisTexte,
onPressed: _ouvrirReglages,
tooltip: 'Serveur',
),
],
),
const SizedBox(height: 14),
@@ -177,6 +188,150 @@ class _ProduitsScreenState extends State<ProduitsScreen> {
],
);
}
Widget _erreurServeur() {
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.wifi_off_rounded, size: 64, color: Colors.red),
const SizedBox(height: 16),
const Text('Serveur injoignable',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
const SizedBox(height: 8),
Text(
_repo.serveurUrl,
textAlign: TextAlign.center,
style: const TextStyle(color: AppTheme.grisTexte, fontSize: 13),
),
const SizedBox(height: 20),
Wrap(
spacing: 12,
alignment: WrapAlignment.center,
children: [
FilledButton.icon(
onPressed: () => _repo.charger(),
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
),
OutlinedButton.icon(
onPressed: _ouvrirReglages,
icon: const Icon(Icons.settings_outlined),
label: const Text('Réglages'),
),
],
),
],
),
);
}
Future<void> _ouvrirReglages() async {
final ctrl = TextEditingController(text: _repo.serveurUrl);
final locaux = await _repo.produitsLocaux();
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Serveur'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: (_repo.connecte ? const Color(0xFF1B873F) : Colors.red)
.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.circle,
size: 12,
color:
_repo.connecte ? const Color(0xFF1B873F) : Colors.red),
const SizedBox(width: 10),
Text(
_repo.connecte ? 'Connecté au serveur' : 'Non connecté',
style: TextStyle(
fontWeight: FontWeight.w700,
color:
_repo.connecte ? const Color(0xFF1B873F) : Colors.red,
),
),
],
),
),
const SizedBox(height: 18),
const Text('Adresse du serveur (Raspberry Pi)',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)),
const SizedBox(height: 8),
TextField(
controller: ctrl,
keyboardType: TextInputType.url,
autocorrect: false,
decoration: const InputDecoration(
hintText: 'http://192.168.1.32:8090',
),
),
if (locaux.isNotEmpty) ...[
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 8),
Text('${locaux.length} produit(s) dans la base locale à envoyer',
style: const TextStyle(fontSize: 13)),
const SizedBox(height: 8),
FilledButton.icon(
style: FilledButton.styleFrom(
backgroundColor: AppTheme.accent,
minimumSize: const Size.fromHeight(46)),
onPressed: () async {
Navigator.pop(ctx);
await _importerLocaux();
},
icon: const Icon(Icons.sync),
label: const Text('Lancer la synchro'),
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Fermer'),
),
FilledButton(
onPressed: () async {
Navigator.pop(ctx);
await _repo.changerServeur(ctrl.text);
},
child: const Text('Connecter'),
),
],
),
);
}
Future<void> _importerLocaux() async {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Envoi vers le serveur…')),
);
try {
final n = await _repo.importerLocaux();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$n produit(s) envoyé(s) au serveur ✓')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Échec de l\'envoi : $e')),
);
}
}
}
class _ProduitCard extends StatelessWidget {