diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 9673721..d89fd99 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -177,8 +177,17 @@ class AppRouter { ); context.read().loadCustomers(); - return BlocProvider( - create: (context) => TicketFormCubit(), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => AttachmentsBloc( + parentType: AttachmentParentType.ticket, + parentId: realTicketId, + ), + ), + BlocProvider(create: (context) => TicketFormCubit()), + ], + child: TicketFormScreen( ticketId: realTicketId, existingTicket: ticketFromExtra, diff --git a/lib/core/widgets/shared_forms/shared_files_section.dart b/lib/core/widgets/shared_forms/shared_files_section.dart new file mode 100644 index 0000000..65d6bec --- /dev/null +++ b/lib/core/widgets/shared_forms/shared_files_section.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart'; +// Adatta gli import alle tue cartelle reali! +import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; + +class SharedFilesSection extends StatelessWidget { + final String + titleNameForUpload; // Es. il nome del cliente o il modello da passare alla pagina di upload + + const SharedFilesSection({super.key, required this.titleNameForUpload}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Allegati e Foto', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextButton.icon( + icon: const Icon(Icons.add_a_photo), + label: const Text('Aggiungi'), + onPressed: () { + // Navighiamo verso la nostra fiammante pagina di upload agnostica! + // Assicurati che l'AttachmentsBloc sopravviva al cambio pagina usando BlocProvider.value + final bloc = context.read(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BlocProvider.value( + value: bloc, + child: SharedMobileUploadScreen( + title: titleNameForUpload, + ), + ), + ), + ); + }, + ), + ], + ), + const SizedBox(height: 8), + + // LA VETRINA DEI FILE + BlocBuilder( + builder: (context, state) { + final files = + state.allFiles; // Unisce sia i remoti che i locali (bozze) + + if (state.status == AttachmentsStatus.loading) { + return const Center(child: CircularProgressIndicator()); + } + + if (files.isEmpty) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border.all( + color: theme.dividerColor, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(12), + ), + child: const Column( + children: [ + Icon( + Icons.image_not_supported_outlined, + color: Colors.grey, + size: 32, + ), + SizedBox(height: 8), + Text( + 'Nessun file allegato', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ); + } + + return Wrap( + spacing: 12, + runSpacing: 12, + children: files.map((file) { + final isImage = [ + 'jpg', + 'jpeg', + 'png', + 'webp', + ].contains(file.extension.toLowerCase()); + + return Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: theme.dividerColor), + ), + child: Stack( + children: [ + // Sfondo File / Anteprima + Center( + child: isImage + ? const Icon( + Icons.image, + color: Colors.blue, + size: 40, + ) // Qui in futuro metteremo Image.network da Supabase + : const Icon( + Icons.picture_as_pdf, + color: Colors.red, + size: 40, + ), + ), + // Indicatore "Bozza" per i file non ancora caricati + if (file.id == null) + Positioned( + bottom: 4, + left: 4, + right: 4, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 2), + decoration: BoxDecoration( + color: Colors.orange, + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'Da salvare', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 8, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + // Pulsante Elimina + Positioned( + top: -8, + right: -8, + child: IconButton( + icon: const Icon( + Icons.cancel, + color: Colors.redAccent, + size: 20, + ), + onPressed: () { + // Manda l'evento di eliminazione + context.read().add( + DeleteSpecificAttachmentEvent(file), + ); + }, + ), + ), + ], + ), + ); + }).toList(), + ); + }, + ), + ], + ); + } +} diff --git a/lib/features/tickets/ui/ticket_form_screen.dart b/lib/features/tickets/ui/ticket_form_screen.dart index 6eea21a..a3a2e91 100644 --- a/lib/features/tickets/ui/ticket_form_screen.dart +++ b/lib/features/tickets/ui/ticket_form_screen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/widgets/shared_forms/customer_section.dart'; +import 'package:flux/core/widgets/shared_forms/model_section.dart'; +import 'package:flux/core/widgets/shared_forms/shared_files_section.dart'; import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart'; import 'package:flux/features/tickets/blocs/ticket_form_state.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; -import 'package:flux/core/widgets/shared_forms/customer_section.dart'; -import 'package:flux/core/widgets/shared_forms/model_section.dart'; import 'package:flux/core/widgets/shared_forms/staff_section.dart'; -import 'package:flux/features/tickets/models/ticket_status_extension.dart'; // Il tuo widget agnostico dello staff +import 'package:flux/features/tickets/models/ticket_status_extension.dart'; class TicketFormScreen extends StatefulWidget { final TicketModel? existingTicket; @@ -21,7 +22,6 @@ class TicketFormScreen extends StatefulWidget { class _TicketFormScreenState extends State { final _formKey = GlobalKey(); - // Controllers testuali final _altPhoneCtrl = TextEditingController(); final _serialCtrl = TextEditingController(); final _requestCtrl = TextEditingController(); @@ -36,10 +36,9 @@ class _TicketFormScreenState extends State { @override void initState() { super.initState(); - // Inizializziamo il Cubit context.read().initForm( - id: widget.ticketId, existingTicket: widget.existingTicket, + id: widget.ticketId, ); } @@ -56,7 +55,6 @@ class _TicketFormScreenState extends State { super.dispose(); } - // Sincronizza i controller con lo stato iniziale senza sovrascrivere se l'utente sta digitando void _syncTextControllers(TicketModel model) { if (_altPhoneCtrl.text.isEmpty) { _altPhoneCtrl.text = model.alternativePhoneNumber ?? ''; @@ -81,7 +79,6 @@ class _TicketFormScreenState extends State { _isInitialized = true; } - // Chiamato prima del salvataggio per pushare i testi nei campi del Cubit void _flushControllersToCubit() { context.read().updateFields( alternativePhoneNumber: _altPhoneCtrl.text, @@ -121,7 +118,6 @@ class _TicketFormScreenState extends State { content: Text('Scheda salvata! Inserisci la prossima.'), ), ); - // Svuotiamo i controller per il nuovo ticket _altPhoneCtrl.clear(); _serialCtrl.clear(); _requestCtrl.clear(); @@ -149,7 +145,6 @@ class _TicketFormScreenState extends State { ticket.id == null ? 'Nuova Scheda Assistenza' : 'Modifica Scheda', ), actions: [ - // PICCOLO BADGE DI STATO IN ALTO A DESTRA Padding( padding: const EdgeInsets.only(right: 16.0), child: Chip( @@ -164,284 +159,86 @@ class _TicketFormScreenState extends State { ), body: Form( key: _formKey, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Center( - child: ConstrainedBox( - // Limitiamo la larghezza su schermi grandi per non renderlo illeggibile - constraints: const BoxConstraints(maxWidth: 800), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // --- CARD 1: INTESTAZIONE & CLIENTE --- - _buildCard( - title: 'Anagrafica', - icon: Icons.person, - children: [ - // Qui usiamo la StaffSection per scegliere "A nome di chi" apriamo la scheda - StaffSection( - label: 'Creato Da', - staffId: ticket.createdById, - staffName: ticket.createdByName, - onStaffSelected: (staff) => - context.read().updateCreator( - staffId: staff.id!, - staffName: staff.name, - ), - ), - const Divider(height: 32), - SharedCustomerSection( - customerId: ticket.customerId, - customerName: ticket.customerName, - onCustomerSelected: (customer) => context - .read() - .updateCustomer(customer), - ), - const SizedBox(height: 16), - TextFormField( - controller: _altPhoneCtrl, - decoration: const InputDecoration( - labelText: - 'Recapito Alternativo (es. se lascia il telefono principale)', - prefixIcon: Icon(Icons.phone), - ), - ), - ], - ), + // IL TRUCCO PER LA TASTIERA: Obblighiamo il tab a seguire il DOM + child: FocusTraversalGroup( + policy: WidgetOrderTraversalPolicy(), + child: LayoutBuilder( + builder: (context, constraints) { + final isUltraWide = constraints.maxWidth > 1400; + final isDesktop = constraints.maxWidth > 900; - // --- CARD 2: DISPOSITIVO --- - _buildCard( - title: 'Dispositivo', - icon: Icons.devices, - children: [ - SharedModelSection( - label: 'Modello da Riparare', - modelId: ticket.targetModelId, - modelName: ticket.targetModelName, - onModelSelected: (id, name) => context - .read() - .updateModel(modelId: id, modelName: name), - ), - const SizedBox(height: 16), - TextFormField( - controller: _serialCtrl, - decoration: const InputDecoration( - labelText: 'Seriale / IMEI', - prefixIcon: Icon(Icons.qr_code), - ), - ), - ], + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: isUltraWide + ? 1600 + : (isDesktop ? 1200 : 800), + ), + child: _buildResponsiveLayout( + isUltraWide, + isDesktop, + ticket, + ), ), - - // --- CARD 3: PROBLEMA E LAVORAZIONE --- - _buildCard( - title: 'Dettagli Riparazione', - icon: Icons.build, - children: [ - // Tipo Lavorazione e Tipo Garanzia - Row( - children: [ - Expanded( - child: DropdownButtonFormField( - initialValue: ticket.ticketType, - decoration: const InputDecoration( - labelText: 'Tipo Lavorazione', - ), - items: TicketType.values - .map( - (t) => DropdownMenuItem( - value: t, - child: Text( - t.name, - ), // Se hai estensioni, usa t.displayName - ), - ) - .toList(), - onChanged: (val) => context - .read() - .updateFields(ticketType: val), - ), - ), - const SizedBox(width: 16), - Expanded( - child: DropdownButtonFormField( - initialValue: ticket.ticketStatus, - decoration: const InputDecoration( - labelText: 'Stato Attuale', - ), - items: TicketStatus.values - .map( - (s) => DropdownMenuItem( - value: s, - child: Text(s.name), // Idem qui - ), - ) - .toList(), - onChanged: (val) => context - .read() - .updateFields(status: val), - ), - ), - ], - ), - const SizedBox(height: 16), - TextFormField( - controller: _requestCtrl, - maxLines: 4, - decoration: const InputDecoration( - labelText: - 'Difetto dichiarato o Richiesta del cliente', - alignLabelWithHint: true, - ), - ), - const SizedBox(height: 16), - TextFormField( - controller: _accessoriesCtrl, - decoration: const InputDecoration( - labelText: - 'Accessori Consegnati (es. cover, caricatore...)', - prefixIcon: Icon(Icons.cable), - ), - ), - const SizedBox(height: 16), - SwitchListTile( - title: const Text('Prestato Telefono di Cortesia?'), - value: ticket.hasCourtesyDevice, - onChanged: (val) => context - .read() - .updateFields(hasCourtesyDevice: val), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide(color: theme.dividerColor), - ), - ), - ], - ), - - // --- CARD 4: COSTI E NOTE --- - _buildCard( - title: 'Costi & Note', - icon: Icons.euro, - children: [ - Row( - children: [ - Expanded( - child: TextFormField( - controller: _priceCtrl, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true, - ), - decoration: const InputDecoration( - labelText: 'Preventivo Cliente (€)', - prefixIcon: Icon(Icons.sell_outlined), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: TextFormField( - controller: _costCtrl, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true, - ), - decoration: const InputDecoration( - labelText: 'Nostro Costo (€)', - prefixIcon: Icon( - Icons.shopping_cart_outlined, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 16), - TextFormField( - controller: _publicNotesCtrl, - maxLines: 2, - decoration: const InputDecoration( - labelText: - 'Note Pubbliche (Visibili su ricevuta)', - ), - ), - const SizedBox(height: 16), - TextFormField( - controller: _internalNotesCtrl, - maxLines: 3, - decoration: InputDecoration( - labelText: 'Note Interne (Solo per lo Staff)', - fillColor: Colors.amber.withValues(alpha: 0.1), - filled: true, - ), - ), - ], - ), - - // --- CARD 5: ASSEGNAZIONE & FILE --- - _buildCard( - title: 'Assegnazione Tecnico', - icon: Icons.engineering, - children: [ - StaffSection( - label: 'Assegnato A', - staffId: ticket.assignedToId, - staffName: ticket.assignedToName, - onStaffSelected: (staff) => - context.read().updateFields( - assignedToId: staff.id, - assignedToName: staff.name, - ), - ), - // TODO: Inserire qui il tuo SharedAttachmentsSection per foto pre-riparazione - ], - ), - - const SizedBox(height: 80), // Spazio per il bottom nav - ], - ), - ), + ), + ); + }, ), ), ), bottomNavigationBar: SafeArea( - child: Padding( + child: Container( padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Expanded( - flex: 1, - child: OutlinedButton( - onPressed: state.status == TicketFormStatus.saving - ? null - : () => _saveTicket(keepAdding: true), - child: const Text( - 'Salva e Aggiungi Altro', - textAlign: TextAlign.center, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - flex: 1, - child: ElevatedButton( - onPressed: state.status == TicketFormStatus.saving - ? null - : () => _saveTicket(keepAdding: false), - child: state.status == TicketFormStatus.saving - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Text('Salva ed Esci'), - ), + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, -3), ), ], ), + child: FocusTraversalGroup( + // Un gruppo a parte per il footer, così viene visitato per ultimo + child: Row( + children: [ + Expanded( + flex: 1, + child: OutlinedButton( + onPressed: state.status == TicketFormStatus.saving + ? null + : () => _saveTicket(keepAdding: true), + child: const Text( + 'Salva e Aggiungi Altro', + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 1, + child: ElevatedButton( + onPressed: state.status == TicketFormStatus.saving + ? null + : () => _saveTicket(keepAdding: false), + child: state.status == TicketFormStatus.saving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text('Salva ed Esci'), + ), + ), + ], + ), + ), ), ), ); @@ -449,16 +246,296 @@ class _TicketFormScreenState extends State { ); } - // Helper per creare le Card esteticamente coerenti + // --- LOGICA DI IMPAGINAZIONE RESPONSIVE --- + Widget _buildResponsiveLayout( + bool isUltraWide, + bool isDesktop, + TicketModel ticket, + ) { + if (isUltraWide) { + // 3 COLONNE + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [_cardAnagrafica(ticket), _cardDispositivo(ticket)], + ), + ), + const SizedBox(width: 24), + Expanded(child: Column(children: [_cardDettagli(ticket)])), + const SizedBox(width: 24), + Expanded( + child: Column( + children: [_cardCosti(ticket), _cardAssegnazione(ticket)], + ), + ), + ], + ); + } else if (isDesktop) { + // 2 COLONNE + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + _cardAnagrafica(ticket), + _cardDispositivo(ticket), + _cardAssegnazione(ticket), + ], + ), + ), + const SizedBox(width: 24), + Expanded( + child: Column( + children: [_cardDettagli(ticket), _cardCosti(ticket)], + ), + ), + ], + ); + } else { + // 1 COLONNA (Mobile) + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _cardAnagrafica(ticket), + _cardDispositivo(ticket), + _cardDettagli(ticket), + _cardCosti(ticket), + _cardAssegnazione(ticket), + ], + ); + } + } + + // --- LE 5 CARD (MODULARIZZATE E COLORATE) --- + + Widget _cardAnagrafica(TicketModel ticket) { + return _buildCard( + title: 'Anagrafica', + icon: Icons.person, + themeColor: Colors.indigo, + children: [ + StaffSection( + label: 'Creato Da', + staffId: ticket.createdById, + staffName: ticket.createdByName, + onStaffSelected: (staff) => context + .read() + .updateCreator(staffId: staff.id!, staffName: staff.name), + ), + const Divider(height: 32), + SharedCustomerSection( + customerId: ticket.customerId, + customerName: ticket.customerName, + onCustomerSelected: (customer) => + context.read().updateCustomer(customer), + ), + const SizedBox(height: 16), + TextFormField( + controller: _altPhoneCtrl, + decoration: const InputDecoration( + labelText: 'Recapito Alternativo', + prefixIcon: Icon(Icons.phone), + ), + ), + ], + ); + } + + Widget _cardDispositivo(TicketModel ticket) { + return _buildCard( + title: 'Dispositivo', + icon: Icons.devices, + themeColor: Colors.deepOrange, + children: [ + SharedModelSection( + label: 'Modello da Riparare', + modelId: ticket.targetModelId, + modelName: ticket.targetModelName, + onModelSelected: (id, name) => context + .read() + .updateModel(modelId: id, modelName: name), + ), + const SizedBox(height: 16), + TextFormField( + controller: _serialCtrl, + decoration: const InputDecoration( + labelText: 'Seriale / IMEI', + prefixIcon: Icon(Icons.qr_code), + ), + ), + ], + ); + } + + Widget _cardDettagli(TicketModel ticket) { + return _buildCard( + title: 'Dettagli Riparazione', + icon: Icons.build, + themeColor: Colors.pink, + children: [ + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + initialValue: ticket.ticketType, + decoration: const InputDecoration( + labelText: 'Tipo Lavorazione', + ), + items: TicketType.values + .map((t) => DropdownMenuItem(value: t, child: Text(t.name))) + .toList(), + onChanged: (val) => context + .read() + .updateFields(ticketType: val), + ), + ), + const SizedBox(width: 16), + Expanded( + child: DropdownButtonFormField( + initialValue: ticket.ticketStatus, + decoration: const InputDecoration(labelText: 'Stato Attuale'), + items: TicketStatus.values + .map((s) => DropdownMenuItem(value: s, child: Text(s.name))) + .toList(), + onChanged: (val) => + context.read().updateFields(status: val), + ), + ), + ], + ), + const SizedBox(height: 16), + TextFormField( + controller: _requestCtrl, + maxLines: 4, + decoration: const InputDecoration( + labelText: 'Difetto dichiarato o Richiesta del cliente', + alignLabelWithHint: true, + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: _accessoriesCtrl, + decoration: const InputDecoration( + labelText: 'Accessori Consegnati', + prefixIcon: Icon(Icons.cable), + ), + ), + const SizedBox(height: 16), + SwitchListTile( + title: const Text('Prestato Telefono di Cortesia?'), + value: ticket.hasCourtesyDevice, + onChanged: (val) => context.read().updateFields( + hasCourtesyDevice: val, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + ], + ); + } + + Widget _cardCosti(TicketModel ticket) { + return _buildCard( + title: 'Costi & Note', + icon: Icons.euro, + themeColor: Colors.teal, + children: [ + Row( + children: [ + Expanded( + child: TextFormField( + controller: _priceCtrl, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + decoration: const InputDecoration( + labelText: 'Preventivo Cliente (€)', + prefixIcon: Icon(Icons.sell_outlined), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _costCtrl, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + decoration: const InputDecoration( + labelText: 'Nostro Costo (€)', + prefixIcon: Icon(Icons.shopping_cart_outlined), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + TextFormField( + controller: _publicNotesCtrl, + maxLines: 2, + decoration: const InputDecoration( + labelText: 'Note Pubbliche (Visibili su ricevuta)', + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: _internalNotesCtrl, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Note Interne (Solo per lo Staff)', + fillColor: Colors.amber.withValues(alpha: 0.1), + filled: true, + ), + ), + ], + ); + } + + Widget _cardAssegnazione(TicketModel ticket) { + return _buildCard( + title: 'Assegnazione e Allegati', + icon: Icons.engineering, + themeColor: Colors.deepPurple, + children: [ + StaffSection( + label: 'Assegnato A', + staffId: ticket.assignedToId, + staffName: ticket.assignedToName, + onStaffSelected: (staff) => context + .read() + .updateFields(assignedToId: staff.id, assignedToName: staff.name), + ), + const Divider(height: 32), + // ECCO LA MAGIA: + SharedFilesSection( + titleNameForUpload: ticket.customerName ?? 'Nuovo Ticket', + ), + ], + ); + } + + // --- WIDGET BASE PER LA CARD --- Widget _buildCard({ required String title, required IconData icon, + required Color themeColor, required List children, }) { return Card( margin: const EdgeInsets.only(bottom: 24), - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, // Tolta l'ombra standard + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: themeColor.withValues(alpha: 0.3), + width: 1, + ), // Bordo colorato delicato + ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( @@ -466,13 +543,22 @@ class _TicketFormScreenState extends State { children: [ Row( children: [ - Icon(icon, color: Theme.of(context).colorScheme.primary), + // Pallino colorato con l'icona dentro + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: themeColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: themeColor), + ), const SizedBox(width: 12), Text( title, - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: themeColor, + ), ), ], ),