boh
This commit is contained in:
397
lib/features/tasks/ui/task_form_screen.dart
Normal file
397
lib/features/tasks/ui/task_form_screen.dart
Normal file
@@ -0,0 +1,397 @@
|
||||
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';
|
||||
|
||||
class TaskFormScreen extends StatelessWidget {
|
||||
const TaskFormScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<TaskFormCubit, TaskFormState>(
|
||||
listenWhen: (previous, current) => previous.status != current.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == TaskFormStatus.success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Task salvato con successo! 🎉')),
|
||||
);
|
||||
context.pop(); // Usciamo dalla pagina
|
||||
} 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: [
|
||||
// Tasto Salva (abilitato solo se il form è valido)
|
||||
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
|
||||
? () =>
|
||||
cubit.saveTask(
|
||||
currentUserId: 'TODO_USER_ID',
|
||||
) // Passa l'id utente loggato dal SessionCubit
|
||||
: null,
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('Salva'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.orange, // Il tuo colore primario
|
||||
disabledForegroundColor: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWideScreen = constraints.maxWidth > 800;
|
||||
|
||||
if (isWideScreen) {
|
||||
// --- VISTA DESKTOP / TABLET LARGHI (2 Colonne) ---
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- VISTA MOBILE (1 Colonna + BottomSheet) ---
|
||||
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',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. I CAMPI DEL FORM PRINCIPALE
|
||||
// =========================================================================
|
||||
Widget _buildFormFields(
|
||||
BuildContext context,
|
||||
TaskFormState state,
|
||||
TaskFormCubit cubit,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- SCOPE TOGGLE ---
|
||||
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),
|
||||
|
||||
// --- TITOLO E DESCRIZIONE ---
|
||||
TextFormField(
|
||||
initialValue: state.title,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Titolo del Task*',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.title),
|
||||
),
|
||||
onChanged: cubit.updateTitle,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
initialValue: state.description,
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user