(Feat) Migration PocketBase
This commit is contained in:
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user