Files
flux/lib/features/tasks/ui/task_form_screen.dart

433 lines
15 KiB
Dart
Raw Normal View History

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-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(
onPressed: state.isFormValid
2026-05-27 16:00:50 +02:00
? () => cubit.saveTask(currentUserId: 'TODO_USER_ID')
2026-05-26 19:31:25 +02:00
: null,
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,
) {
return 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),
),
),
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),
),
),
const SizedBox(height: 24),
2026-05-27 16:00:50 +02:00
// Addio initialValue, benvenuto controller!
2026-05-26 19:31:25 +02:00
TextFormField(
2026-05-27 16:00:50 +02:00
controller: _titleController,
2026-05-26 19:31:25 +02:00
decoration: const InputDecoration(
labelText: 'Titolo del Task*',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.title),
),
onChanged: cubit.updateTitle,
),
const SizedBox(height: 16),
TextFormField(
2026-05-27 16:00:50 +02:00
controller: _descController,
2026-05-26 19:31:25 +02:00
maxLines: 4,
decoration: const InputDecoration(
labelText: 'Descrizione (opzionale)',
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
onChanged: cubit.updateDescription,
),
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
? 'Scadenza: ${state.dueDate!.day}/${state.dueDate!.month}/${state.dueDate!.year}'
: 'Nessuna scadenza impostata',
),
trailing: state.dueDate != null
? IconButton(
icon: const Icon(Icons.close),
onPressed: () => cubit.updateDueDate(null),
)
: null,
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: state.dueDate ?? DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime(2100),
);
if (date != null) cubit.updateDueDate(date);
},
),
],
);
}
// =========================================================================
// 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;
}
}