diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 66eafdd..c957a90 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -29,6 +29,7 @@ import 'package:flux/features/master_data/providers/ui/provider_form_screen.dart import 'package:flux/features/master_data/providers/ui/provider_list_screen.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/master_data/staff/ui/staff_screen.dart'; +import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/ui/stores_screen.dart'; import 'package:flux/features/notes/models/note_model.dart'; import 'package:flux/features/notes/ui/notes_form_screen.dart'; @@ -171,7 +172,11 @@ class AppRouter { path: 'stores', // Sistemata l'inversione path/name -> /master-data/stores name: Routes.stores, - builder: (context, state) => const StoresScreen(), + builder: (context, state) { + context.read().loadAllProviders(); + context.read().loadStores(); + return const StoresScreen(); + }, ), GoRoute( path: 'company-settings', // -> /master-data/company-settings diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index f2cbf24..2762456 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -21,6 +21,7 @@ class FluxTextField extends StatefulWidget { final TextCapitalization? textCapitalization; final bool? autocorrect; final bool? enabled; + final Iterable? autofillHints; const FluxTextField({ super.key, // Usiamo super.key per Flutter moderno @@ -41,6 +42,7 @@ class FluxTextField extends StatefulWidget { this.textCapitalization, this.autocorrect, this.enabled = true, + this.autofillHints, }); @override @@ -118,6 +120,7 @@ class _FluxTextFieldState extends State { textCapitalization: widget.textCapitalization ?? TextCapitalization.none, enabled: widget.enabled, + autofillHints: widget.autofillHints, ); } } diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 03f06fe..bec54af 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -16,7 +16,8 @@ class AuthCubit extends Cubit { emit(state.copyWith(isLoginMode: !state.isLoginMode)); } - Future submitAuth(String email, String password) async { + Future submitAuth(String email, String password) async { + // <-- Modificato in bool // Partiamo puliti: via vecchi messaggi ed errori emit(state.copyWith(status: AuthStatus.loading)); @@ -27,9 +28,17 @@ class AuthCubit extends Cubit { email: email, password: password, ); - // NESSUN EMIT DI SUCCESS! - // Supabase lancerà l'evento 'signedIn', il SessionCubit lo catturerà - // e il GoRouter ci cambierà pagina. Noi stiamo a guardare il caricamento. + + // Il login è andato a buon fine! + emit( + AuthState( + status: AuthStatus.initial, + isLoginMode: true, + errorMessage: null, + infoMessage: null, + ), + ); + return true; } else { // --- LOGICA SIGNUP --- final AuthResponse res = await _supabase.auth.signUp( @@ -38,7 +47,6 @@ class AuthCubit extends Cubit { ); if (res.session == null) { - // Caso: Conferma Email attivata su Supabase emit( state.copyWith( status: AuthStatus.initial, @@ -48,16 +56,24 @@ class AuthCubit extends Cubit { ), ); } else { - // Caso: Autologin post-registrazione (Conferma email disattivata) - // 1. Fermiamo il frullino! emit(state.copyWith(status: AuthStatus.initial)); - // 2. Svegliamo il SessionCubit! GetIt.I().initializeSession(); } - // Se non è null, ha fatto il login automatico. Stessa cosa di sopra, ci pensa il SessionCubit. + + // Anche la registrazione è andata a buon fine! + emit( + AuthState( + status: AuthStatus.initial, + isLoginMode: true, + errorMessage: null, + infoMessage: null, + ), + ); + return true; } } on AuthException catch (e) { emit(state.copyWith(status: AuthStatus.failure, errorMessage: e.message)); + return false; // <-- Il login è fallito } catch (e) { emit( state.copyWith( @@ -65,6 +81,7 @@ class AuthCubit extends Cubit { errorMessage: "Errore imprevisto: $e", ), ); + return false; // <-- Il login è fallito } } diff --git a/lib/features/auth/ui/auth_screen.dart b/lib/features/auth/ui/auth_screen.dart index 428b6d7..3d699c1 100644 --- a/lib/features/auth/ui/auth_screen.dart +++ b/lib/features/auth/ui/auth_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/utils/extensions.dart'; @@ -24,14 +25,18 @@ class _AuthScreenState extends State { super.dispose(); } - void _submit() { + void _submit() async { // Chiudiamo la tastiera per fare pulizia a schermo FocusScope.of(context).unfocus(); - context.read().submitAuth( + final isSuccess = await context.read().submitAuth( _emailController.text.trim(), _passwordController.text.trim(), ); + + if (isSuccess) { + TextInput.finishAutofillContext(); + } } @override @@ -69,125 +74,133 @@ class _AuthScreenState extends State { child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // --- LOGO FLUX --- - const FluxLogoAuto(height: 80), - const SizedBox(height: 60), + child: AutofillGroup( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // --- LOGO FLUX --- + const FluxLogoAuto(height: 80), + const SizedBox(height: 60), - // --- TITOLO DINAMICO --- - Text( - state.isLoginMode - ? context.l10n.authScreenWelcomeBack - : context.l10n.authScreenCreateAccount, - style: TextStyle( - color: context.primaryText, - fontSize: 24, - fontWeight: FontWeight.w900, - letterSpacing: 1.5, + // --- TITOLO DINAMICO --- + Text( + state.isLoginMode + ? context.l10n.authScreenWelcomeBack + : context.l10n.authScreenCreateAccount, + style: TextStyle( + color: context.primaryText, + fontSize: 24, + fontWeight: FontWeight.w900, + letterSpacing: 1.5, + ), ), - ), - const SizedBox(height: 8), - Text( - state.isLoginMode - ? context.l10n.authScreenLoginToManageYourBusiness - : context - .l10n - .authScreenStartTodayToDigitalizeYourStore, - textAlign: TextAlign.center, - style: TextStyle(color: context.secondaryText), - ), - const SizedBox(height: 40), + const SizedBox(height: 8), + Text( + state.isLoginMode + ? context.l10n.authScreenLoginToManageYourBusiness + : context + .l10n + .authScreenStartTodayToDigitalizeYourStore, + textAlign: TextAlign.center, + style: TextStyle(color: context.secondaryText), + ), + const SizedBox(height: 40), - // --- CAMPI INPUT --- - FluxTextField( - label: context.l10n.authScreenBusinessEmail, - icon: Icons.email_outlined, - controller: _emailController, - keyboardType: TextInputType.emailAddress, - ), - const SizedBox(height: 20), - FluxTextField( - label: 'Password', - icon: Icons.lock_outline, - isPassword: true, // Magia del FluxTextField! - controller: _passwordController, - onSubmitted: (_) => - _submit(), // Se lo supporti nel tuo widget custom - ), + // --- CAMPI INPUT --- + FluxTextField( + label: context.l10n.authScreenBusinessEmail, + icon: Icons.email_outlined, + controller: _emailController, + keyboardType: TextInputType.emailAddress, + autofillHints: const [ + AutofillHints.email, + AutofillHints.username, + ], + ), + const SizedBox(height: 20), + FluxTextField( + label: 'Password', + icon: Icons.lock_outline, + isPassword: true, // Magia del FluxTextField! + controller: _passwordController, + autofillHints: const [AutofillHints.password], + onSubmitted: (_) => + _submit(), // Se lo supporti nel tuo widget custom + ), - const SizedBox(height: 40), + const SizedBox(height: 40), - // --- BOTTONE PRINCIPALE --- - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: isLoading ? null : _submit, - child: isLoading - ? const SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, + // --- BOTTONE PRINCIPALE --- + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: isLoading ? null : _submit, + child: isLoading + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + state.isLoginMode + ? context.l10n.authScreenLogin + : context.l10n.authScreenSignUp, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), ), - ) - : Text( - state.isLoginMode - ? context.l10n.authScreenLogin - : context.l10n.authScreenSignUp, - style: const TextStyle( + ), + ), + + // --- SWITCH LOGIN/SIGNUP --- + const SizedBox(height: 24), + TextButton( + onPressed: isLoading + ? null + : () => context.read().toggleMode(), + child: RichText( + text: TextSpan( + text: state.isLoginMode + ? context.l10n.authScreenDontHaveAccount + : context.l10n.authScreenAlreadyHaveAccount, + style: TextStyle(color: context.secondaryText), + children: [ + TextSpan( + text: state.isLoginMode + ? context.l10n.authScreenSignUp + : context.l10n.authScreenLogin, + + style: TextStyle( + color: context.accent, fontWeight: FontWeight.bold, ), ), - ), - ), - - // --- SWITCH LOGIN/SIGNUP --- - const SizedBox(height: 24), - TextButton( - onPressed: isLoading - ? null - : () => context.read().toggleMode(), - child: RichText( - text: TextSpan( - text: state.isLoginMode - ? context.l10n.authScreenDontHaveAccount - : context.l10n.authScreenAlreadyHaveAccount, - style: TextStyle(color: context.secondaryText), - children: [ - TextSpan( - text: state.isLoginMode - ? context.l10n.authScreenSignUp - : context.l10n.authScreenLogin, - - style: TextStyle( - color: context.accent, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - if (state.isLoginMode) ...[ - const SizedBox(height: 24), - TextButton( - onPressed: () => context - .read() - .requestPasswordReset(_emailController.text.trim()), - child: Text( - context.l10n.authScreenForgotPassword, - style: TextStyle( - color: context.accent, - fontWeight: FontWeight.bold, + ], ), ), ), + if (state.isLoginMode) ...[ + const SizedBox(height: 24), + TextButton( + onPressed: () => + context.read().requestPasswordReset( + _emailController.text.trim(), + ), + child: Text( + context.l10n.authScreenForgotPassword, + style: TextStyle( + color: context.accent, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ], - ], + ), ), ), ), diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index ccf77ee..982db85 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; -import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; // Tuo percorso +import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:get_it/get_it.dart'; @@ -17,18 +17,16 @@ class StaffScreen extends StatefulWidget { class _StaffScreenState extends State { String? _selectedStoreId; - bool _showAllCompanyStaff = true; // Partiamo con la vista globale + bool _showAllCompanyStaff = true; @override void initState() { super.initState(); - // Carichiamo subito tutto context.read().loadAllStaff(); } @override Widget build(BuildContext context) { - // 1. Peschiamo chi siamo noi e che poteri abbiamo final myRole = context .read() .state @@ -36,12 +34,12 @@ class _StaffScreenState extends State { ?.systemRole; final canManageStaff = myRole == SystemRole.admin || myRole == SystemRole.manager; + return Scaffold( backgroundColor: context.background, appBar: AppBar( title: const Text("Anagrafica Personale"), actions: [ - // Toggle per vista Azienda / Negozio Padding( padding: const EdgeInsets.only(right: 16), child: FilterChip( @@ -66,7 +64,7 @@ class _StaffScreenState extends State { }, child: Column( children: [ - // --- BARRA FILTRO NEGOZIO (Visibile solo se non 'Tutta l'Azienda') --- + // BARRA FILTRO NEGOZIO AnimatedContainer( duration: const Duration(milliseconds: 300), height: _showAllCompanyStaff ? 0 : 80, @@ -75,7 +73,7 @@ class _StaffScreenState extends State { : _buildStoreSelector(), ), - // --- LISTA PERSONALE --- + // LISTA PERSONALE Expanded( child: BlocBuilder( builder: (context, state) { @@ -87,17 +85,14 @@ class _StaffScreenState extends State { return const Center(child: CircularProgressIndicator()); } - if (list.isEmpty) { - return _buildEmptyState(); - } + if (list.isEmpty) return _buildEmptyState(); return ListView.separated( padding: const EdgeInsets.all(16), itemCount: list.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (context, index) { - final member = list[index]; - return _buildStaffCard(member); + return _buildStaffCard(list[index]); }, ); }, @@ -118,7 +113,6 @@ class _StaffScreenState extends State { Widget _buildStoreSelector() { return BlocBuilder( - // Assumendo tu abbia uno StoreCubit builder: (context, state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -146,6 +140,8 @@ class _StaffScreenState extends State { ?.systemRole; final canManageStaff = myRole == SystemRole.admin || myRole == SystemRole.manager; + final hasEmail = member.email != null && member.email!.trim().isNotEmpty; + return Card( elevation: 0, shape: RoundedRectangleBorder( @@ -156,7 +152,10 @@ class _StaffScreenState extends State { contentPadding: const EdgeInsets.all(12), leading: CircleAvatar( backgroundColor: context.accent.withValues(alpha: 0.1), - child: Text(member.name[0], style: TextStyle(color: context.accent)), + child: Text( + member.name[0].toUpperCase(), + style: TextStyle(color: context.accent), + ), ), title: Text( member.name, @@ -165,55 +164,65 @@ class _StaffScreenState extends State { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (member.email != null && member.email!.isNotEmpty) - Text(member.email!), + if (hasEmail) Text(member.email!), Text( - member.phoneNumber != null && member.phoneNumber!.isNotEmpty + member.phoneNumber != null && + member.phoneNumber!.trim().isNotEmpty ? member.phoneNumber! : "Nessun telefono", ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (member.jobTitle != null && member.jobTitle!.isNotEmpty) ...[ - Text('Qualifica: ${member.jobTitle!}'), - const SizedBox(width: 8), - ], - - if (canManageStaff) ...[ - const SizedBox(width: 8), - - if (!member.hasJoined) - ElevatedButton.icon( - icon: const Icon(Icons.send), - label: const Text("Re-invia Invito (In Attesa)"), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange, - ), - onPressed: () { - // Chiama la funzione di reset password mascherata da invito - context.read().resetPasswordOrResendInviteLink( - member.email!, - ); - }, - ) - else - OutlinedButton.icon( - icon: const Icon(Icons.lock_reset), - label: const Text("Invia Reset Password"), - onPressed: () { - // Chiama LA STESSA IDENTICA FUNZIONE! - context.read().resetPasswordOrResendInviteLink( - member.email!, - ); - }, + if (member.jobTitle != null && + member.jobTitle!.trim().isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + 'Qualifica: ${member.jobTitle!}', + style: TextStyle( + color: context.accent, + fontWeight: FontWeight.w500, ), + ), ], ], ), - + // MODIFICA UX: Menu a tendina per le azioni (Salva spazio e previene overflow) + trailing: canManageStaff && hasEmail + ? PopupMenuButton( + icon: const Icon(Icons.more_vert), + onSelected: (value) { + if (value == 'invite_reset') { + context.read().resetPasswordOrResendInviteLink( + member.email!, + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Operazione richiesta, controlla l\'email!', + ), + ), + ); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'invite_reset', + child: Row( + children: [ + Icon( + !member.hasJoined ? Icons.send : Icons.lock_reset, + size: 20, + ), + const SizedBox(width: 12), + Text( + !member.hasJoined + ? "Re-invia Invito" + : "Reset Password", + ), + ], + ), + ), + ], + ) + : null, onTap: () => canManageStaff ? _openStaffForm(context, member: member) : null, ), @@ -226,7 +235,6 @@ class _StaffScreenState extends State { final phoneController = TextEditingController(text: member?.phoneNumber); final jobTitleController = TextEditingController(text: member?.jobTitle); - // Variabili di stato per il BottomSheet SystemRole selectedRole = member?.systemRole ?? SystemRole.user; List tempSelectedStores = context @@ -263,7 +271,7 @@ class _StaffScreenState extends State { children: [ Text( member == null - ? "Invita Collaboratore" // Cambiato il titolo per chiarezza! + ? "Invita Collaboratore" : "Modifica Collaboratore", style: const TextStyle( fontSize: 20, @@ -279,16 +287,13 @@ class _StaffScreenState extends State { ), const SizedBox(height: 16), - // Reso visivamente obbligatorio se è un nuovo utente FluxTextField( controller: emailController, label: member == null ? "Email (Obbligatoria per invito)*" : "Email", icon: Icons.email, - enabled: - member == - null, // UX: Di solito l'email non si cambia dopo l'invito + enabled: member == null, ), const SizedBox(height: 16), @@ -299,7 +304,6 @@ class _StaffScreenState extends State { ), const SizedBox(height: 16), - // --- NOVITÀ: SCELTA DEL RUOLO E MANSIONE --- Row( children: [ Expanded( @@ -317,9 +321,8 @@ class _StaffScreenState extends State { ); }).toList(), onChanged: (val) { - if (val != null) { + if (val != null) setModalState(() => selectedRole = val); - } }, ), ), @@ -382,7 +385,6 @@ class _StaffScreenState extends State { height: 50, child: ElevatedButton( onPressed: () { - // Validazione di base per i nuovi inviti if (member == null && emailController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( @@ -396,7 +398,7 @@ class _StaffScreenState extends State { } final updatedMember = StaffMemberModel( - id: member?.id, // Sarà null se è nuovo + id: member?.id, name: nameController.text.trim(), email: emailController.text.trim(), phoneNumber: phoneController.text.trim(), @@ -410,17 +412,12 @@ class _StaffScreenState extends State { userId: GetIt.I.get().state.user!.id, ); - // --- IL BIVIO LOGICO MAGICO --- if (member == null) { - // 1. UTENTE NUOVO -> Chiamiamo la Edge Function - // (Nota: Per i negozi, potresti dover fare una logica a parte nel Cubit - // perché l'ID del database viene generato DOPO che l'Edge Function ha finito) context.read().inviteStaffMember( member: updatedMember, selectedStoreIds: tempSelectedStores, ); } else { - // 2. UTENTE ESISTENTE -> Modifica classica context.read().saveStaffWithStores( member: updatedMember, selectedStoreIds: tempSelectedStores, @@ -434,32 +431,6 @@ class _StaffScreenState extends State { ), ), ), - /* const SizedBox(height: 16), - if (!member!.hasJoined) - ElevatedButton.icon( - icon: const Icon(Icons.send), - label: const Text("Re-invia Invito (In Attesa)"), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange, - ), - onPressed: () { - // Chiama la funzione di reset password mascherata da invito - context - .read() - .resetPasswordOrResendInviteLink(member.email!); - }, - ) - else - OutlinedButton.icon( - icon: const Icon(Icons.lock_reset), - label: const Text("Invia Reset Password"), - onPressed: () { - // Chiama LA STESSA IDENTICA FUNZIONE! - context - .read() - .resetPasswordOrResendInviteLink(member.email!); - }, - ), */ ], ), ), diff --git a/lib/features/master_data/store/ui/store_card.dart b/lib/features/master_data/store/ui/store_card.dart index 1abf210..57613f2 100644 --- a/lib/features/master_data/store/ui/store_card.dart +++ b/lib/features/master_data/store/ui/store_card.dart @@ -54,6 +54,8 @@ class _StoreCardState extends State { ), title: Text( widget.store.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( @@ -65,44 +67,43 @@ class _StoreCardState extends State { // context.read().add(ToggleStoreStatus(store.id, val)); }, ), + onTap: () => _openStoreForm(context, store: widget.store), ), const Divider(height: 1), Padding( - padding: const EdgeInsets.all(12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // Mostra quanti dipendenti ci sono (usando lo StaffCubit) - BlocBuilder( - builder: (context, storeState) { - final staffCount = - storeState.staffByStore[widget.store.id]?.length ?? 0; - return Row( - children: [ - ActionChip( - avatar: const Icon(Icons.people, size: 16), - label: Text("$staffCount Dipendenti"), - onPressed: () => _manageStoreStaff(widget.store), - ), - const SizedBox(width: 16), - ActionChip( - avatar: const Icon(Icons.handshake, size: 16), - label: Text( - "${widget.store.associatedProviders.length} Providers", + padding: const EdgeInsets.all(8), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Mostra quanti dipendenti ci sono (usando lo StaffCubit) + BlocBuilder( + builder: (context, storeState) { + final staffCount = + storeState.staffByStore[widget.store.id]?.length ?? 0; + return Row( + children: [ + ActionChip( + avatar: const Icon(Icons.people, size: 16), + label: Text("$staffCount Dipendenti"), + onPressed: () => _manageStoreStaff(widget.store), ), - onPressed: () => _manageStoreProviders(widget.store), - ), - ], - ); - }, - ), - const SizedBox(width: 16), - TextButton.icon( - onPressed: () => _openStoreForm(context, store: widget.store), - icon: const Icon(Icons.edit, size: 18), - label: const Text("Modifica"), - ), - ], + const SizedBox(width: 16), + ActionChip( + avatar: const Icon(Icons.handshake, size: 16), + label: Text( + "${widget.store.associatedProviders.length} Providers", + ), + onPressed: () => + _manageStoreProviders(widget.store), + ), + ], + ); + }, + ), + ], + ), ), ), ], diff --git a/lib/features/notes/ui/notes_form_screen.dart b/lib/features/notes/ui/notes_form_screen.dart index afa0810..21746bd 100644 --- a/lib/features/notes/ui/notes_form_screen.dart +++ b/lib/features/notes/ui/notes_form_screen.dart @@ -248,89 +248,121 @@ class _NoteFormScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // --- HEADER DEL POST-IT (Tavolozza + Azioni) --- - Row( - children: [ - // Tavolozza Colori - Expanded( - child: SizedBox( - height: 40, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: _noteColors.length, - itemBuilder: (context, index) { - final colorHex = _noteColors[index]; - final isSelected = _selectedColor == colorHex; - final c = Color( - int.parse( - 'FF${colorHex.replaceAll('#', '')}', - radix: 16, - ), - ); + LayoutBuilder( + builder: (context, constraints) { + // 1. Capiamo quanto spazio reale ha la finestra in questo momento + final isNarrow = constraints.maxWidth < 500; - return GestureDetector( - onTap: () { - setState(() => _selectedColor = colorHex); - _triggerAutoSave(); - }, - child: Container( - margin: const EdgeInsets.only(right: 12), - width: 40, - decoration: BoxDecoration( - color: c, - shape: BoxShape.circle, - border: Border.all( - color: isSelected - ? Colors.black54 - : Colors.black12, - width: isSelected ? 3 : 1, - ), + // 2. Adattiamo la dimensione dei cerchi + final double circleSize = isNarrow ? 32.0 : 40.0; + + // -- PREPARIAMO IL BLOCCO COLORI -- + final colorPalette = SizedBox( + height: circleSize, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _noteColors.length, + itemBuilder: (context, index) { + final colorHex = _noteColors[index]; + final isSelected = _selectedColor == colorHex; + final c = Color( + int.parse( + 'FF${colorHex.replaceAll('#', '')}', + radix: 16, + ), + ); + + return GestureDetector( + onTap: () { + setState(() => _selectedColor = colorHex); + _triggerAutoSave(); + }, + child: Container( + margin: const EdgeInsets.only(right: 12), + width: circleSize, + decoration: BoxDecoration( + color: c, + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? Colors.black54 + : Colors.black12, + width: isSelected ? 3 : 1, ), - child: isSelected - ? const Icon( - Icons.check, - color: Colors.black54, - size: 20, - ) - : null, ), - ); + child: isSelected + ? Icon( + Icons.check, + color: Colors.black54, + size: isNarrow ? 16 : 20, + ) + : null, + ), + ); + }, + ), + ); + + // -- PREPARIAMO IL BLOCCO AZIONI -- + final actionButtons = Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.delete_outline, + color: Colors.black87, + ), + tooltip: 'Elimina', + onPressed: _deleteNote, + ), + IconButton( + icon: Icon( + _isPinned + ? Icons.push_pin + : Icons.push_pin_outlined, + color: Colors.black87, + ), + tooltip: _isPinned + ? 'Rimuovi in alto' + : 'Fissa in alto', + onPressed: () { + setState(() => _isPinned = !_isPinned); + _triggerAutoSave(); }, ), - ), - ), - const SizedBox(width: 16), + IconButton( + icon: const Icon( + Icons.ios_share, + color: Colors.black87, + ), + tooltip: 'Esporta', + onPressed: _exportNote, + ), + ], + ); - // Azioni spostate dentro la nota! - IconButton( - icon: const Icon( - Icons.delete_outline, - color: Colors.black87, - ), - tooltip: 'Elimina', - onPressed: _deleteNote, - ), - IconButton( - icon: Icon( - _isPinned ? Icons.push_pin : Icons.push_pin_outlined, - color: Colors.black87, - ), - tooltip: _isPinned - ? 'Rimuovi in alto' - : 'Fissa in alto', - onPressed: () { - setState(() => _isPinned = !_isPinned); - _triggerAutoSave(); - }, - ), - IconButton( - icon: const Icon( - Icons.ios_share, - color: Colors.black87, - ), - tooltip: 'Esporta', - onPressed: _exportNote, - ), - ], + // 3. DECIDIAMO IL LAYOUT FINALE IN BASE ALLO SPAZIO REALE + if (isNarrow) { + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + actionButtons, + + const SizedBox(height: 8), + colorPalette, + ], + ); + } + + // Layout "Largo" (Finestra intera) + return Row( + children: [ + Expanded(child: colorPalette), + const SizedBox(width: 16), + actionButtons, + ], + ); + }, ), const SizedBox(height: 32), diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 90a7a8c..f979614 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -483,36 +483,39 @@ class _OperationFormScreenState extends State { icon: Icons.design_services, themeColor: Colors.deepOrange, children: [ - Row( - children: [ - ChoiceChip( - label: const Text('Privato (Domestico)'), - selected: !state.operation.isBusiness, - selectedColor: Colors.blue.withValues(alpha: 0.2), - checkmarkColor: Colors.blue.shade700, - onSelected: (selected) { - if (selected) { - context.read().updateFields( - isBusiness: false, - ); - } - }, - ), - const SizedBox(width: 12), - ChoiceChip( - label: const Text('Business (P.IVA)'), - selected: state.operation.isBusiness, - selectedColor: Colors.orange.withValues(alpha: 0.2), - checkmarkColor: Colors.orange.shade700, - onSelected: (selected) { - if (selected) { - context.read().updateFields( - isBusiness: true, - ); - } - }, - ), - ], + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ChoiceChip( + label: const Text('Privato (Domestico)'), + selected: !state.operation.isBusiness, + selectedColor: Colors.blue.withValues(alpha: 0.2), + checkmarkColor: Colors.blue.shade700, + onSelected: (selected) { + if (selected) { + context.read().updateFields( + isBusiness: false, + ); + } + }, + ), + const SizedBox(width: 12), + ChoiceChip( + label: const Text('Business (P.IVA)'), + selected: state.operation.isBusiness, + selectedColor: Colors.orange.withValues(alpha: 0.2), + checkmarkColor: Colors.orange.shade700, + onSelected: (selected) { + if (selected) { + context.read().updateFields( + isBusiness: true, + ); + } + }, + ), + ], + ), ), const Divider(height: 32), Wrap( diff --git a/lib/features/settings/document_sequence/ui/document_sequence_section.dart b/lib/features/settings/document_sequence/ui/document_sequence_section.dart index 9198556..da1ac52 100644 --- a/lib/features/settings/document_sequence/ui/document_sequence_section.dart +++ b/lib/features/settings/document_sequence/ui/document_sequence_section.dart @@ -8,151 +8,163 @@ class DocumentSequenceSection extends StatelessWidget { @override Widget build(BuildContext context) { - final year = DateTime.now().year; - return BlocBuilder( builder: (context, state) { if (state.status == DocumentSequenceStatus.loading) { return const Center(child: CircularProgressIndicator()); } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Text( - "Protocolli e Numerazione", - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - ), - ), - - // Invece di mappare state.sequences, mappiamo i documenti supportati - ...DocumentType.values.map((docType) { - // Cerchiamo se c'è già una configurazione nello stato per questo documento - final existingList = state.sequences - .where((s) => s.docType == docType.name) - .toList(); - final existingSeq = existingList.isNotEmpty - ? existingList.first - : null; - - // Se esiste usiamo i suoi valori, altrimenti i default - final prefix = existingSeq?.prefix ?? docType.defaultPrefix; - final nextValue = existingSeq?.nextValue ?? 1; - - // Anteprima dinamica (aggiornata a 4 zeri come nel DB!) - final preview = - "${prefix.isNotEmpty ? '$prefix-' : ''}$year-${nextValue.toString().padLeft(4, '0')}"; - - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - docType.label, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.blue, - ), - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - flex: 2, - child: TextFormField( - initialValue: prefix, - decoration: const InputDecoration( - labelText: 'Prefisso', - hintText: 'es. TCK', - ), - onChanged: (val) => context - .read() - .updateLocalSequence( - docType.name, - prefix: val, - ), - ), - ), - const SizedBox(width: 16), - Expanded( - flex: 3, - child: TextFormField( - initialValue: nextValue.toString(), - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Prossimo Numero', - ), - onChanged: (val) => context - .read() - .updateLocalSequence( - docType.name, - nextValue: int.tryParse(val) ?? 1, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors - .grey - .shade100, // Se hai un tema scuro potresti voler usare Theme.of(context).colorScheme.surfaceContainer - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - const Icon( - Icons.visibility, - size: 16, - color: Colors.grey, - ), - const SizedBox(width: 8), - Text( - "Anteprima prossimo: ", - style: TextStyle( - color: Colors - .grey - .shade700, // Idem per la dark mode - fontSize: 12, - ), - ), - Text( - preview, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontFamily: 'monospace', - ), - ), - ], - ), - ), - ], - ), - ), - ); - }), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () => - context.read().saveSequences(), - icon: const Icon(Icons.save), - label: const Text("SALVA PROTOCOLLI"), - ), - ), - ], + return LayoutBuilder( + builder: ((context, constraints) { + final isLargeScreen = constraints.maxWidth >= 600; + return _buildMainContent( + state: state, + isLargeScreen: isLargeScreen, + context: context, + ); + }), ); }, ); } + + Widget _buildMainContent({ + required BuildContext context, + required DocumentSequenceState state, + required bool isLargeScreen, + }) { + final year = DateTime.now().year; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + "Protocolli e Numerazione", + + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ), + + // Invece di mappare state.sequences, mappiamo i documenti supportati + ...DocumentType.values.map((docType) { + // Cerchiamo se c'è già una configurazione nello stato per questo documento + final existingList = state.sequences + .where((s) => s.docType == docType.name) + .toList(); + final existingSeq = existingList.isNotEmpty + ? existingList.first + : null; + + // Se esiste usiamo i suoi valori, altrimenti i default + final prefix = existingSeq?.prefix ?? docType.defaultPrefix; + final nextValue = existingSeq?.nextValue ?? 1; + + // Anteprima dinamica (aggiornata a 4 zeri come nel DB!) + final preview = + "${prefix.isNotEmpty ? '$prefix-' : ''}$year-${nextValue.toString().padLeft(4, '0')}"; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + docType.label, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + flex: 2, + child: TextFormField( + initialValue: prefix, + decoration: const InputDecoration( + labelText: 'Prefisso', + hintText: 'es. TCK', + ), + onChanged: (val) => context + .read() + .updateLocalSequence(docType.name, prefix: val), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 3, + child: TextFormField( + initialValue: nextValue.toString(), + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Prossimo Numero', + ), + onChanged: (val) => context + .read() + .updateLocalSequence( + docType.name, + nextValue: int.tryParse(val) ?? 1, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon( + Icons.visibility, + size: 16, + color: Colors.grey, + ), + const SizedBox(width: 8), + Text( + "Anteprima prossimo: ", + style: TextStyle( + color: + Colors.grey.shade700, // Idem per la dark mode + fontSize: 12, + ), + ), + Flexible( + child: Text( + preview, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'monospace', + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => + context.read().saveSequences(), + icon: const Icon(Icons.save), + label: const Text("SALVA PROTOCOLLI"), + ), + ), + ], + ); + } } diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index 505a8ea..bbde1c6 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -54,9 +54,11 @@ class SettingsScreen extends StatelessWidget { children: [ const Icon(Icons.person, color: FluxColors.primaryBlue), const SizedBox(width: 12), - Text( - 'Modalità utente singolo (dispositivo personale)', - style: Theme.of(context).textTheme.titleLarge, + Flexible( + child: Text( + 'Modalità utente singolo (dispositivo personale)', + style: Theme.of(context).textTheme.titleLarge, + ), ), ], ), diff --git a/lib/features/tickets/ui/ticket_form_screen.dart b/lib/features/tickets/ui/ticket_form_screen.dart index 9e4a131..d3ac948 100644 --- a/lib/features/tickets/ui/ticket_form_screen.dart +++ b/lib/features/tickets/ui/ticket_form_screen.dart @@ -784,6 +784,7 @@ class _TicketFormScreenState extends State { children: [ Expanded( child: DropdownButtonFormField( + isExpanded: true, initialValue: ticket.ticketType, decoration: const InputDecoration( labelText: 'Tipo Lavorazione', @@ -804,6 +805,7 @@ class _TicketFormScreenState extends State { const SizedBox(width: 16), Expanded( child: DropdownButtonFormField( + isExpanded: true, initialValue: ticket.ticketStatus, decoration: const InputDecoration(labelText: 'Stato Attuale'), items: TicketStatus.values @@ -1001,11 +1003,15 @@ class _TicketFormScreenState extends State { child: Icon(icon, color: themeColor), ), const SizedBox(width: 12), - Text( - title, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - color: themeColor, + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: themeColor, + ), ), ), ], diff --git a/lib/features/tickets/ui/widgets/ticket_list.dart b/lib/features/tickets/ui/widgets/ticket_list.dart index abc5504..9593ea3 100644 --- a/lib/features/tickets/ui/widgets/ticket_list.dart +++ b/lib/features/tickets/ui/widgets/ticket_list.dart @@ -133,29 +133,44 @@ class TicketList extends StatelessWidget { horizontal: 16.0, vertical: 8.0, ), - child: Row( + // ECCO LA MAGIA: Wrap invece di Row! + child: Wrap( + alignment: WrapAlignment.spaceBetween, // Sostituisce lo Spacer! + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8.0, // Spazio orizzontale + runSpacing: 8.0, // Spazio verticale se va a capo children: [ - IconButton( - icon: const Icon(Icons.close), - onPressed: () => - context.read().clearSelection(), - ), - Text( - '${state.selectedTickets.length} selezionati', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - const Spacer(), + // BLOCCO 1: Icona e Contatore Row( + mainAxisSize: MainAxisSize + .min, // Fondamentale per non occupare tutto il Wrap + children: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () => + context.read().clearSelection(), + ), + Text( + '${state.selectedTickets.length} selezionati', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + + // BLOCCO 2: I Bottoni (Un altro Wrap per farli andare a capo tra loro se serve!) + Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.end, children: [ FilledButton.icon( onPressed: () => _setStatusClosed(context), icon: const Icon(Icons.approval), label: const Text('Riconsegna'), ), - const SizedBox(width: 8), FilledButton.icon( onPressed: () => _showShippingModal(context), icon: const Icon(Icons.local_shipping), diff --git a/lib/main.dart b/lib/main.dart index 50581ed..aa23b45 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -306,12 +306,14 @@ class _GlobalUpdateCheckerState extends State { size: 32, ), const SizedBox(width: 12), - Text( - kIsWeb - ? "Aggiornamento" - : "Aggiornamento Obbligatorio", - style: Theme.of(context).textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold), + Flexible( + child: Text( + kIsWeb + ? "Aggiornamento" + : "Aggiornamento Obbligatorio", + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), ), ], ), @@ -351,12 +353,10 @@ class _GlobalUpdateCheckerState extends State { onPressed: () async { if (_updateUrl != null) { final url = Uri.parse(_updateUrl!); - if (await canLaunchUrl(url)) { - await launchUrl( - url, - mode: LaunchMode.externalApplication, - ); - } + await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); } }, ),