commit bfd086c488935cbfe75c6db435681b5432b0d2ee Author: Carlo Baratto Date: Fri Oct 9 08:51:15 2026 +0200 Import sorgenti Flutter di Henflow (export da Nextcloud: lib/ + documenti di progetto) diff --git a/DOCUMENTO_PROGETTO.md b/DOCUMENTO_PROGETTO.md new file mode 100644 index 0000000..03bfcf4 --- /dev/null +++ b/DOCUMENTO_PROGETTO.md @@ -0,0 +1,38 @@ +# Henflow - Documento di Progetto + +## Panoramica +**Henflow** è un'applicazione mobile sviluppata in Flutter per la gestione professionale di un piccolo pollaio e delle relative prenotazioni di uova. L'app permette di monitorare la produzione stimata in base alle galline possedute e di organizzare le consegne ai clienti in modo automatizzato. + +## Caratteristiche Principali +- **Gestione Produzione**: Calcolo automatico della produzione giornaliera basato sull'età e sullo stato delle galline. +- **Coda Prenotazioni**: Gestione FIFO degli ordini con ricalcolo dinamico delle date di consegna. +- **Anagrafica Clienti**: Rubrica integrata con ricerca rapida durante la creazione degli ordini. +- **Multilingua**: Supporto completo per Inglese (default), Italiano, Francese e Tedesco. +- **Backup e Ripristino**: Esportazione e importazione dei dati tramite file JSON compatibile con i principali servizi cloud. +- **Offline First**: Tutti i dati sono salvati localmente sul dispositivo. + +## Schermate dell'Applicazione + +### 1. Home Page +La schermata principale mostra il riepilogo della produzione giornaliera e la lista degli ordini attivi. Ogni ordine indica la data di consegna prevista e lo stato (Oggi, Scaduto, o giorni rimanenti). È possibile condividere i dettagli dell'ordine direttamente con il cliente. + +### 2. Gestione Clienti +Una sezione dedicata alla gestione dell'anagrafica. Permette di registrare Nome, Cognome e Numero di telefono dei clienti per velocizzare l'inserimento degli ordini tramite suggerimenti automatici. + +### 3. Configurazione Razze +In questa sezione l'utente può attivare le razze presenti nel proprio pollaio. Per ogni razza è disponibile una foto rappresentativa e una descrizione delle caratteristiche produttive (colore uova, produttività). + +### 4. Il Mio Pollaio +Mostra un riepilogo delle galline possedute per ogni razza attiva. Da qui è possibile accedere alla gestione analitica di ogni singolo animale. + +### 5. Anagrafica Galline +Scheda di dettaglio per ogni razza dove è possibile registrare le singole galline, impostare la loro data di nascita e monitorare il loro stato di deposizione. L'app calcola automaticamente l'età e determina se la gallina è produttiva (6+ mesi). + +### 6. Backup e Ripristino +Strumento per la messa in sicurezza dei dati. Permette di esportare l'intero database (clienti, ordini, galline) in un unico file JSON condivisibile via email, WhatsApp o salvabile su Google Drive/iCloud. + +## Specifiche Tecniche +- **Identificativo Pacchetto**: `it.carlobaratto.henflow` +- **Tecnologia**: Flutter SDK +- **Persistenza**: Shared Preferences / JSON Storage +- **Integrazioni**: File Picker, Share System API diff --git a/README.md b/README.md new file mode 100644 index 0000000..f145426 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Flutter - Prenotazione Uova (fix calendario) + +Correzione applicata: + +- aggiunto flutter_localizations +- configurati localizationsDelegates e supportedLocales +- impostato Locale('it', 'IT') +- inizializzato Intl.defaultLocale = 'it_IT' + +## Avvio + +```bash +flutter pub get +flutter run +``` + +## APK + +```bash +flutter build apk +``` \ No newline at end of file diff --git a/documento_progetto_uova.txt b/documento_progetto_uova.txt new file mode 100644 index 0000000..3c2a1e5 --- /dev/null +++ b/documento_progetto_uova.txt @@ -0,0 +1,123 @@ +Documento di Progetto +Feature: Modalità Utente (Produttore / Cliente) + Flusso Ordini Offline + +1. Obiettivo +Implementare un sistema di selezione modalità utente alla prima apertura dell’app, con possibilità di modifica successiva dalle impostazioni. + +Due modalità: +- Produttore +- Cliente + +2. Requisiti Funzionali + +2.1 Selezione iniziale +Alla prima apertura: +- mostrare schermata di scelta: + - "Sono un Produttore" + - "Sono un Cliente" +- salvare scelta in storage locale (user_mode) + +2.2 Modifica modalità +- accessibile da Impostazioni +- voce: Modalità utente +- conferma cambio modalità + +3. Architettura + +Variabile: +userMode: "producer" | "client" + +Persistenza: +- SharedPreferences / DataStore +- chiave: user_mode + +4. Modalità Cliente + +Flusso: +- selezione tipo uova: Marans / Araucana / Olive +- inserimento quantità +- click “Prenota” + +Output JSON: +{ + "order_id": "uuid", + "timestamp": "ISO8601", + "egg_type": "marans | araucana | olive", + "quantity": 12, + "client_note": "" +} + +File: +ordine_uova_.json + +Condivisione: +- share system (Intent Android) + +5. Modalità Produttore + +UI: +- bottone "Importa ordine" + +Flusso: +- apertura file picker +- selezione file JSON +- parsing + +Validazione: +- order_id +- timestamp +- egg_type +- quantity + +Errori: +- formato non valido +- duplicati + +Dopo import: +- calcolo data consegna +- inserimento in database + +6. Struttura dati + +{ + "order_id": "string", + "source": "imported | manual", + "egg_type": "string", + "quantity": "int", + "created_at": "datetime", + "delivery_date": "datetime" +} + +7. UX + +Cliente: +- semplice +- focus prenotazione + +Produttore: +- UI esistente + import + +8. Edge cases + +- cambio modalità +- file corrotti +- quantità non valida + +9. Non obiettivi + +- cloud +- login +- notifiche + +10. Priorità + +1. selettore modalità +2. flusso cliente +3. import produttore +4. validazione + +11. KPI + +- completamento prenotazioni +- import corretti +- tempo creazione ordine diff --git a/lib/app_language.dart b/lib/app_language.dart new file mode 100644 index 0000000..02798f6 --- /dev/null +++ b/lib/app_language.dart @@ -0,0 +1,480 @@ +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> _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', + }, + 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', + }, + 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', + 'name_generic': 'Pondeuse générique', + '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.', + 'desc_generic': 'Poule pondeuse de race mixte ou non spécifiée.', + '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, tutte 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 automaticamente 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 simple 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', + }, + 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', + }, + }; + + 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; + } +} diff --git a/lib/database_helper.dart b/lib/database_helper.dart new file mode 100644 index 0000000..9675b0e --- /dev/null +++ b/lib/database_helper.dart @@ -0,0 +1,77 @@ +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; + +class DatabaseHelper { + static final DatabaseHelper instance = DatabaseHelper._init(); + static Database? _database; + + DatabaseHelper._init(); + + Future get database async { + if (_database != null) return _database!; + _database = await _initDB('egg_bookings.db'); + return _database!; + } + + Future _initDB(String filePath) async { + final dbPath = await getDatabasesPath(); + final path = join(dbPath, filePath); + + return openDatabase( + path, + version: 1, + onCreate: _createDB, + ); + } + + Future _createDB(Database db, int version) async { + await db.execute(''' + CREATE TABLE bookings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + customer_name TEXT NOT NULL, + marans INTEGER NOT NULL DEFAULT 0, + araucana INTEGER NOT NULL DEFAULT 0, + olive INTEGER NOT NULL DEFAULT 0, + note TEXT NOT NULL DEFAULT '' + ) + '''); + } + + Future>> getAllBookings() async { + final db = await instance.database; + return db.query( + 'bookings', + orderBy: 'date ASC, customer_name ASC', + ); + } + + Future insertBooking(Map booking) async { + final db = await instance.database; + return db.insert('bookings', booking); + } + + Future updateBooking(Map booking) async { + final db = await instance.database; + return db.update( + 'bookings', + booking, + where: 'id = ?', + whereArgs: [booking['id']], + ); + } + + Future deleteBooking(int id) async { + final db = await instance.database; + return db.delete( + 'bookings', + where: 'id = ?', + whereArgs: [id], + ); + } + + Future close() async { + final db = await instance.database; + db.close(); + } +} \ No newline at end of file diff --git a/lib/dialogs/confirm_dialogs.dart b/lib/dialogs/confirm_dialogs.dart new file mode 100644 index 0000000..aad9d09 --- /dev/null +++ b/lib/dialogs/confirm_dialogs.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import '../app_language.dart'; + +Future showDeactivateDialog(BuildContext context) async { + final t = TranslationService.t; + return await showDialog( + 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 showConfirmDialog(BuildContext context) async { + final t = TranslationService.t; + return await showDialog( + 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; +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..bf273d5 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,2986 @@ +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> _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 showDeactivateDialog(BuildContext context) async { + final t = TranslationService.t; + return await showDialog( + 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 showConfirmDialog(BuildContext context) async { + final t = TranslationService.t; + return await showDialog( + 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 createState() => _EggBookingAppState(); +} + +class _EggBookingAppState extends State { + @override + void initState() { + super.initState(); + _loadLanguage(); + } + + Future _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 createState() => _RootPageState(); +} + +class _RootPageState extends State { + String? userMode; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _checkMode(); + } + + Future _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>( + 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 createState() => _ClientProfileDialogState(); +} + +class _ClientProfileDialogState extends State { + 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 createState() => _ClientHomePageState(); +} + +class _ClientHomePageState extends State { + final _quantityController = TextEditingController(text: '6'); + final _noteController = TextEditingController(); + String? _selectedBreedId; + + @override + void initState() { + super.initState(); + if (breedCatalog.isNotEmpty) { + _selectedBreedId = breedCatalog.first.id; + } + } + + Future _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( + 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( + 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 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 toMap() { + return { + 'id': id, + 'name': name, + 'birthDate': birthDate.toIso8601String(), + 'isLaying': isLaying, + }; + } + + factory Hen.fromMap(Map map) { + return Hen( + id: map['id'], + name: map['name'], + birthDate: DateTime.parse(map['birthDate']), + isLaying: map['isLaying'] ?? true, + ); + } +} + +class BreedSetting { + final String breedId; + List hens; + + BreedSetting({ + required this.breedId, + required this.hens, + }); + + Map toMap() { + return { + 'breedId': breedId, + 'hens': hens.map((e) => e.toMap()).toList(), + }; + } + + factory BreedSetting.fromMap(Map map) { + return BreedSetting( + breedId: map['breedId'], + hens: ((map['hens'] ?? []) as List) + .map((e) => Hen.fromMap(Map.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 toMap() { + return { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'phoneNumber': phoneNumber, + }; + } + + factory Customer.fromMap(Map 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 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 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> loadWarehouseStock() async { + final prefs = await SharedPreferences.getInstance(); + final raw = prefs.getString(warehouseStockKey); + if (raw == null || raw.isEmpty) return {}; + final map = jsonDecode(raw) as Map; + return map.map((k, v) => MapEntry(k, (v as num).toDouble())); + } + + static Future saveWarehouseStock(Map stock) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(warehouseStockKey, jsonEncode(stock)); + } + + static Future 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 saveLastProductionUpdate(DateTime date) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(lastProductionUpdateKey, date.toIso8601String()); + } + + static Future> 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.from(e))).toList(); + } + + static Future saveBreedSettings(List items) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(breedsKey, jsonEncode(items.map((e) => e.toMap()).toList())); + } + + static Future> 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.from(e))).toList(); + } + + static Future saveReservations(List items) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(reservationsKey, jsonEncode(items.map((e) => e.toMap()).toList())); + } + + static Future> 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.from(e))).toList(); + } + + static Future saveCustomers(List 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 createState() => _HomePageState(); +} + +class _HomePageState extends State with WidgetsBindingObserver { + List breedSettings = []; + List reservations = []; + List customers = []; + Map 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 _getEventsForDay(DateTime day) { + return reservations.where((r) => !r.isDelivered && isSameDay(r.deliveryDate, day)).toList(); + } + + Future _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 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 virtualStock = Map.from(warehouseStock); + + // Ordina per creazione per mantenere la coda FIFO + reservations.sort((a, b) => a.createdAt.compareTo(b.createdAt)); + + List 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 _saveBreeds() async { + await StorageService.saveBreedSettings(breedSettings); + setState(() {}); + } + + Future _saveReservations() async { + await StorageService.saveReservations(reservations); + setState(() {}); + } + + Future _saveCustomers() async { + await StorageService.saveCustomers(customers); + setState(() {}); + } + + Future _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(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 _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 _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 get breedsWithActiveHens => + breedSettings.where((e) => e.activeHensCount > 0).toList(); + + Future _openAddReservation() async { + if (breedSettings.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(TranslationService.t('config_breed_first'))), + ); + return; + } + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => AddReservationSheet( + availableBreeds: breedsWithActiveHens, + calculateDeliveryDate: _calculateDeliveryDate, + customers: customers, + ), + ); + if (result != null) { + reservations.add(result); + _refreshDates(); + await _saveReservations(); + } + } + + Future _openEditReservation(Reservation reservation) async { + final result = await showModalBottomSheet( + 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 _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 _editWarehouseStock(String breedId) async { + final t = TranslationService.t; + final currentStock = (warehouseStock[breedId] ?? 0).floor(); + final controller = TextEditingController(text: currentStock.toString()); + + final result = await showDialog( + 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( + 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( + 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 initialSettings; + final List initialReservations; + final List initialCustomers; + final Future Function(List, List, List) 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 createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + late List settings; + late List reservations; + late List customers; + + @override + void initState() { + super.initState(); + settings = widget.initialSettings.map((e) => BreedSetting(breedId: e.breedId, hens: List.from(e.hens))).toList(); + reservations = List.from(widget.initialReservations); + customers = List.from(widget.initialCustomers); + } + + bool _isSelected(String breedId) => settings.any((e) => e.breedId == breedId); + + Future _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 _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( + 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( + context: context, + builder: (ctx) => AlertDialog( + title: Text(t('mode_selection_title')), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RadioListTile( + title: Text(t('i_am_producer')), + value: 'producer', + groupValue: currentMode, + onChanged: (val) => Navigator.pop(ctx, val), + ), + RadioListTile( + 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.from(e))) + .toList(); + final newReservations = (data['reservations'] as List) + .map((e) => Reservation.fromMap(Map.from(e))) + .toList(); + final newCustomers = (data['customers'] != null) + ? (data['customers'] as List) + .map((e) => Customer.fromMap(Map.from(e))) + .toList() + : []; + + 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(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 createState() => _BreedDetailPageState(); +} + +class _BreedDetailPageState extends State { + 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(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 createState() => _AddHenDialogState(); +} + +class _AddHenDialogState extends State { + 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 createState() => _AddCustomerDialogState(); +} + +class _AddCustomerDialogState extends State { + 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 initialCustomers; + final Future Function(List) onSave; + const CustomersPage({super.key, required this.initialCustomers, required this.onSave}); + + @override + State createState() => _CustomersPageState(); +} + +class _CustomersPageState extends State { + late List customers; + + @override + void initState() { + super.initState(); + customers = List.from(widget.initialCustomers); + } + + Future _addCustomer() async { + final result = await showDialog( + context: context, + builder: (_) => const AddCustomerDialog(), + ); + if (result != null) { + setState(() => customers.add(result)); + await widget.onSave(customers); + } + } + + Future _editCustomer(Customer customer) async { + final result = await showDialog( + 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 availableBreeds; + final DateTime Function(String breedId, int eggsRequested) calculateDeliveryDate; + final Reservation? initialReservation; + final List customers; + const AddReservationSheet({ + super.key, + required this.availableBreeds, + required this.calculateDeliveryDate, + required this.customers, + this.initialReservation, + }); + @override + State createState() => _AddReservationSheetState(); +} + +class _AddReservationSheetState extends State { + 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( + initialValue: TextEditingValue(text: _currentCustomerName), + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return const Iterable.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(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')))), + ]), + ), + ); + } +} diff --git a/lib/models/breed_catalog.dart b/lib/models/breed_catalog.dart new file mode 100644 index 0000000..dc31523 --- /dev/null +++ b/lib/models/breed_catalog.dart @@ -0,0 +1,83 @@ +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 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, + ); +} diff --git a/lib/models/breed_setting.dart b/lib/models/breed_setting.dart new file mode 100644 index 0000000..ca8011c --- /dev/null +++ b/lib/models/breed_setting.dart @@ -0,0 +1,35 @@ +import 'breed_catalog.dart'; +import 'hen.dart'; + +class BreedSetting { + final String breedId; + List hens; + + BreedSetting({ + required this.breedId, + required this.hens, + }); + + Map toMap() { + return { + 'breedId': breedId, + 'hens': hens.map((e) => e.toMap()).toList(), + }; + } + + factory BreedSetting.fromMap(Map map) { + return BreedSetting( + breedId: map['breedId'], + hens: ((map['hens'] ?? []) as List) + .map((e) => Hen.fromMap(Map.from(e))) + .toList(), + ); + } + + int get activeHensCount { + return hens.where((hen) => hen.isLaying).length; + } + + double get dailyProduction => + activeHensCount * breedFromId(breedId).eggsPerHenPerDay; +} diff --git a/lib/models/customer.dart b/lib/models/customer.dart new file mode 100644 index 0000000..a66ab93 --- /dev/null +++ b/lib/models/customer.dart @@ -0,0 +1,33 @@ +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 toMap() { + return { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'phoneNumber': phoneNumber, + }; + } + + factory Customer.fromMap(Map map) { + return Customer( + id: map['id'], + firstName: map['firstName'], + lastName: map['lastName'], + phoneNumber: map['phoneNumber'], + ); + } +} diff --git a/lib/models/hen.dart b/lib/models/hen.dart new file mode 100644 index 0000000..030d931 --- /dev/null +++ b/lib/models/hen.dart @@ -0,0 +1,31 @@ +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 toMap() { + return { + 'id': id, + 'name': name, + 'birthDate': birthDate.toIso8601String(), + 'isLaying': isLaying, + }; + } + + factory Hen.fromMap(Map map) { + return Hen( + id: map['id'], + name: map['name'], + birthDate: DateTime.parse(map['birthDate']), + isLaying: map['isLaying'] ?? true, + ); + } +} diff --git a/lib/models/reservation.dart b/lib/models/reservation.dart new file mode 100644 index 0000000..1c82e9b --- /dev/null +++ b/lib/models/reservation.dart @@ -0,0 +1,43 @@ +class Reservation { + final String id; + final String customerName; + final String breedId; + final int eggsRequested; + final DateTime createdAt; + final DateTime deliveryDate; + bool isDelivered; + + Reservation({ + required this.id, + required this.customerName, + required this.breedId, + required this.eggsRequested, + required this.createdAt, + required this.deliveryDate, + this.isDelivered = false, + }); + + Map toMap() { + return { + 'id': id, + 'customerName': customerName, + 'breedId': breedId, + 'eggsRequested': eggsRequested, + 'createdAt': createdAt.toIso8601String(), + 'deliveryDate': deliveryDate.toIso8601String(), + 'isDelivered': isDelivered, + }; + } + + factory Reservation.fromMap(Map map) { + return Reservation( + id: map['id'], + customerName: map['customerName'], + breedId: map['breedId'], + eggsRequested: map['eggsRequested'], + createdAt: DateTime.parse(map['createdAt']), + deliveryDate: DateTime.parse(map['deliveryDate']), + isDelivered: map['isDelivered'] ?? false, + ); + } +} diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart new file mode 100644 index 0000000..02888b9 --- /dev/null +++ b/lib/services/storage_service.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/breed_setting.dart'; +import '../models/reservation.dart'; +import '../models/customer.dart'; + +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> loadWarehouseStock() async { + final prefs = await SharedPreferences.getInstance(); + final raw = prefs.getString(warehouseStockKey); + if (raw == null || raw.isEmpty) return {}; + final map = jsonDecode(raw) as Map; + return map.map((k, v) => MapEntry(k, (v as num).toDouble())); + } + + static Future saveWarehouseStock(Map stock) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(warehouseStockKey, jsonEncode(stock)); + } + + static Future 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 saveLastProductionUpdate(DateTime date) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(lastProductionUpdateKey, date.toIso8601String()); + } + + static Future> 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.from(e))).toList(); + } + + static Future saveBreedSettings(List items) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(breedsKey, jsonEncode(items.map((e) => e.toMap()).toList())); + } + + static Future> 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.from(e))).toList(); + } + + static Future saveReservations(List items) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(reservationsKey, jsonEncode(items.map((e) => e.toMap()).toList())); + } + + static Future> 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.from(e))).toList(); + } + + static Future saveCustomers(List items) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(customersKey, jsonEncode(items.map((e) => e.toMap()).toList())); + } +} diff --git a/lib/widgets/coop_icon.dart b/lib/widgets/coop_icon.dart new file mode 100644 index 0000000..2cb630a --- /dev/null +++ b/lib/widgets/coop_icon.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +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; +}