2026-05-26 19:31:25 +02:00
import ' package:flutter/material.dart ' ;
import ' package:flutter_bloc/flutter_bloc.dart ' ;
import ' package:flux/features/tasks/blocs/task_form_cubit.dart ' ;
import ' package:go_router/go_router.dart ' ;
2026-05-27 16:00:50 +02:00
class TaskFormScreen extends StatefulWidget {
2026-05-26 19:31:25 +02:00
const TaskFormScreen ( { super . key } ) ;
2026-05-27 16:00:50 +02:00
@ override
State < TaskFormScreen > createState ( ) = > _TaskFormScreenState ( ) ;
}
class _TaskFormScreenState extends State < TaskFormScreen > {
late final TextEditingController _titleController ;
late final TextEditingController _descController ;
@ override
void initState ( ) {
super . initState ( ) ;
// Leggiamo lo stato iniziale dal Cubit (che ha già i dati del task esistente)
final initialState = context . read < TaskFormCubit > ( ) . state ;
_titleController = TextEditingController ( text: initialState . title ) ;
_descController = TextEditingController ( text: initialState . description ) ;
}
@ override
void dispose ( ) {
_titleController . dispose ( ) ;
_descController . dispose ( ) ;
super . dispose ( ) ;
}
2026-05-29 12:26:41 +02:00
void _showAddReminderDialog ( BuildContext context , TaskFormCubit cubit ) {
int minutes = 15 ;
String channel = ' push ' ;
showDialog (
context: context ,
builder: ( dialogContext ) {
return AlertDialog (
title: const Text ( ' Aggiungi Promemoria ' ) ,
content: Column (
mainAxisSize: MainAxisSize . min ,
children: [
DropdownButtonFormField < int > (
initialValue: minutes ,
decoration: const InputDecoration ( labelText: ' Preavviso ' ) ,
items: const [
DropdownMenuItem ( value: 5 , child: Text ( ' 5 minuti prima ' ) ) ,
DropdownMenuItem ( value: 15 , child: Text ( ' 15 minuti prima ' ) ) ,
DropdownMenuItem ( value: 60 , child: Text ( ' 1 ora prima ' ) ) ,
DropdownMenuItem ( value: 1440 , child: Text ( ' 1 giorno prima ' ) ) ,
] ,
onChanged: ( v ) = > { if ( v ! = null ) minutes = v } ,
) ,
const SizedBox ( height: 16 ) ,
DropdownButtonFormField < String > (
initialValue: channel ,
decoration: const InputDecoration ( labelText: ' Canale ' ) ,
items: const [
DropdownMenuItem ( value: ' push ' , child: Text ( ' Notifica Push ' ) ) ,
DropdownMenuItem ( value: ' email ' , child: Text ( ' Email ' ) ) ,
] ,
onChanged: ( v ) = > { if ( v ! = null ) channel = v } ,
) ,
] ,
) ,
actions: [
TextButton (
onPressed: ( ) = > Navigator . pop ( dialogContext ) ,
child: const Text ( ' Annulla ' ) ,
) ,
TextButton (
onPressed: ( ) {
cubit . addReminderRule ( minutes , channel ) ;
Navigator . pop ( dialogContext ) ;
} ,
child: const Text (
' Inserisci ' ,
style: TextStyle ( color: Colors . orange ) ,
) ,
) ,
] ,
) ;
} ,
) ;
}
2026-05-26 19:31:25 +02:00
@ override
Widget build ( BuildContext context ) {
return BlocConsumer < TaskFormCubit , TaskFormState > (
listenWhen: ( previous , current ) = > previous . status ! = current . status ,
listener: ( context , state ) {
2026-05-27 16:00:50 +02:00
// GESTIONE DEEP LINK: Se eravamo in caricamento e ora siamo pronti, popoliamo i controller!
if ( state . status = = TaskFormStatus . initial ) {
if ( _titleController . text ! = state . title ) {
_titleController . text = state . title ;
}
if ( _descController . text ! = state . description ) {
_descController . text = state . description ;
}
}
2026-05-26 19:31:25 +02:00
if ( state . status = = TaskFormStatus . success ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar ( content: Text ( ' Task salvato con successo! 🎉 ' ) ) ,
) ;
2026-05-27 16:00:50 +02:00
context . pop ( ) ;
2026-05-26 19:31:25 +02:00
} else if ( state . status = = TaskFormStatus . failure ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
SnackBar (
content: Text ( state . errorMessage ? ? ' Errore di salvataggio ' ) ,
backgroundColor: Theme . of ( context ) . colorScheme . error ,
) ,
) ;
}
} ,
builder: ( context , state ) {
final cubit = context . read < TaskFormCubit > ( ) ;
final isEditing = state . id ! = null ;
return Scaffold (
appBar: AppBar (
title: Text ( isEditing ? ' Modifica Task ' : ' Nuovo Task ' ) ,
actions: [
if ( state . status = = TaskFormStatus . submitting )
const Padding (
padding: EdgeInsets . all ( 16.0 ) ,
child: SizedBox (
width: 20 ,
height: 20 ,
child: CircularProgressIndicator ( strokeWidth: 2 ) ,
) ,
)
else
TextButton . icon (
2026-05-30 12:12:14 +02:00
onPressed: state . isFormValid ? ( ) = > cubit . saveTask ( ) : null ,
2026-05-26 19:31:25 +02:00
icon: const Icon ( Icons . save ) ,
label: const Text ( ' Salva ' ) ,
style: TextButton . styleFrom (
2026-05-27 16:00:50 +02:00
foregroundColor: Colors . orange ,
2026-05-26 19:31:25 +02:00
disabledForegroundColor: Colors . grey ,
) ,
) ,
] ,
) ,
2026-05-27 16:00:50 +02:00
body: state . status = = TaskFormStatus . loading
// Se sta scaricando i dati dal Deep Link, mostriamo un bel loader centrato
? const Center ( child: CircularProgressIndicator ( ) )
: LayoutBuilder (
builder: ( context , constraints ) {
final isWideScreen = constraints . maxWidth > 800 ;
2026-05-26 19:31:25 +02:00
2026-05-27 16:00:50 +02:00
if ( isWideScreen ) {
return Row (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Expanded (
flex: 6 ,
child: SingleChildScrollView (
padding: const EdgeInsets . all ( 24.0 ) ,
child: _buildFormFields ( context , state , cubit ) ,
) ,
) ,
VerticalDivider (
color: Theme . of ( context ) . dividerColor ,
) ,
Expanded (
flex: 4 ,
child: _buildStaffSelectorInline (
context ,
state ,
cubit ,
) ,
) ,
] ,
) ;
}
2026-05-26 19:31:25 +02:00
2026-05-27 16:00:50 +02:00
return SingleChildScrollView (
padding: const EdgeInsets . all ( 16.0 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
_buildFormFields ( context , state , cubit ) ,
const SizedBox ( height: 30 ) ,
ElevatedButton . icon (
style: ElevatedButton . styleFrom (
padding: const EdgeInsets . symmetric ( vertical: 16 ) ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
) ,
onPressed: ( ) = >
_showStaffBottomSheet ( context , cubit ) ,
icon: const Icon ( Icons . group_add ) ,
label: Text (
state . selectedStaffIds . isEmpty
? ' Assegna Staff '
: ' Assegnato a ${ state . selectedStaffIds . length } persone ' ,
) ,
) ,
] ,
2026-05-26 19:31:25 +02:00
) ,
2026-05-27 16:00:50 +02:00
) ;
} ,
2026-05-26 19:31:25 +02:00
) ,
) ;
} ,
) ;
}
2026-05-27 16:00:50 +02:00
// --- I CAMPI DEL FORM (Aggiornati con i Controller) ---
2026-05-26 19:31:25 +02:00
Widget _buildFormFields (
BuildContext context ,
TaskFormState state ,
TaskFormCubit cubit ,
) {
2026-05-29 12:26:41 +02:00
return FocusTraversalGroup (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Card (
elevation: 0 ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
side: BorderSide (
color: Theme . of ( context ) . dividerColor . withValues ( alpha: 0.2 ) ,
) ,
2026-05-26 19:31:25 +02:00
) ,
2026-05-29 12:26:41 +02:00
child: SwitchListTile (
title: const Text (
' Task Globale Aziendale ' ,
style: TextStyle ( fontWeight: FontWeight . bold ) ,
) ,
subtitle: const Text (
' Visibile a tutta l \' azienda, non legato a un negozio specifico. ' ,
) ,
value: state . isGlobal ,
activeThumbColor: Colors . orange ,
onChanged: ( val ) = > cubit . toggleGlobalScope ( val ) ,
2026-05-26 19:31:25 +02:00
) ,
) ,
2026-05-29 12:26:41 +02:00
const SizedBox ( height: 24 ) ,
// Addio initialValue, benvenuto controller!
TextFormField (
controller: _titleController ,
decoration: const InputDecoration (
labelText: ' Titolo del Task* ' ,
border: OutlineInputBorder ( ) ,
prefixIcon: Icon ( Icons . title ) ,
) ,
onChanged: cubit . updateTitle ,
2026-05-26 19:31:25 +02:00
) ,
2026-05-29 12:26:41 +02:00
const SizedBox ( height: 16 ) ,
TextFormField (
controller: _descController ,
maxLines: 4 ,
decoration: const InputDecoration (
labelText: ' Descrizione (opzionale) ' ,
border: OutlineInputBorder ( ) ,
alignLabelWithHint: true ,
) ,
onChanged: cubit . updateDescription ,
2026-05-26 19:31:25 +02:00
) ,
2026-05-29 12:26:41 +02:00
const SizedBox ( height: 24 ) ,
// --- SCADENZA ---
ListTile (
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
side: BorderSide ( color: Theme . of ( context ) . dividerColor ) ,
) ,
leading: const Icon ( Icons . calendar_today , color: Colors . orange ) ,
title: Text (
state . dueDate ! = null
// Formattiamo aggiungendo gli zeri (es. 05/09/2026 alle 09:05)
? ' Scadenza: ${ state . dueDate ! . day . toString ( ) . padLeft ( 2 , ' 0 ' ) } / ${ state . dueDate ! . month . toString ( ) . padLeft ( 2 , ' 0 ' ) } / ${ state . dueDate ! . year } alle ${ state . dueDate ! . hour . toString ( ) . padLeft ( 2 , ' 0 ' ) } : ${ state . dueDate ! . minute . toString ( ) . padLeft ( 2 , ' 0 ' ) } '
: ' Nessuna scadenza impostata ' ,
) ,
trailing: state . dueDate ! = null
? IconButton (
icon: const Icon ( Icons . close ) ,
onPressed: ( ) = > cubit . updateDueDate ( null ) ,
)
: null ,
onTap: ( ) async {
// 1. Chiediamo prima la Data
final date = await showDatePicker (
context: context ,
initialDate: state . dueDate ? ? DateTime . now ( ) ,
firstDate: DateTime . now ( ) ,
lastDate: DateTime ( 2100 ) ,
) ;
// Se l'utente chiude il calendario senza scegliere, ci fermiamo
if ( date = = null | | ! context . mounted ) return ;
// 2. Chiediamo subito dopo l'Orario
final time = await showTimePicker (
context: context ,
initialTime: state . dueDate ! = null
? TimeOfDay . fromDateTime ( state . dueDate ! )
: const TimeOfDay ( hour: 9 , minute: 0 ) , // Default ore 09:00
) ;
// Se l'utente chiude l'orologio senza scegliere, ci fermiamo
if ( time = = null ) return ;
// 3. Fondiamo Data e Ora in un nuovo oggetto DateTime
final finalDateTime = DateTime (
date . year ,
date . month ,
date . day ,
time . hour ,
time . minute ,
) ;
// Aggiorniamo lo stato tramite il Cubit
cubit . updateDueDate ( finalDateTime ) ;
} ,
2026-05-26 19:31:25 +02:00
) ,
2026-05-29 12:26:41 +02:00
if ( state . dueDate ! = null ) . . . [
const SizedBox ( height: 24 ) ,
Text (
' Promemoria del Task ' ,
style: Theme . of (
context ,
) . textTheme . titleMedium ? . copyWith ( fontWeight: FontWeight . bold ) ,
) ,
const SizedBox ( height: 8 ) ,
Card (
elevation: 0 ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
side: BorderSide (
color: Theme . of ( context ) . dividerColor . withValues ( alpha: 0.3 ) ,
) ,
) ,
child: Padding (
padding: const EdgeInsets . all ( 12.0 ) ,
child: Column (
children: [
// Elenco dei promemoria attuali del form
ListView . builder (
shrinkWrap: true ,
physics: const NeverScrollableScrollPhysics ( ) ,
itemCount: state . reminders . length ,
itemBuilder: ( context , index ) {
final reminder = state . reminders [ index ] ;
final isPush = reminder . channel = = ' push ' ;
return ListTile (
dense: true ,
leading: Icon (
isPush
? Icons . notifications_active_outlined
: Icons . mail_outline ,
color: isPush ? Colors . orange : Colors . blue ,
) ,
title: Text (
reminder . friendlyTime ,
style: const TextStyle ( fontWeight: FontWeight . w600 ) ,
) ,
trailing: IconButton (
icon: const Icon (
Icons . close ,
size: 18 ,
color: Colors . redAccent ,
) ,
onPressed: ( ) = > cubit . removeReminderRule ( index ) ,
) ,
) ;
} ,
) ,
const Divider ( ) ,
// Tasto di aggiunta rapida promemoria
TextButton . icon (
onPressed: ( ) = > _showAddReminderDialog ( context , cubit ) ,
icon: const Icon ( Icons . add , size: 18 ) ,
label: const Text ( ' Aggiungi un promemoria a questo task ' ) ,
style: TextButton . styleFrom (
foregroundColor: Colors . orange ,
) ,
) ,
] ,
) ,
) ,
) ,
] ,
] ,
) ,
2026-05-26 19:31:25 +02:00
) ;
}
// =========================================================================
// 2. SELEZIONE STAFF INLINE (PER DESKTOP/WIDE)
// =========================================================================
Widget _buildStaffSelectorInline (
BuildContext context ,
TaskFormState state ,
TaskFormCubit cubit ,
) {
return Container (
color: Theme . of ( context ) . cardColor ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
const Padding (
padding: EdgeInsets . all ( 24.0 ) ,
child: Text (
' Assegnazione Staff ' ,
style: TextStyle ( fontSize: 20 , fontWeight: FontWeight . bold ) ,
) ,
) ,
Expanded (
child: ListView (
padding: const EdgeInsets . symmetric ( horizontal: 16.0 ) ,
children: _buildGroupedStaffList ( context , state , cubit ) ,
) ,
) ,
] ,
) ,
) ;
}
// =========================================================================
// 3. BOTTOM SHEET SELEZIONE STAFF (PER MOBILE)
// =========================================================================
void _showStaffBottomSheet ( BuildContext context , TaskFormCubit cubit ) {
showModalBottomSheet (
context: context ,
isScrollControlled: true ,
useSafeArea: true ,
shape: const RoundedRectangleBorder (
borderRadius: BorderRadius . vertical ( top: Radius . circular ( 20 ) ) ,
) ,
builder: ( bottomSheetContext ) {
return DraggableScrollableSheet (
expand: false ,
initialChildSize: 0.7 , // Occupa il 70% dello schermo in altezza
minChildSize: 0.5 ,
maxChildSize: 0.9 ,
builder: ( _ , controller ) {
return BlocBuilder < TaskFormCubit , TaskFormState > (
bloc:
cubit , // Passiamo il cubit esistente per mantenere lo stato!
builder: ( context , state ) {
return Column (
children: [
const Padding (
padding: EdgeInsets . all ( 16.0 ) ,
child: Text (
' Assegna Staff ' ,
style: TextStyle (
fontSize: 20 ,
fontWeight: FontWeight . bold ,
) ,
) ,
) ,
const Divider ( height: 1 ) ,
Expanded (
child: ListView (
controller: controller ,
padding: const EdgeInsets . only ( bottom: 24 ) ,
children: _buildGroupedStaffList ( context , state , cubit ) ,
) ,
) ,
] ,
) ;
} ,
) ;
} ,
) ;
} ,
) ;
}
// =========================================================================
// 4. GENERATORE DELLA LISTA RAGGRUPPATA (RIUTILIZZABILE)
// =========================================================================
List < Widget > _buildGroupedStaffList (
BuildContext context ,
TaskFormState state ,
TaskFormCubit cubit ,
) {
final widgets = < Widget > [ ] ;
if ( state . groupedAvailableStaff . isEmpty ) {
return [
const Padding (
padding: EdgeInsets . all ( 32.0 ) ,
child: Center (
child: Text (
' Nessun membro dello staff trovato. ' ,
style: TextStyle ( fontStyle: FontStyle . italic ) ,
) ,
) ,
) ,
] ;
}
// Iteriamo sulla mappa { "Nome Negozio" : [Lista Dipendenti] }
for ( final entry in state . groupedAvailableStaff . entries ) {
final storeName = entry . key ;
final staffList = entry . value ;
// Verifichiamo se TUTTI i membri di questo negozio sono selezionati
final allSelectedInStore = staffList . every (
( staff ) = > state . selectedStaffIds . contains ( staff . id ) ,
) ;
widgets . add (
Padding (
padding: const EdgeInsets . only (
top: 24.0 ,
bottom: 8.0 ,
left: 16 ,
right: 8 ,
) ,
child: Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Text (
storeName . toUpperCase ( ) ,
style: TextStyle (
color: Theme . of ( context ) . colorScheme . primary ,
fontWeight: FontWeight . bold ,
letterSpacing: 1.2 ,
) ,
) ,
// IL MAGICO BOTTONE "SELEZIONA TUTTI" DEL NEGOZIO
TextButton . icon (
onPressed: ( ) = >
cubit . toggleStoreSelection ( storeName , ! allSelectedInStore ) ,
icon: Icon (
allSelectedInStore ? Icons . deselect : Icons . select_all ,
size: 18 ,
) ,
label: Text (
allSelectedInStore ? ' Deseleziona ' : ' Seleziona Tutti ' ,
) ,
style: TextButton . styleFrom (
visualDensity: VisualDensity . compact ,
foregroundColor: allSelectedInStore
? Colors . grey
: Colors . orange ,
) ,
) ,
] ,
) ,
) ,
) ;
// Renderizziamo i dipendenti di questo negozio usando dei Wrap con FilterChip
widgets . add (
Padding (
padding: const EdgeInsets . symmetric ( horizontal: 16.0 ) ,
child: Wrap (
spacing: 8.0 ,
runSpacing: 8.0 ,
children: staffList . map ( ( staff ) {
final isSelected = state . selectedStaffIds . contains ( staff . id ) ;
return FilterChip (
label: Text ( staff . name ) ,
selected: isSelected ,
selectedColor: Colors . orange . withValues ( alpha: 0.2 ) ,
checkmarkColor: Colors . orange ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
onSelected: ( _ ) = > cubit . toggleStaffSelection ( staff . id ! ) ,
) ;
} ) . toList ( ) ,
) ,
) ,
) ;
}
return widgets ;
}
}