Files
HenFlow/lib/main.dart
T

2987 lines
109 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:table_calendar/table_calendar.dart';
enum AppLanguage { it, en, fr, de }
class TranslationService {
static AppLanguage _currentLanguage = AppLanguage.en;
static void setLanguage(AppLanguage lang) {
_currentLanguage = lang;
}
static AppLanguage get currentLanguage => _currentLanguage;
static final Map<AppLanguage, Map<String, String>> _data = {
AppLanguage.it: {
'app_title': 'Henflow',
'daily_prod_est': 'Produzione giornaliera stimata',
'eggs_per_day': 'uova/giorno',
'no_breeds': 'Nessuna razza configurata.',
'hens': 'galline',
'eggs_per_g': 'uova/g',
'reservations': 'Prenotazioni',
'no_reservations': 'Nessuna prenotazione inserita.',
'use_button': 'Usa il pulsante "Nuovo ordine" per registrare un cliente.',
'new_order': 'Nuovo ordine',
'edit_order': 'Modifica ordine',
'breed': 'Razza',
'eggs_req': 'Uova richieste',
'delivery': 'Consegna',
'today': 'Oggi',
'ready_delivery': 'Pronti per la consegna',
'expired': 'Scaduto',
'in_day': 'Tra 1 giorno',
'in_days': 'Tra {} giorni',
'settings': 'Configurazione',
'save': 'Salva',
'available_breeds': 'Razze disponibili',
'my_coop': 'Il mio pollaio',
'no_breeds_sel': 'Nessuna razza selezionata.\nVai in "Razze disponibili" per attivare quelle che allevi.',
'reg_hens': 'Galline registrate',
'in_laying': 'In deposizione',
'est_prod': 'Produzione stimata',
'manage_hens': 'Gestisci anagrafica galline',
'breed_sheet': 'Scheda {}',
'new_hen': 'Nuova gallina',
'egg_color': 'Colore uova',
'laying_6_months': 'Galline in deposizione (6+ mesi)',
'hen_registry': 'Anagrafica galline',
'no_hens_reg': 'Nessuna gallina registrata per questa razza.',
'birth': 'Nascita',
'age_months': 'Età: {} mesi',
'status_laying': 'Stato: in deposizione',
'status_rest': 'Stato: a riposo',
'status_young': 'Stato: troppo giovane per deporre',
'hen_name': 'Nome gallina',
'birth_date': 'Data di nascita',
'select_date': 'Seleziona data',
'cancel': 'Annulla',
'customer_name': 'Nome cliente',
'need_hen_error': 'Per creare un ordine serve almeno una gallina di 6 mesi o più in una delle razze configurate.',
'delivery_preview': 'Anteprima consegna',
'save_order': 'Salva ordine',
'language': 'Lingua',
'config_breed_first': 'Configura prima almeno una razza nelle impostazioni.',
'name_marans': 'Marans',
'name_araucana': 'Araucana',
'name_olive': 'Olive Egger',
'name_livorno': 'Livorno',
'name_sussex': 'Sussex',
'name_australorp': 'Australorp',
'name_generic': 'Ovaiola generica',
'desc_marans': 'Famosa per le sue uova color cioccolato fondente.',
'desc_araucana': 'Uccello senza coda che depone uova azzurro-verdi.',
'desc_olive': 'Incrocio che produce uova di un bellissimo verde oliva.',
'desc_livorno': 'Eccellente produttrice di uova bianche, snella e vivace.',
'desc_sussex': 'Razza versatile e docile, depone uova color crema.',
'desc_australorp': 'Detentrice del record mondiale di deposizione, uova marroni chiare.',
'desc_generic': 'Gallina ovaiola di razza mista o non specificata.',
'color_dark_brown': 'Marrone scuro',
'color_blue': 'Azzurro',
'color_olive': 'Verde Oliva',
'color_white': 'Bianco',
'color_cream': 'Crema',
'color_light_brown': 'Marrone chiaro',
'confirm_delete': 'Conferma eliminazione',
'delete_msg': 'Sei sicuro di voler eliminare questo elemento?',
'delete': 'Elimina',
'backup_restore': 'Backup e Ripristino',
'export_data': 'Esporta dati',
'import_data': 'Importa dati',
'copy_to_clipboard': 'Copia negli appunti',
'copied': 'Copiato!',
'paste_json_here': 'Incolla qui il JSON di backup',
'import_success': 'Dati importati con successo!',
'import_error': 'Errore durante l\'importazione. Controlla il formato.',
'confirm_deactivation': 'Conferma disattivazione',
'deactivation_msg': 'Disattivando questa razza, tutte le galline registrate verranno eliminate definitivamente. Vuoi procedere?',
'deactivate': 'Disattiva',
'new_customer': 'Nuovo cliente',
'first_name': 'Nome',
'last_name': 'Cognome',
'phone_number': 'Numero di telefono',
'save_customer': 'Salva cliente',
'customers_list': 'Elenco clienti',
'edit_customer': 'Modifica cliente',
'no_customers': 'Nessun cliente registrato.',
'share_order_msg': 'Ciao {}, il tuo ordine di {} uova di razza {} è previsto per il {}.',
'welcome': 'Benvenuto in Henflow!',
'intro_step1_title': '1. Seleziona le razze',
'intro_step1_desc': 'Vai nelle impostazioni e attiva le razze che allevi nel tuo pollaio.',
'intro_step2_title': '2. Aggiungi le galline',
'intro_step2_desc': 'Per ogni razza attiva, inserisci le tue galline. Solo quelle con più di 6 mesi contribuiscono alla produzione.',
'intro_step3_title': '3. Anagrafica clienti',
'intro_step3_desc': 'Registra i tuoi clienti per velocizzare la creazione degli ordini.',
'intro_step4_title': '4. Gestisci gli ordini',
'intro_step4_desc': 'Aggiungi le prenotazioni. Eggflow calcolerà automaticamente la data di consegna in base alla produzione.',
'start_app': 'Inizia ora',
'show_intro': 'Mostra introduzione',
'about': 'Informazioni',
'about_desc': 'Henflow è la tua soluzione completa per la gestione professionale di un piccolo pollaio. \n\nCon questa app puoi monitorare la produzione giornaliera delle tue galline, gestire un\'anagrafica clienti dettagliata e organizzare le prenotazioni delle uova con ricalcoli automatici delle date di consegna. \n\nProgettata per rendere semplice ed efficiente l\'attività di ogni allevatore.',
'calendar': 'Calendario',
'orders_list': 'Elenco ordini',
'warehouse': 'Magazzino',
'eggs_in_stock': 'Uova in magazzino',
'edit_stock': 'Modifica giacenza',
'last_update': 'Ultimo aggiornamento',
'client_mode': 'Modalità Cliente',
'producer_mode': 'Modalità Produttore',
'i_am_client': 'Sono un Cliente',
'i_am_producer': 'Sono un Produttore',
'mode_selection_title': 'Scegli la tua modalità',
'mode_selection_desc': 'Come vuoi usare Henflow?',
'book': 'Prenota',
'import_order': 'Importa ordine',
'client_note': 'Note (opzionale)',
'order_imported': 'Ordine importato con successo!',
'invalid_order_format': 'Formato ordine non valido',
'duplicate_order': 'Ordine già importato',
'user_mode': 'Modalità utente',
'client_profile_title': 'I tuoi dati',
'client_profile_desc': 'Inserisci i tuoi dati per velocizzare gli ordini (solo una volta).',
},
AppLanguage.en: {
'app_title': 'Henflow',
'daily_prod_est': 'Estimated daily production',
'eggs_per_day': 'eggs/day',
'no_breeds': 'No breeds configured.',
'hens': 'hens',
'eggs_per_g': 'eggs/d',
'reservations': 'Reservations',
'no_reservations': 'No reservations entered.',
'use_button': 'Use the "New order" button to register a customer.',
'new_order': 'New order',
'edit_order': 'Edit order',
'breed': 'Breed',
'eggs_req': 'Eggs requested',
'delivery': 'Delivery',
'today': 'Today',
'ready_delivery': 'Ready for delivery',
'expired': 'Expired',
'in_day': 'In 1 day',
'in_days': 'In {} days',
'settings': 'Configuration',
'save': 'Save',
'available_breeds': 'Available breeds',
'my_coop': 'My coop',
'no_breeds_sel': 'No breeds selected.\nGo to "Available breeds" to activate the ones you raise.',
'reg_hens': 'Registered hens',
'in_laying': 'Laying',
'est_prod': 'Estimated production',
'manage_hens': 'Manage hen records',
'breed_sheet': '{} sheet',
'new_hen': 'New hen',
'egg_color': 'Egg color',
'laying_6_months': 'Laying hens (6+ months)',
'hen_registry': 'Hen registry',
'no_hens_reg': 'No hens registered for this breed.',
'birth': 'Birth',
'age_months': 'Age: {} months',
'status_laying': 'Status: laying',
'status_rest': 'Status: resting',
'status_young': 'Status: too young to lay',
'hen_name': 'Hen name',
'birth_date': 'Birth date',
'select_date': 'Select date',
'cancel': 'Cancel',
'customer_name': 'Customer name',
'need_hen_error': 'To create an order you need at least one hen 6 months or older in one of the configured breeds.',
'delivery_preview': 'Delivery preview',
'save_order': 'Save order',
'language': 'Language',
'config_breed_first': 'Configure at least one breed in settings first.',
'name_marans': 'Marans',
'name_araucana': 'Araucana',
'name_olive': 'Olive Egger',
'name_livorno': 'Leghorn',
'name_sussex': 'Sussex',
'name_australorp': 'Australorp',
'name_generic': 'Generic Layer',
'desc_marans': 'Famous for its dark chocolate-colored eggs.',
'desc_araucana': 'Rumpless bird that lays blue-green eggs.',
'desc_olive': 'Hybrid that produces beautiful olive green eggs.',
'desc_livorno': 'Excellent producer of white eggs, slender and active.',
'desc_sussex': 'Versatile and docile breed, lays cream-colored eggs.',
'desc_australorp': 'World record holder for egg laying, light brown eggs.',
'desc_generic': 'Laying hen of mixed or unspecified breed.',
'color_dark_brown': 'Dark brown',
'color_blue': 'Blue',
'color_olive': 'Olive',
'color_white': 'White',
'color_cream': 'Cream',
'color_light_brown': 'Light brown',
'confirm_delete': 'Confirm deletion',
'delete_msg': 'Are you sure you want to delete this item?',
'delete': 'Delete',
'backup_restore': 'Backup & Restore',
'export_data': 'Export data',
'import_data': 'Import data',
'copy_to_clipboard': 'Copy to clipboard',
'copied': 'Copied!',
'paste_json_here': 'Paste backup JSON here',
'import_success': 'Data imported successfully!',
'import_error': 'Import error. Check the format.',
'confirm_deactivation': 'Confirm deactivation',
'deactivation_msg': 'By deactivating this breed, all registered hens will be permanently deleted. Do you want to proceed?',
'deactivate': 'Deactivate',
'new_customer': 'New customer',
'first_name': 'First name',
'last_name': 'Last name',
'phone_number': 'Phone number',
'save_customer': 'Save customer',
'customers_list': 'Customers list',
'edit_customer': 'Edit customer',
'no_customers': 'No customers registered.',
'share_order_msg': 'Hi {}, your order for {} eggs of breed {} is scheduled for {}.',
'welcome': 'Welcome to Henflow!',
'intro_step1_title': '1. Select breeds',
'intro_step1_desc': 'Go to settings and activate the breeds you raise in your coop.',
'intro_step2_title': '2. Add hens',
'intro_step2_desc': 'For each active breed, enter your hens. Only those older than 6 months contribute to production.',
'intro_step3_title': '3. Customer records',
'intro_step3_desc': 'Register your customers to speed up order creation.',
'intro_step4_title': '4. Manage orders',
'intro_step4_desc': 'Add bookings. Eggflow will automatically calculate the delivery date based on production.',
'start_app': 'Get started',
'show_intro': 'Show introduction',
'about': 'About',
'about_desc': 'Henflow is your complete solution for professional management of a small poultry farm. \n\nWith this app, you can monitor the daily production of your hens, manage detailed customer records, and organize egg bookings with automatic recalculations of delivery dates. \n\nDesigned to make every breeder\'s activity simple and efficient.',
'calendar': 'Calendar',
'orders_list': 'Orders list',
'warehouse': 'Warehouse',
'eggs_in_stock': 'Eggs in stock',
'edit_stock': 'Edit stock',
'last_update': 'Last update',
'client_mode': 'Client Mode',
'producer_mode': 'Producer Mode',
'i_am_client': 'I am a Client',
'i_am_producer': 'I am a Producer',
'mode_selection_title': 'Choose your mode',
'mode_selection_desc': 'How do you want to use Henflow?',
'book': 'Book',
'import_order': 'Import order',
'client_note': 'Notes (optional)',
'order_imported': 'Order imported successfully!',
'invalid_order_format': 'Invalid order format',
'duplicate_order': 'Order already imported',
'user_mode': 'User mode',
'client_profile_title': 'Your profile',
'client_profile_desc': 'Enter your details to speed up orders (only once).',
},
AppLanguage.fr: {
'app_title': 'Henflow',
'daily_prod_est': 'Production quotidienne estimée',
'eggs_per_day': 'œufs/jour',
'no_breeds': 'Aucune race configurée.',
'hens': 'poules',
'eggs_per_g': 'œufs/j',
'reservations': 'Réservations',
'no_reservations': 'Aucune réservation saisie.',
'use_button': 'Utilisez le bouton "Nouvelle commande" pour enregistrer un client.',
'new_order': 'Nouvelle commande',
'edit_order': 'Modifier la commande',
'breed': 'Race',
'eggs_req': 'Œufs demandés',
'delivery': 'Livraison',
'today': 'Aujourd\'hui',
'ready_delivery': 'Prêt per la consegna',
'expired': 'Expiré',
'in_day': 'Dans 1 jour',
'in_days': 'Dans {} jours',
'settings': 'Configuration',
'save': 'Sauvegarder',
'available_breeds': 'Races disponibles',
'my_coop': 'Mon poulailler',
'no_breeds_sel': 'Aucune race sélectionnée.\nAllez dans "Races disponibles" pour activer celles que vous élevez.',
'reg_hens': 'Poules enregistrées',
'in_laying': 'En ponte',
'est_prod': 'Production estimée',
'manage_hens': 'Gérer les fiches des poules',
'breed_sheet': 'Fiche {}',
'new_hen': 'Nouvelle poule',
'egg_color': 'Couleur des œufs',
'laying_6_months': 'Poules en ponte (6+ mois)',
'hen_registry': 'Registre des poules',
'no_hens_reg': 'Aucune poule enregistrée pour cette race.',
'birth': 'Naissance',
'age_months': 'Âge: {} mois',
'status_laying': 'Statut: en ponte',
'status_rest': 'Statut: au repos',
'status_young': 'Statut: trop jeune pour pondre',
'hen_name': 'Nom de la poule',
'birth_date': 'Date de naissance',
'select_date': 'Choisir une date',
'cancel': 'Annuler',
'customer_name': 'Nom du client',
'need_hen_error': 'Pour créer une commande, vous avez besoin d\'au moins une poule de 6 mois ou plus dans l\'une des races configurées.',
'delivery_preview': 'Aperçu de la livraison',
'save_order': 'Sauvegarder la commande',
'language': 'Langue',
'config_breed_first': 'Configurez d\'abord au moins une race dans les paramètres.',
'name_marans': 'Marans',
'name_araucana': 'Araucana',
'name_olive': 'Olive Egger',
'name_livorno': 'Livourne',
'name_sussex': 'Sussex',
'name_australorp': 'Australorp',
'desc_marans': 'Célèbre pour ses œufs couleur chocolat noir.',
'desc_araucana': 'Oiseau sans queue qui pond des œufs bleu-vert.',
'desc_olive': 'Croisement qui produit de magnifiques œufs vert olive.',
'desc_livorno': 'Excellente pondeuse d\'œufs blancs, svelte et vive.',
'desc_sussex': 'Race polyvalente et docile, pond des œufs de couleur crème.',
'desc_australorp': 'Détentrice du record mondial de ponte, œufs brun clair.',
'color_dark_brown': 'Brun foncé',
'color_blue': 'Bleu',
'color_olive': 'Olive',
'color_white': 'Blanc',
'color_cream': 'Crème',
'color_light_brown': 'Brun clair',
'confirm_delete': 'Confirmer la suppression',
'delete_msg': 'Êtes-vous sûr de vouloir supprimer cet élément ?',
'delete': 'Supprimer',
'backup_restore': 'Sauvegarde et Restauration',
'export_data': 'Exporter les données',
'import_data': 'Importer des données',
'copy_to_clipboard': 'Copier dans le presse-papiers',
'copied': 'Copié !',
'paste_json_here': 'Collez le JSON de sauvegarde ici',
'import_success': 'Données importées avec succès !',
'import_error': 'Erreur d\'importation. Vérifiez le format.',
'confirm_deactivation': 'Confirmer la désactivation',
'deactivation_msg': 'En désactivant cette race, toutes le galline registrate verranno eliminate definitivamente. Vuoi procedere?',
'deactivate': 'Désactiver',
'new_customer': 'Nouveau client',
'first_name': 'Prénom',
'last_name': 'Nom',
'phone_number': 'Numéro de téléphone',
'save_customer': 'Sauvegarder le client',
'customers_list': 'Liste des clients',
'edit_customer': 'Modifier le client',
'no_customers': 'Aucun client enregistré.',
'share_order_msg': 'Bonjour {}, votre commande de {} œufs de race {} est prévue pour le {}.',
'welcome': 'Bienvenue sur Henflow !',
'intro_step1_title': '1. Sélectionnez les races',
'intro_step1_desc': 'Allez dans les paramètres et activez les races que vous élevez dans votre poulailler.',
'intro_step2_title': '2. Ajoutez les poules',
'intro_step2_desc': 'Pour chaque race active, saisissez vos poules. Seules celles de plus de 6 mois contribuent à la production.',
'intro_step3_title': '3. Fiches clients',
'intro_step3_desc': 'Enregistrez vos clients pour accélérer la création de commandes.',
'intro_step4_title': '4. Gérez les commandes',
'intro_step4_desc': 'Ajoutez des réservations. Eggflow calculera automatiquement la date de livraison en fonction de la production.',
'start_app': 'Commencer',
'show_intro': 'Afficher l\'introduction',
'about': 'À propos',
'about_desc': 'Henflow est votre solution complète per la gestione professionale di un piccolo pollaio. \n\nAvec cette application, vous pouvez surveiller la production quotidienne de vos poules, gestire un\'anagrafica clienti dettagliata e organizzare le prenotazioni delle uova con ricalcoli automatici delle date di consegna. \n\nConçue per rendere semplice ed efficiente l\'attività di ogni allevatore.',
'calendar': 'Calendrier',
'orders_list': 'Liste des commandes',
'warehouse': 'Entrepôt',
'eggs_in_stock': 'Œufs in stock',
'edit_stock': 'Modifier le stock',
'last_update': 'Dernière mise à jour',
'client_mode': 'Mode Client',
'producer_mode': 'Mode Producteur',
'i_am_client': 'Je suis un Client',
'i_am_producer': 'Je suis un Producteur',
'mode_selection_title': 'Choisissez votre mode',
'mode_selection_desc': 'Comment voulez-vous utiliser Henflow?',
'book': 'Réserver',
'import_order': 'Importer commande',
'client_note': 'Notes (facultatif)',
'order_imported': 'Commande importée avec succès!',
'invalid_order_format': 'Format de commande invalide',
'duplicate_order': 'Commande déjà importée',
'user_mode': 'Mode utilisateur',
'client_profile_title': 'Vos données',
'client_profile_desc': 'Saisissez vos données pour les commandes (une seule fois).',
},
AppLanguage.de: {
'app_title': 'Henflow',
'daily_prod_est': 'Geschätzte tägliche Produktion',
'eggs_per_day': 'Eier/Tag',
'no_breeds': 'Keine Rassen konfiguriert.',
'hens': 'Hennen',
'eggs_per_g': 'Eier/Tag',
'reservations': 'Reservierungen',
'no_reservations': 'Keine Reservierungen eingetragen.',
'use_button': 'Verwenden Sie die Schaltfläche "Neue Bestellung", um einen Kunden zu registrieren.',
'new_order': 'Neue Bestellung',
'edit_order': 'Bestellung bearbeiten',
'breed': 'Rasse',
'eggs_req': 'Anzahl Eier',
'delivery': 'Lieferung',
'today': 'Heute',
'ready_delivery': 'Bereit zur Auslieferung',
'expired': 'Abgelaufen',
'in_day': 'In 1 Tag',
'in_days': 'In {} Tagen',
'settings': 'Konfiguration',
'save': 'Speichern',
'available_breeds': 'Verfügbare Rassen',
'my_coop': 'Mein Hühnerstall',
'no_breeds_sel': 'Keine Rassen ausgewählt.\nGehen Sie zu "Verfügbare Rassen", um die von Ihnen gezüchteten zu aktivieren.',
'reg_hens': 'Registrierte Hennen',
'in_laying': 'In Legephase',
'est_prod': 'Geschätzte Produktion',
'manage_hens': 'Hühnerbestand verwalten',
'breed_sheet': 'Datenblatt {}',
'new_hen': 'Neue Henne',
'egg_color': 'Eierfarbe',
'laying_6_months': 'Legehennen (6+ Monate)',
'hen_registry': 'Hühnerregister',
'no_hens_reg': 'Keine Hennen für diese Rasse registriert.',
'birth': 'Geburt',
'age_months': 'Alter: {} Monate',
'status_laying': 'Status: legend',
'status_rest': 'Status: ruhend',
'status_young': 'Status: zu jung zum Legen',
'hen_name': 'Name der Henne',
'birth_date': 'Geburtsdatum',
'select_date': 'Datum auswählen',
'cancel': 'Abbrechen',
'customer_name': 'Kundenname',
'need_hen_error': 'Um eine Bestellung aufzugeben, benötigen Sie mindestens eine Henne, die 6 Monate oder älter ist, in einer der konfigurierten Rassen.',
'delivery_preview': 'Liefervorschau',
'save_order': 'Bestellung speichern',
'language': 'Sprache',
'config_breed_first': 'Konfigurieren Sie zuerst mindestens eine Rasse in den Einstellungen.',
'name_marans': 'Marans',
'name_araucana': 'Araucana',
'name_olive': 'Olive Egger',
'name_livorno': 'Leghorn',
'name_sussex': 'Sussex',
'name_australorp': 'Australorp',
'name_generic': 'Generische Leghenne',
'desc_marans': 'Berühmt für ihre dunklen, schokoladenbraunen Eier.',
'desc_araucana': 'Schwanzloses Huhn, das blau-grüne Eier legt.',
'desc_olive': 'Kreuzung, die wunderschöne olivgrüne Eier hervorbringt.',
'desc_livorno': 'Hervorragende Legerin von weißen Eiern, schlank und lebhaft.',
'desc_sussex': 'Vielseitige und sanftmütige Rasse, legt cremefarbene Eier.',
'desc_australorp': 'Weltrekordhalterin im Eierlegen, hellbraune Eier.',
'desc_generic': 'Legehenne gemischter oder nicht spezifizierter Rasse.',
'color_dark_brown': 'Dunkelbraun',
'color_blue': 'Blau',
'color_olive': 'Olivgrün',
'color_white': 'Weiß',
'color_cream': 'Creme',
'color_light_brown': 'Hellbraun',
'confirm_delete': 'Löschen bestätigen',
'delete_msg': 'Sind Sie sicher, dass Sie dieses Element löschen möchten?',
'delete': 'Löschen',
'backup_restore': 'Backup & Wiederherstellung',
'export_data': 'Daten exportieren',
'import_data': 'Daten importieren',
'copy_to_clipboard': 'In die Zwischenablage kopieren',
'copied': 'Kopiert!',
'paste_json_here': 'Backup-JSON hier einfügen',
'import_success': 'Daten erfolgreich importiert!',
'import_error': 'Fehler beim Importieren. Überprüfen Sie das Format.',
'confirm_deactivation': 'Deaktivierung bestätigen',
'deactivation_msg': 'Durch Deaktivieren dieser Rasse werden alle registrierten Hennen dauerhaft gelöscht. Möchten Sie fortfahren?',
'deactivate': 'Deaktivieren',
'new_customer': 'Neuer Kunde',
'first_name': 'Vorname',
'last_name': 'Nachname',
'phone_number': 'Telefonnummer',
'save_customer': 'Kunde speichern',
'customers_list': 'Kundenliste',
'edit_customer': 'Kunde bearbeiten',
'no_customers': 'Keine Kunden registriert.',
'share_order_msg': 'Hallo {}, Ihre Bestellung über {} Eier der Rasse {} ist für den {} geplant.',
'welcome': 'Willkommen bei Henflow!',
'intro_step1_title': '1. Rassen auswählen',
'intro_step1_desc': 'Gehen Sie zu den Einstellungen und aktivieren Sie die Rassen, die Sie in Ihrem Stall halten.',
'intro_step2_title': '2. Hennen hinzufügen',
'intro_step2_desc': 'Geben Sie für jede aktive Rasse Ihre Hennen ein. Nur Hennen, die älter als 6 Monate sind, tragen zur Produktion bei.',
'intro_step3_title': '3. Kundenkartei',
'intro_step3_desc': 'Registrieren Sie Ihre Kunden, um die Bestellerstellung zu beschleunigen.',
'intro_step4_title': '4. Bestellungen verwalten',
'intro_step4_desc': 'Fügen Sie Buchungen hinzu. Eggflow berechnet automatisch das Lieferdatum basierend auf der Produktion.',
'start_app': 'Jetzt starten',
'show_intro': 'Einführung anzeigen',
'about': 'Über uns',
'about_desc': 'Henflow ist Ihre Komplettlösung für die professionelle Verwaltung eines kleinen Hühnerstalls. \n\nMit dieser App können Sie die tägliche Produktion Ihrer Hennen überwachen, eine detaillierte Kundenkartei verwalten und Eierreservierungen mit automatischen Neuberechnungen der Liefertermine organisieren. \n\nEntwickelt, um die Tätigkeit jedes Züchters einfach und effizient zu gestalten.',
'calendar': 'Kalender',
'orders_list': 'Bestellliste',
'warehouse': 'Lager',
'eggs_in_stock': 'Eier auf Lager',
'edit_stock': 'Bestand bearbeiten',
'last_update': 'Letzte Aktualisierung',
'client_mode': 'Kundenmodus',
'producer_mode': 'Erzeugermodus',
'i_am_client': 'Ich bin ein Kunde',
'i_am_producer': 'Ich bin ein Erzeuger',
'mode_selection_title': 'Wählen Sie Ihren Modus',
'mode_selection_desc': 'Wie möchten Sie Henflow nutzen?',
'book': 'Buchen',
'import_order': 'Bestellung importieren',
'client_note': 'Notizen (optional)',
'order_imported': 'Bestellung erfolgreich importiert!',
'invalid_order_format': 'Ungültiges Bestellformat',
'duplicate_order': 'Bestellung bereits importiert',
'user_mode': 'Benutzermodus',
'client_profile_title': 'Ihre Daten',
'client_profile_desc': 'Geben Sie Ihre Daten für Bestellungen ein (nur einmal).',
},
};
static String t(String key, {dynamic arg}) {
String text = _data[_currentLanguage]?[key] ?? key;
if (arg != null) {
if (arg is List) {
for (var a in arg) {
text = text.replaceFirst('{}', a.toString());
}
} else {
text = text.replaceFirst('{}', arg.toString());
}
}
return text;
}
}
Future<bool> showDeactivateDialog(BuildContext context) async {
final t = TranslationService.t;
return await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(t('confirm_deactivation')),
content: Text(t('deactivation_msg')),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(t('cancel'))),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(t('deactivate'), style: const TextStyle(color: Colors.red)),
),
],
),
) ?? false;
}
Future<bool> showConfirmDialog(BuildContext context) async {
final t = TranslationService.t;
return await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(t('confirm_delete')),
content: Text(t('delete_msg')),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(t('cancel'))),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(t('delete'), style: const TextStyle(color: Colors.red)),
),
],
),
) ?? false;
}
class AboutPage extends StatelessWidget {
const AboutPage({super.key});
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return Scaffold(
appBar: AppBar(title: Text(t('about'))),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const SizedBox(height: 20),
const Icon(Icons.egg_outlined, size: 80, color: Colors.green),
const SizedBox(height: 24),
const Text('Henflow', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
const SizedBox(height: 40),
Text(
t('about_desc'),
style: const TextStyle(fontSize: 16, height: 1.6),
textAlign: TextAlign.center,
),
const SizedBox(height: 60),
const Text(
'v 0.1.2',
style: TextStyle(color: Colors.grey),
),
],
),
),
);
}
}
class OnboardingPage extends StatelessWidget {
const OnboardingPage({super.key});
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const SizedBox(height: 40),
const Icon(Icons.egg_outlined, size: 80, color: Colors.green),
const SizedBox(height: 24),
Text(t('welcome'), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
const SizedBox(height: 40),
Expanded(
child: ListView(
children: [
_buildStep(Icons.check_box_outlined, t('intro_step1_title'), t('intro_step1_desc')),
_buildStep(Icons.egg_outlined, t('intro_step2_title'), t('intro_step2_desc')),
_buildStep(Icons.person_add_outlined, t('intro_step3_title'), t('intro_step3_desc')),
_buildStep(Icons.add_shopping_cart, t('intro_step4_title'), t('intro_step4_desc')),
],
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 56,
child: FilledButton(
onPressed: () => Navigator.pop(context),
child: Text(t('start_app'), style: const TextStyle(fontSize: 18)),
),
),
],
),
),
),
);
}
Widget _buildStep(IconData icon, String title, String desc) {
return Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: Colors.green, size: 32),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(desc, style: const TextStyle(fontSize: 14, color: Colors.grey)),
],
),
),
],
),
);
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Richiesta consenso GDPR tramite UMP SDK
final params = ConsentRequestParameters();
ConsentInformation.instance.requestConsentInfoUpdate(
params,
() async {
ConsentForm.loadAndShowConsentFormIfRequired((FormError? formError) async {
if (await ConsentInformation.instance.canRequestAds()) {
MobileAds.instance.initialize();
}
});
},
(FormError formError) {
// In caso di errore procediamo comunque (potrebbe essere offline)
},
);
// Inizializza subito se il consenso è già stato fornito in sessioni precedenti
if (await ConsentInformation.instance.canRequestAds()) {
MobileAds.instance.initialize();
}
runApp(const EggBookingApp());
}
class EggBookingApp extends StatefulWidget {
const EggBookingApp({super.key});
@override
State<EggBookingApp> createState() => _EggBookingAppState();
}
class _EggBookingAppState extends State<EggBookingApp> {
@override
void initState() {
super.initState();
_loadLanguage();
}
Future<void> _loadLanguage() async {
final prefs = await SharedPreferences.getInstance();
final langStr = prefs.getString('app_language') ?? 'it';
setState(() {
TranslationService.setLanguage(
AppLanguage.values.firstWhere((e) => e.name == langStr, orElse: () => AppLanguage.it),
);
});
}
void _updateLanguage() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: TranslationService.t('app_title'),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
),
locale: Locale(TranslationService.currentLanguage.name),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLanguage.values.map((l) => Locale(l.name)).toList(),
home: RootPage(onLanguageChanged: _updateLanguage),
);
}
}
class RootPage extends StatefulWidget {
final VoidCallback onLanguageChanged;
const RootPage({super.key, required this.onLanguageChanged});
@override
State<RootPage> createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
String? userMode;
bool isLoading = true;
@override
void initState() {
super.initState();
_checkMode();
}
Future<void> _checkMode() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
userMode = prefs.getString('user_mode');
isLoading = false;
});
}
void _updateMode(String mode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_mode', mode);
setState(() {
userMode = mode;
});
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
if (userMode == null) {
return ModeSelectionPage(onModeSelected: _updateMode);
} else if (userMode == 'client') {
return ClientHomePage(
onLanguageChanged: widget.onLanguageChanged,
onModeChanged: () => _checkMode(),
);
} else {
return HomePage(
onLanguageChanged: widget.onLanguageChanged,
onModeChanged: () => _checkMode(),
);
}
}
}
class ModeSelectionPage extends StatelessWidget {
final Function(String) onModeSelected;
const ModeSelectionPage({super.key, required this.onModeSelected});
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.egg_outlined, size: 100, color: Colors.green),
const SizedBox(height: 32),
Text(t('mode_selection_title'), style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
const SizedBox(height: 16),
Text(t('mode_selection_desc'), style: const TextStyle(fontSize: 16, color: Colors.grey), textAlign: TextAlign.center),
const SizedBox(height: 48),
SizedBox(
width: double.infinity,
height: 64,
child: FilledButton.icon(
onPressed: () => onModeSelected('producer'),
icon: const Icon(Icons.storefront, size: 28),
label: Text(t('i_am_producer'), style: const TextStyle(fontSize: 18)),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 64,
child: FilledButton.tonalIcon(
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
final hasProfile = prefs.getString('client_first_name') != null;
if (!hasProfile) {
if (!context.mounted) return;
final profile = await showDialog<Map<String, String>>(
context: context,
barrierDismissible: false,
builder: (_) => const ClientProfileDialog(),
);
if (profile != null) {
await prefs.setString('client_first_name', profile['first_name']!);
await prefs.setString('client_last_name', profile['last_name']!);
await prefs.setString('client_phone', profile['phone']!);
onModeSelected('client');
}
} else {
onModeSelected('client');
}
},
icon: const Icon(Icons.person_outline, size: 28),
label: Text(t('i_am_client'), style: const TextStyle(fontSize: 18)),
),
),
],
),
),
),
);
}
}
class ClientProfileDialog extends StatefulWidget {
const ClientProfileDialog({super.key});
@override
State<ClientProfileDialog> createState() => _ClientProfileDialogState();
}
class _ClientProfileDialogState extends State<ClientProfileDialog> {
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _phoneController = TextEditingController();
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return AlertDialog(
title: Text(t('client_profile_title')),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(t('client_profile_desc'), style: const TextStyle(color: Colors.grey)),
const SizedBox(height: 16),
TextField(
controller: _firstNameController,
decoration: InputDecoration(labelText: t('first_name'), border: const OutlineInputBorder()),
),
const SizedBox(height: 12),
TextField(
controller: _lastNameController,
decoration: InputDecoration(labelText: t('last_name'), border: const OutlineInputBorder()),
),
const SizedBox(height: 12),
TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(labelText: t('phone_number'), border: const OutlineInputBorder()),
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t('cancel'))),
FilledButton(
onPressed: () {
if (_firstNameController.text.trim().isEmpty) return;
Navigator.pop(context, {
'first_name': _firstNameController.text.trim(),
'last_name': _lastNameController.text.trim(),
'phone': _phoneController.text.trim(),
});
},
child: Text(t('save')),
),
],
);
}
}
class ClientHomePage extends StatefulWidget {
final VoidCallback onLanguageChanged;
final VoidCallback onModeChanged;
const ClientHomePage({super.key, required this.onLanguageChanged, required this.onModeChanged});
@override
State<ClientHomePage> createState() => _ClientHomePageState();
}
class _ClientHomePageState extends State<ClientHomePage> {
final _quantityController = TextEditingController(text: '6');
final _noteController = TextEditingController();
String? _selectedBreedId;
@override
void initState() {
super.initState();
if (breedCatalog.isNotEmpty) {
_selectedBreedId = breedCatalog.first.id;
}
}
Future<void> _bookOrder() async {
final qty = int.tryParse(_quantityController.text) ?? 0;
if (qty <= 0 || _selectedBreedId == null) return;
final orderId = 'ORD-${DateTime.now().millisecondsSinceEpoch}';
final timestamp = DateTime.now();
final prefs = await SharedPreferences.getInstance();
final firstName = prefs.getString('client_first_name') ?? '';
final lastName = prefs.getString('client_last_name') ?? '';
final phone = prefs.getString('client_phone') ?? '';
final orderData = {
"order_id": orderId,
"timestamp": timestamp.toIso8601String(),
"egg_type": _selectedBreedId,
"quantity": qty,
"client_note": _noteController.text.trim(),
"client_first_name": firstName,
"client_last_name": lastName,
"client_phone": phone,
};
try {
final tempDir = await getTemporaryDirectory();
final fileName = 'ordine_uova_${timestamp.millisecondsSinceEpoch}.json';
final file = File('${tempDir.path}/$fileName');
await file.writeAsString(jsonEncode(orderData));
if (!mounted) return;
final xFile = XFile(file.path, mimeType: 'application/json');
final RenderBox? box = context.findRenderObject() as RenderBox?;
await Share.shareXFiles(
[xFile],
subject: 'Ordine Henflow',
sharePositionOrigin: box != null ? box.localToGlobal(Offset.zero) & box.size : null,
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore: $e')),
);
}
}
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return Scaffold(
appBar: AppBar(
title: Text(t('app_title')),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: DropdownButton<AppLanguage>(
value: TranslationService.currentLanguage,
underline: const SizedBox(),
icon: const Icon(Icons.language, color: Colors.white),
dropdownColor: Colors.green,
items: AppLanguage.values.map((l) {
String flag;
switch (l) {
case AppLanguage.it: flag = '🇮🇹'; break;
case AppLanguage.en: flag = '🇬🇧'; break;
case AppLanguage.fr: flag = '🇫🇷'; break;
case AppLanguage.de: flag = '🇩🇪'; break;
}
return DropdownMenuItem(
value: l,
child: Text(flag, style: const TextStyle(fontSize: 20)),
);
}).toList(),
onChanged: (lang) async {
if (lang == null) return;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('app_language', lang.name);
TranslationService.setLanguage(lang);
widget.onLanguageChanged();
setState(() {});
},
),
)
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t('new_order'), style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 24),
DropdownButtonFormField<String>(
value: _selectedBreedId,
decoration: InputDecoration(labelText: t('breed'), border: const OutlineInputBorder()),
items: breedCatalog.map((e) => DropdownMenuItem(value: e.id, child: Text(t(e.name)))).toList(),
onChanged: (v) => setState(() => _selectedBreedId = v),
),
const SizedBox(height: 16),
TextField(
controller: _quantityController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: t('eggs_req'), border: const OutlineInputBorder()),
),
const SizedBox(height: 16),
TextField(
controller: _noteController,
maxLines: 3,
decoration: InputDecoration(labelText: t('client_note'), border: const OutlineInputBorder()),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 56,
child: FilledButton.icon(
onPressed: _bookOrder,
icon: const Icon(Icons.send),
label: Text(t('book'), style: const TextStyle(fontSize: 18)),
),
),
],
),
),
);
}
}
class BreedCatalogItem {
final String id;
final String name;
final String description;
final String eggColor;
final double eggsPerHenPerDay;
final String assetPath;
const BreedCatalogItem({
required this.id,
required this.name,
required this.description,
required this.eggColor,
required this.eggsPerHenPerDay,
required this.assetPath,
});
}
const List<BreedCatalogItem> breedCatalog = [
BreedCatalogItem(
id: 'marans',
name: 'name_marans',
description: 'desc_marans',
eggColor: 'color_dark_brown',
eggsPerHenPerDay: 1.0,
assetPath: 'assets/breeds/marans.jpg',
),
BreedCatalogItem(
id: 'araucana',
name: 'name_araucana',
description: 'desc_araucana',
eggColor: 'color_blue',
eggsPerHenPerDay: 1.0,
assetPath: 'assets/breeds/araucana.jpg',
),
BreedCatalogItem(
id: 'olive',
name: 'name_olive',
description: 'desc_olive',
eggColor: 'color_olive',
eggsPerHenPerDay: 1.0,
assetPath: 'assets/breeds/olive.jpg',
),
BreedCatalogItem(
id: 'livorno',
name: 'name_livorno',
description: 'desc_livorno',
eggColor: 'color_white',
eggsPerHenPerDay: 1.0,
assetPath: 'assets/breeds/livorno.jpg',
),
BreedCatalogItem(
id: 'sussex',
name: 'name_sussex',
description: 'desc_sussex',
eggColor: 'color_cream',
eggsPerHenPerDay: 1.0,
assetPath: 'assets/breeds/sussex.jpg',
),
BreedCatalogItem(
id: 'australorp',
name: 'name_australorp',
description: 'desc_australorp',
eggColor: 'color_light_brown',
eggsPerHenPerDay: 1.0,
assetPath: 'assets/breeds/australorp.jpg',
),
BreedCatalogItem(
id: 'generic',
name: 'name_generic',
description: 'desc_generic',
eggColor: 'color_light_brown',
eggsPerHenPerDay: 1.0,
assetPath: 'assets/breeds/generic.jpg',
),
];
BreedCatalogItem breedFromId(String id) {
return breedCatalog.firstWhere(
(e) => e.id == id,
orElse: () => breedCatalog.first,
);
}
class Hen {
final String id;
final String name;
final DateTime birthDate;
bool isLaying;
Hen({
required this.id,
required this.name,
required this.birthDate,
this.isLaying = true,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'birthDate': birthDate.toIso8601String(),
'isLaying': isLaying,
};
}
factory Hen.fromMap(Map<String, dynamic> map) {
return Hen(
id: map['id'],
name: map['name'],
birthDate: DateTime.parse(map['birthDate']),
isLaying: map['isLaying'] ?? true,
);
}
}
class BreedSetting {
final String breedId;
List<Hen> hens;
BreedSetting({
required this.breedId,
required this.hens,
});
Map<String, dynamic> toMap() {
return {
'breedId': breedId,
'hens': hens.map((e) => e.toMap()).toList(),
};
}
factory BreedSetting.fromMap(Map<String, dynamic> map) {
return BreedSetting(
breedId: map['breedId'],
hens: ((map['hens'] ?? []) as List)
.map((e) => Hen.fromMap(Map<String, dynamic>.from(e)))
.toList(),
);
}
int get activeHensCount {
return hens.where((hen) => hen.isLaying).length;
}
double get dailyProduction =>
activeHensCount * breedFromId(breedId).eggsPerHenPerDay;
}
class Customer {
final String id;
final String firstName;
final String lastName;
final String phoneNumber;
Customer({
required this.id,
required this.firstName,
required this.lastName,
required this.phoneNumber,
});
String get fullName => '$firstName $lastName'.trim();
Map<String, dynamic> toMap() {
return {
'id': id,
'firstName': firstName,
'lastName': lastName,
'phoneNumber': phoneNumber,
};
}
factory Customer.fromMap(Map<String, dynamic> map) {
return Customer(
id: map['id'],
firstName: map['firstName'],
lastName: map['lastName'],
phoneNumber: map['phoneNumber'],
);
}
}
class Reservation {
final String id;
final String customerName;
final String breedId;
final int eggsRequested;
final DateTime createdAt;
final DateTime deliveryDate;
bool isDelivered;
final String source;
final String clientNote;
Reservation({
required this.id,
required this.customerName,
required this.breedId,
required this.eggsRequested,
required this.createdAt,
required this.deliveryDate,
this.isDelivered = false,
this.source = 'manual',
this.clientNote = '',
});
Map<String, dynamic> toMap() {
return {
'id': id,
'customerName': customerName,
'breedId': breedId,
'eggsRequested': eggsRequested,
'createdAt': createdAt.toIso8601String(),
'deliveryDate': deliveryDate.toIso8601String(),
'isDelivered': isDelivered,
'source': source,
'clientNote': clientNote,
};
}
factory Reservation.fromMap(Map<String, dynamic> map) {
return Reservation(
id: map['id'],
customerName: map['customerName'] ?? 'Sconosciuto',
breedId: map['breedId'],
eggsRequested: map['eggsRequested'],
createdAt: DateTime.parse(map['createdAt']),
deliveryDate: DateTime.parse(map['deliveryDate']),
isDelivered: map['isDelivered'] ?? false,
source: map['source'] ?? 'manual',
clientNote: map['clientNote'] ?? '',
);
}
}
class CoopIcon extends StatelessWidget {
final double size;
final Color? color;
const CoopIcon({super.key, this.size = 24, this.color});
@override
Widget build(BuildContext context) {
final iconColor = color ?? Theme.of(context).colorScheme.primary;
return SizedBox(
width: size,
height: size,
child: CustomPaint(
painter: _CoopPainter(iconColor),
),
);
}
}
class _CoopPainter extends CustomPainter {
final Color color;
_CoopPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = size.width / 12
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
final w = size.width;
final h = size.height;
final path = Path();
// Main structure
path.moveTo(w * 0.2, h * 0.85);
path.lineTo(w * 0.8, h * 0.85);
path.lineTo(w * 0.8, h * 0.4);
path.lineTo(w * 0.2, h * 0.4);
path.close();
// Roof
path.moveTo(w * 0.1, h * 0.4);
path.lineTo(w * 0.5, h * 0.1);
path.lineTo(w * 0.9, h * 0.4);
// Legs
path.moveTo(w * 0.3, h * 0.85);
path.lineTo(w * 0.3, h * 1.0);
path.moveTo(w * 0.7, h * 0.85);
path.lineTo(w * 0.7, h * 1.0);
// Window/Mesh
path.addRect(Rect.fromLTWH(w * 0.4, h * 0.55, w * 0.2, h * 0.2));
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class StorageService {
static const String breedsKey = 'breed_settings_v2';
static const String reservationsKey = 'reservations_v2';
static const String customersKey = 'customers_v1';
static const String warehouseStockKey = 'warehouse_stock_v1';
static const String lastProductionUpdateKey = 'last_production_update_v1';
static Future<Map<String, double>> loadWarehouseStock() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(warehouseStockKey);
if (raw == null || raw.isEmpty) return {};
final map = jsonDecode(raw) as Map<String, dynamic>;
return map.map((k, v) => MapEntry(k, (v as num).toDouble()));
}
static Future<void> saveWarehouseStock(Map<String, double> stock) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(warehouseStockKey, jsonEncode(stock));
}
static Future<DateTime?> loadLastProductionUpdate() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(lastProductionUpdateKey);
if (raw == null || raw.isEmpty) return null;
return DateTime.parse(raw);
}
static Future<void> saveLastProductionUpdate(DateTime date) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(lastProductionUpdateKey, date.toIso8601String());
}
static Future<List<BreedSetting>> loadBreedSettings() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(breedsKey);
if (raw == null || raw.isEmpty) return [];
final list = jsonDecode(raw) as List;
return list.map((e) => BreedSetting.fromMap(Map<String, dynamic>.from(e))).toList();
}
static Future<void> saveBreedSettings(List<BreedSetting> items) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(breedsKey, jsonEncode(items.map((e) => e.toMap()).toList()));
}
static Future<List<Reservation>> loadReservations() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(reservationsKey);
if (raw == null || raw.isEmpty) return [];
final list = jsonDecode(raw) as List;
return list.map((e) => Reservation.fromMap(Map<String, dynamic>.from(e))).toList();
}
static Future<void> saveReservations(List<Reservation> items) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(reservationsKey, jsonEncode(items.map((e) => e.toMap()).toList()));
}
static Future<List<Customer>> loadCustomers() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(customersKey);
if (raw == null || raw.isEmpty) return [];
final list = jsonDecode(raw) as List;
return list.map((e) => Customer.fromMap(Map<String, dynamic>.from(e))).toList();
}
static Future<void> saveCustomers(List<Customer> items) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(customersKey, jsonEncode(items.map((e) => e.toMap()).toList()));
}
}
class HomePage extends StatefulWidget {
final VoidCallback onLanguageChanged;
final VoidCallback onModeChanged;
const HomePage({super.key, required this.onLanguageChanged, required this.onModeChanged});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
List<BreedSetting> breedSettings = [];
List<Reservation> reservations = [];
List<Customer> customers = [];
Map<String, double> warehouseStock = {};
DateTime? lastProductionUpdate;
bool loading = true;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
BannerAd? _bannerAd;
bool _isBannerAdReady = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_loadData();
_selectedDay = _focusedDay;
_loadBannerAd();
}
void _loadBannerAd() {
_bannerAd = BannerAd(
adUnitId: Platform.isAndroid
? 'ca-app-pub-2524986836023937/5718100842' // TODO: INSERISCI QUI IL TUO AD UNIT ID ANDROID REALE (deve contenere lo slash /)
: 'ca-app-pub-2524986836023937/5718100842',
request: const AdRequest(),
size: AdSize.banner,
listener: BannerAdListener(
onAdLoaded: (_) {
setState(() {
_isBannerAdReady = true;
});
},
onAdFailedToLoad: (ad, err) {
debugPrint('BannerAd failed to load: ${err.message}');
_isBannerAdReady = false;
ad.dispose();
},
),
);
_bannerAd!.load();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_bannerAd?.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_loadData();
}
}
List<Reservation> _getEventsForDay(DateTime day) {
return reservations.where((r) => !r.isDelivered && isSameDay(r.deliveryDate, day)).toList();
}
Future<void> _loadData() async {
final loadedBreeds = await StorageService.loadBreedSettings();
final loadedReservations = await StorageService.loadReservations();
final loadedCustomers = await StorageService.loadCustomers();
final loadedStock = await StorageService.loadWarehouseStock();
final loadedDate = await StorageService.loadLastProductionUpdate();
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
Map<String, double> newStock = Map.from(loadedStock);
// Se è la prima volta (loadedDate null), inizializziamo a ieri
// in modo che oggi venga già aggiunta la produzione odierna all'apertura.
DateTime lastUpdate = loadedDate ?? today.subtract(const Duration(days: 1));
if (lastUpdate.isBefore(today)) {
final daysPassed = today.difference(lastUpdate).inDays;
if (daysPassed > 0) {
bool productionAdded = false;
for (var breed in loadedBreeds) {
if (breed.activeHensCount > 0) {
final prod = breed.dailyProduction;
newStock[breed.breedId] = (newStock[breed.breedId] ?? 0) + (prod * daysPassed);
productionAdded = true;
}
}
if (productionAdded) {
await StorageService.saveWarehouseStock(newStock);
}
}
await StorageService.saveLastProductionUpdate(today);
}
setState(() {
breedSettings = loadedBreeds;
reservations = loadedReservations;
customers = loadedCustomers;
warehouseStock = newStock;
lastProductionUpdate = today;
loading = false;
});
_refreshDates();
await StorageService.saveReservations(reservations);
// Controllo primo avvio
final prefs = await SharedPreferences.getInstance();
final firstLaunch = prefs.getBool('first_launch_done') ?? false;
if (!firstLaunch) {
if (!mounted) return;
await Navigator.push(context, MaterialPageRoute(builder: (_) => const OnboardingPage()));
await prefs.setBool('first_launch_done', true);
}
}
void _refreshDates() {
final today = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
Map<String, double> virtualStock = Map.from(warehouseStock);
// Ordina per creazione per mantenere la coda FIFO
reservations.sort((a, b) => a.createdAt.compareTo(b.createdAt));
List<Reservation> updated = [];
for (var r in reservations) {
if (r.isDelivered) {
updated.add(r);
continue;
}
final production = _dailyProductionForBreed(r.breedId);
virtualStock[r.breedId] = (virtualStock[r.breedId] ?? 0) - r.eggsRequested;
if (virtualStock[r.breedId]! >= 0) {
updated.add(Reservation(
id: r.id,
customerName: r.customerName,
breedId: r.breedId,
eggsRequested: r.eggsRequested,
createdAt: r.createdAt,
deliveryDate: today,
isDelivered: r.isDelivered,
));
} else {
if (production <= 0) {
updated.add(r);
continue;
}
double missingEggs = -virtualStock[r.breedId]!;
int days = (missingEggs / production).ceil();
DateTime newDate = today.add(Duration(days: days));
updated.add(Reservation(
id: r.id,
customerName: r.customerName,
breedId: r.breedId,
eggsRequested: r.eggsRequested,
createdAt: r.createdAt,
deliveryDate: newDate,
isDelivered: r.isDelivered,
));
}
}
setState(() {
reservations = updated;
reservations.sort((a, b) => a.deliveryDate.compareTo(b.deliveryDate));
});
}
Future<void> _saveBreeds() async {
await StorageService.saveBreedSettings(breedSettings);
setState(() {});
}
Future<void> _saveReservations() async {
await StorageService.saveReservations(reservations);
setState(() {});
}
Future<void> _saveCustomers() async {
await StorageService.saveCustomers(customers);
setState(() {});
}
Future<void> _saveWarehouseStock() async {
await StorageService.saveWarehouseStock(warehouseStock);
setState(() {});
}
double _dailyProductionForBreed(String breedId) {
final index = breedSettings.indexWhere((e) => e.breedId == breedId);
if (index == -1) return 0;
return breedSettings[index].dailyProduction;
}
DateTime _calculateDeliveryDate(String breedId, int eggsRequested) {
final production = _dailyProductionForBreed(breedId);
final today = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
final pendingEggs = reservations
.where((r) => r.breedId == breedId && !r.isDelivered)
.fold<int>(0, (sum, r) => sum + r.eggsRequested);
final availableStock = warehouseStock[breedId] ?? 0;
final totalEggsNeeded = pendingEggs + eggsRequested - availableStock;
if (totalEggsNeeded <= 0) return today;
if (production <= 0) return today;
final neededDays = (totalEggsNeeded / production).ceil();
return today.add(Duration(days: neededDays));
}
int _daysFromToday(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final target = DateTime(date.year, date.month, date.day);
return target.difference(today).inDays;
}
Future<void> _openSettings() async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SettingsPage(
initialSettings: breedSettings,
initialReservations: reservations,
initialCustomers: customers,
onSave: (updatedBreeds, updatedReservations, updatedCustomers) async {
breedSettings = updatedBreeds;
reservations = updatedReservations;
customers = updatedCustomers;
await _saveBreeds();
await _saveReservations();
await _saveCustomers();
},
onLanguageChanged: widget.onLanguageChanged,
onModeChanged: widget.onModeChanged,
),
),
);
await _loadData();
}
Future<void> _importOrder() async {
final t = TranslationService.t;
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final content = await file.readAsString();
final data = jsonDecode(content);
// Validation
if (data['order_id'] == null || data['timestamp'] == null || data['egg_type'] == null || data['quantity'] == null) {
throw Exception(t('invalid_order_format'));
}
final orderId = data['order_id'];
if (reservations.any((r) => r.id == orderId)) {
throw Exception(t('duplicate_order'));
}
final breedId = data['egg_type'];
final quantity = data['quantity'] is int ? data['quantity'] as int : int.parse(data['quantity'].toString());
final clientNote = data['client_note'] ?? '';
final createdAt = DateTime.parse(data['timestamp']);
final deliveryDate = _calculateDeliveryDate(breedId, quantity);
final String firstName = data['client_first_name'] ?? '';
final String lastName = data['client_last_name'] ?? '';
final String phone = data['client_phone'] ?? '';
String customerName = 'Cliente Importato';
if (firstName.isNotEmpty || lastName.isNotEmpty) {
customerName = '$firstName $lastName'.trim();
bool exists = customers.any((c) => c.firstName == firstName && c.lastName == lastName);
if (!exists) {
customers.add(Customer(
id: DateTime.now().microsecondsSinceEpoch.toString(),
firstName: firstName,
lastName: lastName,
phoneNumber: phone,
));
await _saveCustomers();
}
}
final newReservation = Reservation(
id: orderId,
customerName: customerName,
breedId: breedId,
eggsRequested: quantity,
createdAt: createdAt,
deliveryDate: deliveryDate,
source: 'imported',
clientNote: clientNote,
);
setState(() {
reservations.add(newReservation);
});
_refreshDates();
await _saveReservations();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t('order_imported')), backgroundColor: Colors.green),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${t('import_error')} $e'), backgroundColor: Colors.red),
);
}
}
List<BreedSetting> get breedsWithActiveHens =>
breedSettings.where((e) => e.activeHensCount > 0).toList();
Future<void> _openAddReservation() async {
if (breedSettings.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(TranslationService.t('config_breed_first'))),
);
return;
}
final result = await showModalBottomSheet<Reservation>(
context: context,
isScrollControlled: true,
builder: (_) => AddReservationSheet(
availableBreeds: breedsWithActiveHens,
calculateDeliveryDate: _calculateDeliveryDate,
customers: customers,
),
);
if (result != null) {
reservations.add(result);
_refreshDates();
await _saveReservations();
}
}
Future<void> _openEditReservation(Reservation reservation) async {
final result = await showModalBottomSheet<Reservation>(
context: context,
isScrollControlled: true,
builder: (_) => AddReservationSheet(
availableBreeds: breedsWithActiveHens,
calculateDeliveryDate: _calculateDeliveryDate,
initialReservation: reservation,
customers: customers,
),
);
if (result != null) {
final index = reservations.indexWhere((e) => e.id == reservation.id);
if (index != -1) {
reservations[index] = result;
_refreshDates();
await _saveReservations();
}
}
}
Future<void> _openCustomersPage() async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CustomersPage(
initialCustomers: customers,
onSave: (updated) async {
setState(() => customers = updated);
await _saveCustomers();
},
),
),
);
}
void _shareReservation(Reservation r, BuildContext context) {
final t = TranslationService.t;
final breed = breedFromId(r.breedId);
final dateStr = DateFormat('dd/MM/yyyy').format(r.deliveryDate);
final message = t('share_order_msg', arg: [
r.customerName,
r.eggsRequested.toString(),
t(breed.name),
dateStr,
]);
final RenderBox? box = context.findRenderObject() as RenderBox?;
Share.share(
message,
sharePositionOrigin: box != null ? box.localToGlobal(Offset.zero) & box.size : null,
);
}
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text(t('app_title')),
actions: [
IconButton(
onPressed: _openCustomersPage,
icon: const Icon(Icons.people_outline),
tooltip: t('customers_list'),
),
IconButton(onPressed: _openSettings, icon: const Icon(Icons.settings)),
],
bottom: TabBar(
tabs: [
Tab(text: t('orders_list'), icon: const Icon(Icons.list)),
Tab(text: t('calendar'), icon: const Icon(Icons.calendar_month)),
Tab(text: t('warehouse'), icon: const Icon(Icons.inventory_2_outlined)),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _openAddReservation,
icon: const Icon(Icons.add),
label: Text(t('new_order')),
),
body: loading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Expanded(
child: TabBarView(
children: [
RefreshIndicator(
onRefresh: _loadData,
child: _buildOrdersListTab(),
),
_buildCalendarTab(),
_buildWarehouseTab(),
],
),
),
if (_isBannerAdReady)
SizedBox(
width: _bannerAd!.size.width.toDouble(),
height: _bannerAd!.size.height.toDouble(),
child: AdWidget(ad: _bannerAd!),
),
],
),
),
);
}
Widget _buildOrdersListTab() {
final t = TranslationService.t;
return ListView(
padding: const EdgeInsets.all(16),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(t('reservations'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
TextButton.icon(
onPressed: _importOrder,
icon: const Icon(Icons.file_download_outlined),
label: Text(t('import_order')),
),
],
),
const SizedBox(height: 8),
if (reservations.where((r) => !r.isDelivered).isEmpty) Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(t('no_reservations')), const SizedBox(height: 8), Text(t('use_button'))],
),
),
) else ...reservations.where((r) {
if (r.isDelivered) return false;
final days = _daysFromToday(r.deliveryDate);
return days >= -1; // Mostra solo se oggi, futuro o scaduti da max 1 giorno
}).map((r) => _buildReservationCard(r)),
],
);
}
Widget _buildWarehouseTab() {
final t = TranslationService.t;
final activeBreeds = breedsWithActiveHens;
if (activeBreeds.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(t('no_breeds_sel'), textAlign: TextAlign.center, style: const TextStyle(fontSize: 16)),
),
);
}
return ListView(
padding: const EdgeInsets.all(16),
children: activeBreeds.map((b) {
final breed = breedFromId(b.breedId);
final stock = (warehouseStock[b.breedId] ?? 0).floor();
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundImage: AssetImage(breed.assetPath),
radius: 24,
),
title: Text(t(breed.name), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
subtitle: Text('${t('eggs_in_stock')}: $stock'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _editWarehouseStock(b.breedId),
),
),
);
}).toList(),
);
}
Future<void> _editWarehouseStock(String breedId) async {
final t = TranslationService.t;
final currentStock = (warehouseStock[breedId] ?? 0).floor();
final controller = TextEditingController(text: currentStock.toString());
final result = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(t('edit_stock')),
content: TextField(
controller: controller,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(t('cancel'))),
FilledButton(
onPressed: () => Navigator.pop(ctx, controller.text),
child: Text(t('save')),
),
],
),
);
if (result != null) {
final newStock = int.tryParse(result);
if (newStock != null && newStock >= 0) {
setState(() {
warehouseStock[breedId] = newStock.toDouble();
});
_refreshDates();
await _saveWarehouseStock();
await _saveReservations();
}
}
}
Widget _buildCalendarTab() {
return Column(
children: [
TableCalendar<Reservation>(
firstDay: DateTime.now().subtract(const Duration(days: 365)),
lastDay: DateTime.now().add(const Duration(days: 365)),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
eventLoader: _getEventsForDay,
calendarStyle: CalendarStyle(
markerDecoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle),
todayDecoration: BoxDecoration(color: Colors.green.withOpacity(0.5), shape: BoxShape.circle),
selectedDecoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle),
),
headerStyle: const HeaderStyle(formatButtonVisible: false, titleCentered: true),
),
const Divider(),
Expanded(
child: _selectedDay == null
? const SizedBox()
: ListView(
padding: const EdgeInsets.all(16),
children: _getEventsForDay(_selectedDay!).map((r) => _buildReservationCard(r)).toList(),
),
),
],
);
}
Widget _buildReservationCard(Reservation r) {
final t = TranslationService.t;
final breed = breedFromId(r.breedId);
final days = _daysFromToday(r.deliveryDate);
String badgeText;
Color badgeColor;
if (days < 0) {
badgeText = t('expired');
badgeColor = Colors.red.shade100;
} else if (days == 0) {
badgeText = t('ready_delivery');
badgeColor = Colors.yellow.shade200;
} else if (days == 1) {
badgeText = t('in_day');
badgeColor = Colors.green.shade100;
} else {
badgeText = t('in_days', arg: days.toString());
badgeColor = Colors.green.shade100;
}
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
contentPadding: const EdgeInsets.all(16),
title: Text(r.customerName, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${t('breed')}: ${t(breed.name)}'),
Text('${t('eggs_req')}: ${r.eggsRequested}'),
Text('${t('delivery')}: ${DateFormat('dd/MM/yyyy').format(r.deliveryDate)}'),
if (r.clientNote.isNotEmpty) ...[
const SizedBox(height: 4),
Text('${t('client_note')}: ${r.clientNote}', style: const TextStyle(fontStyle: FontStyle.italic)),
],
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(color: badgeColor, borderRadius: BorderRadius.circular(20)),
child: Text(badgeText, style: const TextStyle(fontWeight: FontWeight.w600)),
),
],
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Conferma Consegna'),
content: const Text('Vuoi segnare questo ordine come evaso e sottrarre le uova dal magazzino?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(t('cancel'))),
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Conferma')),
],
),
);
if (confirm == true) {
setState(() {
r.isDelivered = true;
warehouseStock[r.breedId] = (warehouseStock[r.breedId] ?? 0) - r.eggsRequested;
});
_refreshDates();
await _saveWarehouseStock();
await _saveReservations();
}
},
icon: const Icon(Icons.check_circle_outline, color: Colors.green),
),
Builder(builder: (ctx) {
return IconButton(
onPressed: () => _shareReservation(r, ctx),
icon: const Icon(Icons.share_outlined),
color: Colors.blue,
);
}),
IconButton(
onPressed: () => _openEditReservation(r),
icon: const Icon(Icons.edit_outlined),
),
IconButton(
onPressed: () async {
if (await showConfirmDialog(context)) {
reservations.removeWhere((e) => e.id == r.id);
_saveReservations();
}
},
icon: const Icon(Icons.delete_outline),
),
],
),
),
);
}
}
class SettingsPage extends StatefulWidget {
final List<BreedSetting> initialSettings;
final List<Reservation> initialReservations;
final List<Customer> initialCustomers;
final Future<void> Function(List<BreedSetting>, List<Reservation>, List<Customer>) onSave;
final VoidCallback onLanguageChanged;
final VoidCallback onModeChanged;
const SettingsPage({
super.key,
required this.initialSettings,
required this.initialReservations,
required this.initialCustomers,
required this.onSave,
required this.onLanguageChanged,
required this.onModeChanged,
});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
late List<BreedSetting> settings;
late List<Reservation> reservations;
late List<Customer> customers;
@override
void initState() {
super.initState();
settings = widget.initialSettings.map((e) => BreedSetting(breedId: e.breedId, hens: List<Hen>.from(e.hens))).toList();
reservations = List<Reservation>.from(widget.initialReservations);
customers = List<Customer>.from(widget.initialCustomers);
}
bool _isSelected(String breedId) => settings.any((e) => e.breedId == breedId);
Future<void> _toggleBreed(String breedId, bool selected) async {
if (!selected) {
if (!await showDeactivateDialog(context)) return;
}
setState(() {
if (selected) {
if (!_isSelected(breedId)) {
settings.add(BreedSetting(breedId: breedId, hens: []));
}
} else {
settings.removeWhere((e) => e.breedId == breedId);
}
});
await widget.onSave(settings, reservations, customers);
}
Future<void> _changeLanguage(AppLanguage? lang) async {
if (lang == null) return;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('app_language', lang.name);
TranslationService.setLanguage(lang);
widget.onLanguageChanged();
setState(() {});
await widget.onSave(settings, reservations, customers);
}
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text(t('settings')),
bottom: TabBar(tabs: [
Tab(text: t('available_breeds')),
Tab(text: t('my_coop')),
Tab(icon: const Icon(Icons.backup_outlined)),
]),
actions: [
DropdownButton<AppLanguage>(
value: TranslationService.currentLanguage,
underline: const SizedBox(),
items: AppLanguage.values.map((l) {
String label;
String flag;
switch (l) {
case AppLanguage.it: label = 'Italiano'; flag = '🇮🇹'; break;
case AppLanguage.en: label = 'English'; flag = '🇬🇧'; break;
case AppLanguage.fr: label = 'Français'; flag = '🇫🇷'; break;
case AppLanguage.de: label = 'Deutsch'; flag = '🇩🇪'; break;
}
return DropdownMenuItem(
value: l,
child: Row(
children: [
Text(flag, style: const TextStyle(fontSize: 18)),
const SizedBox(width: 8),
Text(label),
],
),
);
}).toList(),
onChanged: _changeLanguage,
),
],
),
body: TabBarView(children: [
_buildCatalogTab(),
_buildMyHensTab(),
_buildBackupTab(),
]),
),
);
}
Widget _buildBackupTab() {
final t = TranslationService.t;
return ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading: const Icon(Icons.upload, color: Colors.green),
title: Text(t('export_data')),
onTap: _exportData,
),
),
const SizedBox(height: 12),
Card(
child: ListTile(
leading: const Icon(Icons.download, color: Colors.green),
title: Text(t('import_data')),
onTap: _importData,
),
),
const SizedBox(height: 12),
Card(
child: ListTile(
leading: const Icon(Icons.help_outline, color: Colors.green),
title: Text(t('show_intro')),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const OnboardingPage())),
),
),
const SizedBox(height: 12),
Card(
child: ListTile(
leading: const Icon(Icons.info_outline, color: Colors.green),
title: Text(t('about')),
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AboutPage())),
),
),
const SizedBox(height: 12),
Card(
child: ListTile(
leading: const Icon(Icons.swap_horiz, color: Colors.green),
title: Text(t('user_mode')),
onTap: () async {
final prefs = await SharedPreferences.getInstance();
final currentMode = prefs.getString('user_mode') ?? 'producer';
if (!mounted) return;
final newMode = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(t('mode_selection_title')),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<String>(
title: Text(t('i_am_producer')),
value: 'producer',
groupValue: currentMode,
onChanged: (val) => Navigator.pop(ctx, val),
),
RadioListTile<String>(
title: Text(t('i_am_client')),
value: 'client',
groupValue: currentMode,
onChanged: (val) => Navigator.pop(ctx, val),
),
],
),
),
);
if (newMode != null && newMode != currentMode) {
await prefs.setString('user_mode', newMode);
widget.onModeChanged();
if (mounted) {
Navigator.pop(context); // Chiude le impostazioni e torna alla root
}
}
},
),
),
],
);
}
void _exportData() async {
// Verifichiamo se il widget è ancora montato prima di usare il context
if (!mounted) return;
final data = {
'breedSettings': settings.map((e) => e.toMap()).toList(),
'reservations': reservations.map((e) => e.toMap()).toList(),
'customers': customers.map((e) => e.toMap()).toList(),
};
final jsonStr = jsonEncode(data);
final t = TranslationService.t;
try {
final tempDir = await getTemporaryDirectory();
final file = File('${tempDir.path}/backup_uova.json');
await file.writeAsString(jsonStr);
if (!mounted) return;
final xFile = XFile(file.path, mimeType: 'application/json');
final RenderBox? box = context.findRenderObject() as RenderBox?;
await Share.shareXFiles(
[xFile],
subject: 'Backup Henflow',
sharePositionOrigin: box != null ? box.localToGlobal(Offset.zero) & box.size : null,
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore durante l\'esportazione: $e')),
);
}
}
void _importData() async {
if (!mounted) return;
final t = TranslationService.t;
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final content = await file.readAsString();
final data = jsonDecode(content);
final newSettings = (data['breedSettings'] as List)
.map((e) => BreedSetting.fromMap(Map<String, dynamic>.from(e)))
.toList();
final newReservations = (data['reservations'] as List)
.map((e) => Reservation.fromMap(Map<String, dynamic>.from(e)))
.toList();
final newCustomers = (data['customers'] != null)
? (data['customers'] as List)
.map((e) => Customer.fromMap(Map<String, dynamic>.from(e)))
.toList()
: <Customer>[];
setState(() {
settings = newSettings;
reservations = newReservations;
customers = newCustomers;
});
await widget.onSave(settings, reservations, customers);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t('import_success'))),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t('import_error')), backgroundColor: Colors.red),
);
}
}
Widget _buildCatalogTab() {
final t = TranslationService.t;
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: breedCatalog.length,
itemBuilder: (context, index) {
final breed = breedCatalog[index];
return Card(
child: CheckboxListTile(
title: Text(t(breed.name), style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(t(breed.description)),
secondary: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
breed.assetPath,
width: 45,
height: 45,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 45,
height: 45,
color: Colors.green.shade50,
child: const Center(child: CoopIcon(size: 24, color: Colors.green)),
),
),
),
controlAffinity: ListTileControlAffinity.trailing,
value: _isSelected(breed.id),
onChanged: (val) => _toggleBreed(breed.id, val ?? false),
activeColor: Colors.green,
),
);
},
);
}
Widget _buildMyHensTab() {
final t = TranslationService.t;
if (settings.isEmpty) return Center(child: Padding(padding: const EdgeInsets.all(32.0), child: Text(t('no_breeds_sel'), textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, color: Colors.grey))));
final totalDailyProduction = settings.fold<double>(0, (sum, item) => sum + item.dailyProduction);
return ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t('daily_prod_est'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text('${totalDailyProduction.toStringAsFixed(1)} ${t('eggs_per_day')}',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w700)),
const SizedBox(height: 12),
...settings.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text('${t(breedFromId(e.breedId).name)} (${e.hens.length} ${t('hens')})')),
Text('${e.dailyProduction.toStringAsFixed(1)} ${t('eggs_per_g')}',
style: const TextStyle(fontWeight: FontWeight.w600)),
],
),
)),
],
),
),
),
...settings.map((setting) {
final breed = breedFromId(setting.breedId);
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
breed.assetPath,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 50,
height: 50,
color: Colors.green.shade50,
child: const Center(child: CoopIcon(size: 28, color: Colors.green)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(t(breed.name), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
),
const CoopIcon(color: Colors.green),
],
),
const Divider(),
Text('${t('reg_hens')}: ${setting.hens.length}'),
Text('${t('in_laying')}: ${setting.activeHensCount}'),
Text('${t('est_prod')}: ${setting.dailyProduction.toStringAsFixed(1)} ${t('eggs_per_day')}', style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: FilledButton.tonalIcon(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BreedDetailPage(
setting: setting,
onChanged: () async {
setState(() {});
// Salvataggio immediato quando cambiano le galline
await StorageService.saveBreedSettings(settings);
},
),
),
);
setState(() {});
},
icon: const Icon(Icons.edit_note),
label: Text(t('manage_hens')),
),
),
],
),
),
);
}),
],
);
}
}
class BreedDetailPage extends StatefulWidget {
final BreedSetting setting;
final VoidCallback onChanged;
const BreedDetailPage({super.key, required this.setting, required this.onChanged});
@override
State<BreedDetailPage> createState() => _BreedDetailPageState();
}
class _BreedDetailPageState extends State<BreedDetailPage> {
BreedCatalogItem get breed => breedFromId(widget.setting.breedId);
int _ageInMonths(DateTime birthDate) {
final now = DateTime.now();
int months = (now.year - birthDate.year) * 12 + now.month - birthDate.month;
if (now.day < birthDate.day) months--;
return months < 0 ? 0 : months;
}
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return Scaffold(
appBar: AppBar(
title: Text(t('breed_sheet', arg: t(breed.name))),
),
floatingActionButton: FloatingActionButton.extended(onPressed: () async { final hen = await showDialog<Hen>(context: context, builder: (_) => const AddHenDialog()); if (hen != null) { setState(() => widget.setting.hens.add(hen)); widget.onChanged(); } }, icon: const Icon(Icons.add), label: Text(t('new_hen'))),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
clipBehavior: Clip.antiAlias,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Image.asset(
breed.assetPath,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
height: 180,
color: Colors.green.shade100,
child: const Icon(Icons.image_not_supported, size: 50, color: Colors.green),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(t(breed.name), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
const SizedBox(height: 8), Text(t(breed.description)), const SizedBox(height: 8),
Text('${t('egg_color')}: ${t(breed.eggColor)}'),
Text('${t('reg_hens')}: ${widget.setting.hens.length}'),
Text('${t('laying_6_months')}: ${widget.setting.activeHensCount}'),
Text('${t('est_prod')}: ${widget.setting.dailyProduction.toStringAsFixed(1)} ${t('eggs_per_day')}'),
]),
),
]),
),
const SizedBox(height: 16), Text(t('hen_registry'), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
const SizedBox(height: 8),
if (widget.setting.hens.isEmpty) Card(child: Padding(padding: const EdgeInsets.all(16), child: Text(t('no_hens_reg'))))
else ...widget.setting.hens.map((hen) => Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
title: Text(hen.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(height: 6),
Text('${t('birth')}: ${DateFormat('dd/MM/yyyy').format(hen.birthDate)}'),
Text(t('age_months', arg: _ageInMonths(hen.birthDate).toString())),
Text(_ageInMonths(hen.birthDate) >= 6
? (hen.isLaying ? t('status_laying') : t('status_rest'))
: (hen.isLaying ? t('status_laying') : t('status_young'))),
]),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
Switch(value: hen.isLaying, onChanged: (val) { setState(() => hen.isLaying = val); widget.onChanged(); }),
IconButton(
onPressed: () async {
if (await showConfirmDialog(context)) {
setState(() => widget.setting.hens.removeWhere((e) => e.id == hen.id));
widget.onChanged();
}
},
icon: const Icon(Icons.delete_outline),
),
]),
),
)),
],
),
);
}
}
class AddHenDialog extends StatefulWidget {
const AddHenDialog({super.key});
@override
State<AddHenDialog> createState() => _AddHenDialogState();
}
class _AddHenDialogState extends State<AddHenDialog> {
final nameController = TextEditingController();
DateTime? birthDate;
bool isLaying = true;
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return AlertDialog(
title: Text(t('new_hen')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(controller: nameController, decoration: InputDecoration(labelText: t('hen_name'), border: const OutlineInputBorder())),
const SizedBox(height: 12),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now().subtract(const Duration(days: 180)),
firstDate: DateTime(2010),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
birthDate = picked;
final now = DateTime.now();
int months = (now.year - picked.year) * 12 + now.month - picked.month;
if (now.day < picked.day) months--;
// Se ha meno di 6 mesi, disattiva il flag per default
isLaying = (months >= 6);
});
}
},
child: InputDecorator(
decoration: InputDecoration(labelText: t('birth_date'), border: const OutlineInputBorder()),
child: Text(birthDate == null ? t('select_date') : DateFormat('dd/MM/yyyy').format(birthDate!)),
),
),
const SizedBox(height: 12), SwitchListTile(title: Text(t('in_laying')), value: isLaying, onChanged: (val) => setState(() => isLaying = val), contentPadding: EdgeInsets.zero),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t('cancel'))),
FilledButton(onPressed: () { if (nameController.text.trim().isNotEmpty && birthDate != null) Navigator.pop(context, Hen(id: DateTime.now().microsecondsSinceEpoch.toString(), name: nameController.text.trim(), birthDate: birthDate!, isLaying: isLaying)); }, child: Text(t('save'))),
],
);
}
}
class AddCustomerDialog extends StatefulWidget {
final Customer? initialCustomer;
const AddCustomerDialog({super.key, this.initialCustomer});
@override
State<AddCustomerDialog> createState() => _AddCustomerDialogState();
}
class _AddCustomerDialogState extends State<AddCustomerDialog> {
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
final phoneController = TextEditingController();
@override
void initState() {
super.initState();
if (widget.initialCustomer != null) {
firstNameController.text = widget.initialCustomer!.firstName;
lastNameController.text = widget.initialCustomer!.lastName;
phoneController.text = widget.initialCustomer!.phoneNumber;
}
}
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return AlertDialog(
title: Text(widget.initialCustomer == null ? t('new_customer') : t('edit_customer')),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(controller: firstNameController, decoration: InputDecoration(labelText: t('first_name'))),
TextField(controller: lastNameController, decoration: InputDecoration(labelText: t('last_name'))),
TextField(controller: phoneController, decoration: InputDecoration(labelText: t('phone_number')), keyboardType: TextInputType.phone),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text(t('cancel'))),
TextButton(
onPressed: () {
if (firstNameController.text.trim().isEmpty) return;
Navigator.pop(
context,
Customer(
id: widget.initialCustomer?.id ?? DateTime.now().microsecondsSinceEpoch.toString(),
firstName: firstNameController.text.trim(),
lastName: lastNameController.text.trim(),
phoneNumber: phoneController.text.trim(),
),
);
},
child: Text(t('save')),
),
],
);
}
}
class CustomersPage extends StatefulWidget {
final List<Customer> initialCustomers;
final Future<void> Function(List<Customer>) onSave;
const CustomersPage({super.key, required this.initialCustomers, required this.onSave});
@override
State<CustomersPage> createState() => _CustomersPageState();
}
class _CustomersPageState extends State<CustomersPage> {
late List<Customer> customers;
@override
void initState() {
super.initState();
customers = List<Customer>.from(widget.initialCustomers);
}
Future<void> _addCustomer() async {
final result = await showDialog<Customer>(
context: context,
builder: (_) => const AddCustomerDialog(),
);
if (result != null) {
setState(() => customers.add(result));
await widget.onSave(customers);
}
}
Future<void> _editCustomer(Customer customer) async {
final result = await showDialog<Customer>(
context: context,
builder: (_) => AddCustomerDialog(initialCustomer: customer),
);
if (result != null) {
final index = customers.indexWhere((e) => e.id == customer.id);
if (index != -1) {
setState(() => customers[index] = result);
await widget.onSave(customers);
}
}
}
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
return Scaffold(
appBar: AppBar(title: Text(t('customers_list'))),
floatingActionButton: FloatingActionButton(
onPressed: _addCustomer,
child: const Icon(Icons.add),
),
body: customers.isEmpty
? Center(child: Text(t('no_customers')))
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: customers.length,
itemBuilder: (context, index) {
final c = customers[index];
return Card(
child: ListTile(
title: Text(c.fullName, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(c.phoneNumber),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => _editCustomer(c),
),
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () async {
if (await showConfirmDialog(context)) {
setState(() => customers.removeAt(index));
await widget.onSave(customers);
}
},
),
],
),
),
);
},
),
);
}
}
class AddReservationSheet extends StatefulWidget {
final List<BreedSetting> availableBreeds;
final DateTime Function(String breedId, int eggsRequested) calculateDeliveryDate;
final Reservation? initialReservation;
final List<Customer> customers;
const AddReservationSheet({
super.key,
required this.availableBreeds,
required this.calculateDeliveryDate,
required this.customers,
this.initialReservation,
});
@override
State<AddReservationSheet> createState() => _AddReservationSheetState();
}
class _AddReservationSheetState extends State<AddReservationSheet> {
final eggsController = TextEditingController(text: '6');
String? selectedBreedId;
String _currentCustomerName = '';
@override
void initState() {
super.initState();
_currentCustomerName = widget.initialReservation?.customerName ?? '';
if (widget.initialReservation != null) {
eggsController.text = widget.initialReservation!.eggsRequested.toString();
selectedBreedId = widget.initialReservation!.breedId;
} else {
final valid = widget.availableBreeds.where((e) => e.dailyProduction > 0).toList();
if (valid.isNotEmpty) selectedBreedId = valid.first.breedId;
}
}
@override
Widget build(BuildContext context) {
final t = TranslationService.t;
final keyboardInset = MediaQuery.of(context).viewInsets.bottom;
final previewEggs = int.tryParse(eggsController.text.trim()) ?? 0;
final validBreeds = widget.availableBreeds.where((e) => e.dailyProduction > 0).toList();
final previewDate = (selectedBreedId != null && previewEggs > 0) ? widget.calculateDeliveryDate(selectedBreedId!, previewEggs) : null;
return Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, keyboardInset + 16),
child: SingleChildScrollView(
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(widget.initialReservation == null ? t('new_order') : t('edit_order'), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Autocomplete<String>(
initialValue: TextEditingValue(text: _currentCustomerName),
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const Iterable<String>.empty();
}
return widget.customers
.map((c) => c.fullName)
.where((name) => name.toLowerCase().contains(textEditingValue.text.toLowerCase()));
},
onSelected: (String selection) {
setState(() => _currentCustomerName = selection);
},
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
// Sincronizziamo il valore quando l'utente scrive manualmente
return TextField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: t('customer_name'),
border: const OutlineInputBorder(),
),
onChanged: (val) {
setState(() => _currentCustomerName = val);
},
);
},
),
const SizedBox(height: 12),
if (validBreeds.isEmpty) Card(color: Colors.green.shade50, child: Padding(padding: const EdgeInsets.all(12), child: Text(t('need_hen_error'))))
else DropdownButtonFormField<String>(initialValue: selectedBreedId, decoration: InputDecoration(labelText: t('breed'), border: const OutlineInputBorder()), items: validBreeds.map((e) => DropdownMenuItem(value: e.breedId, child: Text(t(breedFromId(e.breedId).name)))).toList(), onChanged: (v) => setState(() => selectedBreedId = v)),
const SizedBox(height: 12),
TextField(controller: eggsController, keyboardType: TextInputType.number, decoration: InputDecoration(labelText: t('eggs_req'), border: const OutlineInputBorder()), onChanged: (_) => setState(() {})),
const SizedBox(height: 16),
if (previewDate != null) Card(color: Colors.green.shade50, child: Padding(padding: const EdgeInsets.all(12), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(t('delivery_preview'), style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 6), Text(DateFormat('dd/MM/yyyy').format(previewDate))]))),
const SizedBox(height: 8),
SizedBox(width: double.infinity, child: FilledButton(onPressed: validBreeds.isNotEmpty && _currentCustomerName.trim().isNotEmpty && previewEggs > 0 ? () => Navigator.pop(context, Reservation(id: widget.initialReservation?.id ?? DateTime.now().microsecondsSinceEpoch.toString(), customerName: _currentCustomerName.trim(), breedId: selectedBreedId!, eggsRequested: previewEggs, createdAt: widget.initialReservation?.createdAt ?? DateTime.now(), deliveryDate: previewDate!)) : null, child: Text(t('save_order')))),
]),
),
);
}
}