boh
This commit is contained in:
@@ -16,6 +16,8 @@ class Tables {
|
|||||||
static const String staffInStores = 'staff_in_stores';
|
static const String staffInStores = 'staff_in_stores';
|
||||||
static const String staffMembers = 'staff_members';
|
static const String staffMembers = 'staff_members';
|
||||||
static const String stores = 'stores';
|
static const String stores = 'stores';
|
||||||
|
static const String tasks = 'tasks';
|
||||||
|
static const String taskAssignments = 'task_assignments';
|
||||||
static const String tickets = 'tickets';
|
static const String tickets = 'tickets';
|
||||||
static const String trackings = 'trackings';
|
static const String trackings = 'trackings';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.da
|
|||||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||||
import 'package:flux/features/master_data/providers/ui/provider_form_screen.dart';
|
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/providers/ui/provider_list_screen.dart';
|
||||||
|
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/staff/models/staff_member_model.dart';
|
||||||
import 'package:flux/features/master_data/staff/ui/staff_screen.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/bloc/store_cubit.dart';
|
||||||
@@ -44,7 +45,9 @@ import 'package:flux/features/operations/ui/operation_form_screen.dart';
|
|||||||
import 'package:flux/features/operations/ui/operation_list_screen.dart';
|
import 'package:flux/features/operations/ui/operation_list_screen.dart';
|
||||||
import 'package:flux/features/settings/settings_screen.dart';
|
import 'package:flux/features/settings/settings_screen.dart';
|
||||||
import 'package:flux/features/settings/theme_settings_view.dart';
|
import 'package:flux/features/settings/theme_settings_view.dart';
|
||||||
|
import 'package:flux/features/tasks/blocs/task_form_cubit.dart';
|
||||||
import 'package:flux/features/tasks/models/task_model.dart';
|
import 'package:flux/features/tasks/models/task_model.dart';
|
||||||
|
import 'package:flux/features/tasks/ui/task_form_screen.dart';
|
||||||
import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart';
|
import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart';
|
||||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
import 'package:flux/features/tickets/ui/ticket_form_screen.dart';
|
import 'package:flux/features/tickets/ui/ticket_form_screen.dart';
|
||||||
@@ -498,33 +501,39 @@ class AppRouter {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
/* GoRoute(
|
GoRoute(
|
||||||
path: '/task/edit/:id',
|
path: '/tasks/form/:id',
|
||||||
name: Routes.taskForm,
|
name: Routes.taskForm,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final id = state.pathParameters['id']!;
|
final String pathId = state.pathParameters['id'] ?? 'new';
|
||||||
final TaskModel task = state.extra as TaskModel;
|
final TaskModel? task = state.extra as TaskModel?;
|
||||||
|
final String? realTaskId;
|
||||||
|
if (pathId == 'new') {
|
||||||
|
realTaskId = null;
|
||||||
|
} else if (task?.id != null) {
|
||||||
|
realTaskId = task!.id;
|
||||||
|
} else {
|
||||||
|
realTaskId = pathId;
|
||||||
|
}
|
||||||
|
|
||||||
|
final allStaffList = context.read<StaffCubit>().state.allStaff;
|
||||||
|
|
||||||
// Creiamo il BLoC "al volo" solo per questa schermata
|
// Creiamo il BLoC "al volo" solo per questa schermata
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider<AttachmentsBloc>(
|
|
||||||
create: (context) => AttachmentsBloc(
|
|
||||||
parentId: id,
|
|
||||||
parentType: AttachmentParentType.note,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BlocProvider<TaskFormCubit>(
|
BlocProvider<TaskFormCubit>(
|
||||||
create: (context) => TaskFormCubit(
|
create: (context) => TaskFormCubit(
|
||||||
existingTask: task,
|
globalStaff: allStaffList,
|
||||||
|
initialTask: task,
|
||||||
|
initialTaskId: realTaskId,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
],
|
||||||
|
|
||||||
child: TaskFormScreen(task: task),
|
child: TaskFormScreen(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
), */
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ class DashboardTaskListCubit extends Cubit<DashboardTaskListState> {
|
|||||||
|
|
||||||
DashboardTaskListCubit() : super(DashboardTaskListState());
|
DashboardTaskListCubit() : super(DashboardTaskListState());
|
||||||
|
|
||||||
void startListening({required String staffId}) {
|
void startListening({required String staffId}) async {
|
||||||
emit(state.copyWith(status: DashboardTaskListStatus.loading));
|
emit(state.copyWith(status: DashboardTaskListStatus.loading));
|
||||||
_loadTasks(staffId: staffId);
|
await _loadTasks(staffId: staffId);
|
||||||
_taskChannel?.unsubscribe();
|
_taskChannel?.unsubscribe();
|
||||||
_taskChannel = _supabase
|
_taskChannel = _supabase
|
||||||
.channel('public:tasks_staff_$staffId')
|
.channel('public:tasks_staff_$staffId')
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class _DashboardTasksCardContent extends StatelessWidget {
|
|||||||
if (state.status == DashboardTaskListStatus.failure) {
|
if (state.status == DashboardTaskListStatus.failure) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Errore di caricamento",
|
"Errore di caricamento ${state.errorMessage}",
|
||||||
style: TextStyle(color: theme.colorScheme.error),
|
style: TextStyle(color: theme.colorScheme.error),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -236,23 +236,10 @@ class HomeScreen extends StatelessWidget {
|
|||||||
label: context.l10n.commonTask,
|
label: context.l10n.commonTask,
|
||||||
color: Colors.teal,
|
color: Colors.teal,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final companyId = context.read<SessionCubit>().state.company!.id!;
|
|
||||||
final currentStaffId = context
|
|
||||||
.read<SessionCubit>()
|
|
||||||
.state
|
|
||||||
.currentStaffMember!
|
|
||||||
.id!;
|
|
||||||
final emptyTask = TaskModel.empty(
|
|
||||||
companyId: companyId,
|
|
||||||
createdById: currentStaffId,
|
|
||||||
);
|
|
||||||
final savedTask = GetIt.I.get<TaskRepository>().createTask(
|
|
||||||
emptyTask,
|
|
||||||
);
|
|
||||||
context.pushNamed(
|
context.pushNamed(
|
||||||
Routes.taskForm,
|
Routes.taskForm,
|
||||||
pathParameters: {'id': 'new'},
|
pathParameters: {'id': 'new'},
|
||||||
extra: savedTask,
|
extra: (task: null),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ class StaffRepository {
|
|||||||
Future<List<StaffMemberModel>> getStaffMembers(String companyId) async {
|
Future<List<StaffMemberModel>> getStaffMembers(String companyId) async {
|
||||||
final response = await _supabase
|
final response = await _supabase
|
||||||
.from(Tables.staffMembers)
|
.from(Tables.staffMembers)
|
||||||
.select()
|
.select('''
|
||||||
|
*,
|
||||||
|
stores (*)
|
||||||
|
''')
|
||||||
.eq('company_id', companyId)
|
.eq('company_id', companyId)
|
||||||
.order('name', ascending: true);
|
.order('name', ascending: true);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flux/features/master_data/store/models/store_model.dart';
|
||||||
|
|
||||||
// L'Enum magico e blindato per il sistema
|
// L'Enum magico e blindato per il sistema
|
||||||
enum SystemRole {
|
enum SystemRole {
|
||||||
@@ -26,6 +27,7 @@ class StaffMemberModel extends Equatable {
|
|||||||
final SystemRole systemRole;
|
final SystemRole systemRole;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final bool hasJoined;
|
final bool hasJoined;
|
||||||
|
final StoreModel? store;
|
||||||
|
|
||||||
const StaffMemberModel({
|
const StaffMemberModel({
|
||||||
this.id,
|
this.id,
|
||||||
@@ -38,6 +40,7 @@ class StaffMemberModel extends Equatable {
|
|||||||
this.systemRole = SystemRole.user,
|
this.systemRole = SystemRole.user,
|
||||||
this.isActive = true,
|
this.isActive = true,
|
||||||
this.hasJoined = false,
|
this.hasJoined = false,
|
||||||
|
this.store,
|
||||||
});
|
});
|
||||||
|
|
||||||
StaffMemberModel copyWith({
|
StaffMemberModel copyWith({
|
||||||
@@ -52,6 +55,7 @@ class StaffMemberModel extends Equatable {
|
|||||||
SystemRole? systemRole,
|
SystemRole? systemRole,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
bool? hasJoined,
|
bool? hasJoined,
|
||||||
|
StoreModel? store,
|
||||||
}) {
|
}) {
|
||||||
return StaffMemberModel(
|
return StaffMemberModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -64,6 +68,7 @@ class StaffMemberModel extends Equatable {
|
|||||||
systemRole: systemRole ?? this.systemRole,
|
systemRole: systemRole ?? this.systemRole,
|
||||||
isActive: isActive ?? this.isActive,
|
isActive: isActive ?? this.isActive,
|
||||||
hasJoined: hasJoined ?? this.hasJoined,
|
hasJoined: hasJoined ?? this.hasJoined,
|
||||||
|
store: store ?? this.store,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +95,7 @@ class StaffMemberModel extends Equatable {
|
|||||||
systemRole: SystemRole.fromString(map['system_role']),
|
systemRole: SystemRole.fromString(map['system_role']),
|
||||||
isActive: map['is_active'] ?? true,
|
isActive: map['is_active'] ?? true,
|
||||||
hasJoined: map['has_joined'] ?? false,
|
hasJoined: map['has_joined'] ?? false,
|
||||||
|
store: map['store'] != null ? StoreModel.fromMap(map['store']) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,5 +126,6 @@ class StaffMemberModel extends Equatable {
|
|||||||
systemRole,
|
systemRole,
|
||||||
isActive,
|
isActive,
|
||||||
hasJoined,
|
hasJoined,
|
||||||
|
store,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
195
lib/features/tasks/blocs/task_form_cubit.dart
Normal file
195
lib/features/tasks/blocs/task_form_cubit.dart
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
|
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
||||||
|
import 'package:flux/features/tasks/data/task_repository.dart';
|
||||||
|
import 'package:flux/features/tasks/models/task_model.dart';
|
||||||
|
import 'package:flux/features/tasks/models/task_status.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
part 'task_form_state.dart';
|
||||||
|
|
||||||
|
class TaskFormCubit extends Cubit<TaskFormState> {
|
||||||
|
final TaskRepository _taskRepository = GetIt.I.get<TaskRepository>();
|
||||||
|
final TaskModel? _initialTask;
|
||||||
|
final String? _initialTaskId;
|
||||||
|
// Cache in memoria proveniente dallo StaffCubit!
|
||||||
|
final List<StaffMemberModel> _globalStaff;
|
||||||
|
|
||||||
|
final String currentCompanyId = GetIt.I
|
||||||
|
.get<SessionCubit>()
|
||||||
|
.state
|
||||||
|
.company!
|
||||||
|
.id!;
|
||||||
|
final String? currentStoreId = GetIt.I
|
||||||
|
.get<SessionCubit>()
|
||||||
|
.state
|
||||||
|
.currentStore
|
||||||
|
?.id;
|
||||||
|
|
||||||
|
TaskFormCubit({
|
||||||
|
required List<StaffMemberModel> globalStaff, // Iniettiamo la lista qui
|
||||||
|
TaskModel? initialTask,
|
||||||
|
String? initialTaskId,
|
||||||
|
}) : _globalStaff = globalStaff,
|
||||||
|
_initialTask = initialTask,
|
||||||
|
_initialTaskId = initialTaskId,
|
||||||
|
super(const TaskFormState()) {
|
||||||
|
_initForm(task: initialTask, initialTaskId: initialTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 1. INIZIALIZZAZIONE SINCRONA ---
|
||||||
|
void _initForm({TaskModel? task, String? initialTaskId}) {
|
||||||
|
final isGlobalMode = task != null
|
||||||
|
? task.storeId == null
|
||||||
|
: currentStoreId == null;
|
||||||
|
|
||||||
|
// MAGIA: Estraiamo gli ID dagli oggetti staff, o facciamo fallback su assignedToIds se c'è
|
||||||
|
final existingStaffIds =
|
||||||
|
task?.assignedToStaff.map((s) => s.id!).toList() ??
|
||||||
|
task?.assignedToIds ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
id: task?.id,
|
||||||
|
title: task?.title ?? '',
|
||||||
|
description: task?.description ?? '',
|
||||||
|
dueDate: task?.dueDate,
|
||||||
|
taskStatus: task?.status ?? TaskStatus.open,
|
||||||
|
isGlobal: isGlobalMode,
|
||||||
|
selectedStaffIds:
|
||||||
|
existingStaffIds, // Ora non si perde più i dipendenti!
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_updateStaffScope(isGlobalMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. SWITCH SCOPE E RAGGRUPPAMENTO ---
|
||||||
|
void toggleGlobalScope(bool isGlobal) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isGlobal: isGlobal,
|
||||||
|
selectedStaffIds: [], // Resettiamo la selezione se si cambia scope
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_updateStaffScope(isGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateStaffScope(bool isGlobal) {
|
||||||
|
// 1. Filtriamo in memoria
|
||||||
|
final filteredStaff = isGlobal
|
||||||
|
? _globalStaff
|
||||||
|
: _globalStaff.where((s) => s.store?.id == currentStoreId).toList();
|
||||||
|
|
||||||
|
// 2. Raggruppiamo per nome negozio (usando groupBy del pacchetto collection)
|
||||||
|
final groupedStaff = groupBy(
|
||||||
|
filteredStaff,
|
||||||
|
(StaffMemberModel s) => s.store?.name ?? 'Direzione / HQ',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Emettiamo il nuovo stato all'istante
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: TaskFormStatus.initial,
|
||||||
|
groupedAvailableStaff: groupedStaff,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. SELEZIONE AVANZATA (SINGOLA E PER NEGOZIO) ---
|
||||||
|
void toggleStaffSelection(String staffId) {
|
||||||
|
final currentList = List<String>.from(state.selectedStaffIds);
|
||||||
|
if (currentList.contains(staffId)) {
|
||||||
|
currentList.remove(staffId);
|
||||||
|
} else {
|
||||||
|
currentList.add(staffId);
|
||||||
|
}
|
||||||
|
emit(state.copyWith(selectedStaffIds: currentList));
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleStoreSelection(String storeName, bool selectAll) {
|
||||||
|
// Recupera tutti i membri di quel gruppo
|
||||||
|
final staffInStore = state.groupedAvailableStaff[storeName] ?? [];
|
||||||
|
final idsInStore = staffInStore.map((s) => s.id!).toList();
|
||||||
|
|
||||||
|
final currentSelection = Set<String>.from(state.selectedStaffIds);
|
||||||
|
|
||||||
|
if (selectAll) {
|
||||||
|
currentSelection.addAll(idsInStore);
|
||||||
|
} else {
|
||||||
|
currentSelection.removeAll(idsInStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(selectedStaffIds: currentSelection.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. AGGIORNAMENTO CAMPI STANDARD ---
|
||||||
|
void updateTitle(String title) => emit(state.copyWith(title: title));
|
||||||
|
|
||||||
|
void updateDescription(String desc) =>
|
||||||
|
emit(state.copyWith(description: desc));
|
||||||
|
|
||||||
|
void updateDueDate(DateTime? date) =>
|
||||||
|
emit(state.copyWith(dueDate: date, clearDueDate: date == null));
|
||||||
|
|
||||||
|
void updateLinkedTicket(String? ticketId) =>
|
||||||
|
emit(state.copyWith(linkedTicketId: ticketId));
|
||||||
|
|
||||||
|
// --- 5. LOGICA DEI REMINDER FINTI ---
|
||||||
|
void addMockReminder(String type, int minutes) {
|
||||||
|
final currentReminders = List<TaskReminder>.from(state.reminders);
|
||||||
|
currentReminders.add(TaskReminder(type: type, minutesBefore: minutes));
|
||||||
|
emit(state.copyWith(reminders: currentReminders));
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeMockReminder(int index) {
|
||||||
|
final currentReminders = List<TaskReminder>.from(state.reminders);
|
||||||
|
currentReminders.removeAt(index);
|
||||||
|
emit(state.copyWith(reminders: currentReminders));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 6. SALVATAGGIO FINALE ---
|
||||||
|
Future<void> saveTask({required String currentUserId}) async {
|
||||||
|
if (!state.isFormValid) return;
|
||||||
|
|
||||||
|
emit(state.copyWith(status: TaskFormStatus.submitting));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final taskToSave = TaskModel(
|
||||||
|
id: state.id,
|
||||||
|
companyId: currentCompanyId,
|
||||||
|
storeId: state.isGlobal
|
||||||
|
? null
|
||||||
|
: currentStoreId, // La vera discriminante Globale/Store!
|
||||||
|
createdById: state.id == null
|
||||||
|
? currentUserId
|
||||||
|
: null, // Lo settiamo solo alla creazione
|
||||||
|
title: state.title.trim(),
|
||||||
|
description: state.description.trim().isEmpty
|
||||||
|
? null
|
||||||
|
: state.description.trim(),
|
||||||
|
dueDate: state.dueDate,
|
||||||
|
status: state.taskStatus,
|
||||||
|
assignedToIds: state
|
||||||
|
.selectedStaffIds, // L'array che andrà a popolare la tabella di giunzione
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state.id == null) {
|
||||||
|
await _taskRepository.createTask(taskToSave);
|
||||||
|
} else {
|
||||||
|
await _taskRepository.updateTask(taskToSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(status: TaskFormStatus.success));
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: TaskFormStatus.failure,
|
||||||
|
errorMessage: 'Errore durante il salvataggio: $e',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/features/tasks/blocs/task_form_state.dart
Normal file
109
lib/features/tasks/blocs/task_form_state.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
part of 'task_form_cubit.dart';
|
||||||
|
|
||||||
|
enum TaskFormStatus { initial, loading, submitting, success, failure }
|
||||||
|
|
||||||
|
/// Placeholder finto per i futuri reminder (pg_cron)
|
||||||
|
class TaskReminder extends Equatable {
|
||||||
|
final String type; // es. 'email', 'push'
|
||||||
|
final int minutesBefore;
|
||||||
|
const TaskReminder({required this.type, required this.minutesBefore});
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [type, minutesBefore];
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskFormState extends Equatable {
|
||||||
|
final TaskFormStatus status;
|
||||||
|
final String? id; // Null se stiamo creando un nuovo task
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final DateTime? dueDate;
|
||||||
|
final TaskStatus taskStatus;
|
||||||
|
|
||||||
|
// --- SCOPING & ASSIGNMENTS ---
|
||||||
|
final bool isGlobal;
|
||||||
|
final List<String> selectedStaffIds;
|
||||||
|
final List<StaffMemberModel> availableStaff;
|
||||||
|
final Map<String, List<StaffMemberModel>> groupedAvailableStaff;
|
||||||
|
|
||||||
|
// --- FUTURI ANCORAGGI ---
|
||||||
|
final List<TaskReminder> reminders;
|
||||||
|
final String? linkedTicketId;
|
||||||
|
final String? linkedEmailId;
|
||||||
|
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
const TaskFormState({
|
||||||
|
this.status = TaskFormStatus.initial,
|
||||||
|
this.id,
|
||||||
|
this.title = '',
|
||||||
|
this.description = '',
|
||||||
|
this.dueDate,
|
||||||
|
this.taskStatus = TaskStatus.open,
|
||||||
|
this.isGlobal = false,
|
||||||
|
this.selectedStaffIds = const [],
|
||||||
|
this.availableStaff = const [],
|
||||||
|
this.groupedAvailableStaff = const {},
|
||||||
|
this.reminders = const [],
|
||||||
|
this.linkedTicketId,
|
||||||
|
this.linkedEmailId,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
// MAGIA: Il form è valido solo se c'è un titolo e, opzionalmente, altre regole.
|
||||||
|
bool get isFormValid => title.trim().isNotEmpty;
|
||||||
|
|
||||||
|
TaskFormState copyWith({
|
||||||
|
TaskFormStatus? status,
|
||||||
|
String? id,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
DateTime? dueDate,
|
||||||
|
bool clearDueDate = false, // Trucco Ninja per rimettere a null una data!
|
||||||
|
TaskStatus? taskStatus,
|
||||||
|
bool? isGlobal,
|
||||||
|
List<String>? selectedStaffIds,
|
||||||
|
List<StaffMemberModel>? availableStaff,
|
||||||
|
Map<String, List<StaffMemberModel>>? groupedAvailableStaff,
|
||||||
|
List<TaskReminder>? reminders,
|
||||||
|
String? linkedTicketId,
|
||||||
|
String? linkedEmailId,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return TaskFormState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
id: id ?? this.id,
|
||||||
|
title: title ?? this.title,
|
||||||
|
description: description ?? this.description,
|
||||||
|
dueDate: clearDueDate ? null : (dueDate ?? this.dueDate),
|
||||||
|
taskStatus: taskStatus ?? this.taskStatus,
|
||||||
|
isGlobal: isGlobal ?? this.isGlobal,
|
||||||
|
selectedStaffIds: selectedStaffIds ?? this.selectedStaffIds,
|
||||||
|
availableStaff: availableStaff ?? this.availableStaff,
|
||||||
|
groupedAvailableStaff:
|
||||||
|
groupedAvailableStaff ?? this.groupedAvailableStaff,
|
||||||
|
reminders: reminders ?? this.reminders,
|
||||||
|
linkedTicketId: linkedTicketId ?? this.linkedTicketId,
|
||||||
|
linkedEmailId: linkedEmailId ?? this.linkedEmailId,
|
||||||
|
errorMessage:
|
||||||
|
errorMessage, // Se copyWith è chiamato senza errore, lo pulisce in automatico
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
status,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
dueDate,
|
||||||
|
taskStatus,
|
||||||
|
isGlobal,
|
||||||
|
selectedStaffIds,
|
||||||
|
availableStaff,
|
||||||
|
groupedAvailableStaff,
|
||||||
|
reminders,
|
||||||
|
linkedTicketId,
|
||||||
|
linkedEmailId,
|
||||||
|
errorMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flux/core/enums_and_consts/consts.dart';
|
||||||
import 'package:flux/features/tasks/models/task_status.dart';
|
import 'package:flux/features/tasks/models/task_status.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
// Sostituisci con i percorsi corretti di FLUX
|
// Sostituisci con i percorsi corretti di FLUX
|
||||||
@@ -18,10 +19,15 @@ class TaskRepository {
|
|||||||
int? limit,
|
int? limit,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// 1. FASE FILTRI (PostgrestFilterBuilder)
|
// 1. FASE FILTRI: Usa il join esplicito stile "Notes"
|
||||||
var filterBuilder = _supabase
|
var filterBuilder = _supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
.select('*, assigned_to_staff:staff_members!task_assignments(*)')
|
.select('''
|
||||||
|
*,
|
||||||
|
task_assignments (
|
||||||
|
staff_members (*)
|
||||||
|
)
|
||||||
|
''')
|
||||||
.eq('company_id', companyId);
|
.eq('company_id', companyId);
|
||||||
|
|
||||||
if (storeId != null) {
|
if (storeId != null) {
|
||||||
@@ -31,9 +37,8 @@ class TaskRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (staffId != null) {
|
if (staffId != null) {
|
||||||
filterBuilder = filterBuilder.or(
|
// Grazie al trigger, hai l'array pronto per il filtro senza impazzire!
|
||||||
'staff_id.eq.$staffId,staff_id.is.null',
|
filterBuilder = filterBuilder.contains('assigned_to_ids', [staffId]);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statuses != null && statuses.isNotEmpty) {
|
if (statuses != null && statuses.isNotEmpty) {
|
||||||
@@ -41,13 +46,11 @@ class TaskRepository {
|
|||||||
filterBuilder = filterBuilder.inFilter('status', statusValues);
|
filterBuilder = filterBuilder.inFilter('status', statusValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. FASE TRASFORMAZIONI (PostgrestTransformBuilder)
|
// 2. FASE TRASFORMAZIONI
|
||||||
// L'ordinamento lo facciamo sempre, quindi partiamo da qui
|
|
||||||
var transformBuilder = filterBuilder
|
var transformBuilder = filterBuilder
|
||||||
.order('due_date', ascending: true, nullsFirst: false)
|
.order('due_date', ascending: true, nullsFirst: false)
|
||||||
.order('created_at', ascending: false, nullsFirst: false);
|
.order('created_at', ascending: false, nullsFirst: false);
|
||||||
|
|
||||||
// Aggiungiamo il limite se richiesto
|
|
||||||
if (limit != null) {
|
if (limit != null) {
|
||||||
transformBuilder = transformBuilder.limit(limit);
|
transformBuilder = transformBuilder.limit(limit);
|
||||||
}
|
}
|
||||||
@@ -55,6 +58,7 @@ class TaskRepository {
|
|||||||
// 3. ESECUZIONE DELLA QUERY
|
// 3. ESECUZIONE DELLA QUERY
|
||||||
final response = await transformBuilder;
|
final response = await transformBuilder;
|
||||||
|
|
||||||
|
// 4. PARSING DEI DATI
|
||||||
return (response as List).map((json) => TaskModel.fromMap(json)).toList();
|
return (response as List).map((json) => TaskModel.fromMap(json)).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Errore nel recupero dei task: $e');
|
throw Exception('Errore nel recupero dei task: $e');
|
||||||
@@ -95,8 +99,9 @@ class TaskRepository {
|
|||||||
|
|
||||||
// --- 3. AGGIORNAMENTO DEL TASK ---
|
// --- 3. AGGIORNAMENTO DEL TASK ---
|
||||||
Future<TaskModel> updateTask(TaskModel task) async {
|
Future<TaskModel> updateTask(TaskModel task) async {
|
||||||
if (task.id == null)
|
if (task.id == null) {
|
||||||
throw Exception('ID Task mancante. Impossibile aggiornare.');
|
throw Exception('ID Task mancante. Impossibile aggiornare.');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final taskData = task.toMap();
|
final taskData = task.toMap();
|
||||||
@@ -133,7 +138,7 @@ class TaskRepository {
|
|||||||
try {
|
try {
|
||||||
final response = await _supabase
|
final response = await _supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
.select('*, assigned_to_staff:staff!task_assignments(*)')
|
.select('*, assigned_to_staff:staff_members!task_assignments(*)')
|
||||||
.eq('id', taskId)
|
.eq('id', taskId)
|
||||||
.single();
|
.single();
|
||||||
return TaskModel.fromMap(response);
|
return TaskModel.fromMap(response);
|
||||||
|
|||||||
@@ -91,37 +91,46 @@ class TaskModel extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- SERIALIZZAZIONE DA SUPABASE ---
|
// --- SERIALIZZAZIONE DA SUPABASE ---
|
||||||
factory TaskModel.fromMap(Map<String, dynamic> json) {
|
factory TaskModel.fromMap(Map<String, dynamic> map) {
|
||||||
// 1. Gestiamo l'array nullo di Supabase trasformandolo in lista vuota
|
// 1. Gestiamo l'array nullo di Supabase trasformandolo in lista vuota
|
||||||
final List<String> parsedAssignedToIds = json['assigned_to_ids'] != null
|
final List<String> parsedAssignedToIds = map['assigned_to_ids'] != null
|
||||||
? List<String>.from(json['assigned_to_ids'])
|
? List<String>.from(map['assigned_to_ids'])
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// 2. Mappiamo il JOIN dello staff, se presente
|
// 2. Mappiamo il JOIN dello staff, se presente
|
||||||
List<StaffMemberModel> parsedAssignedToStaff = [];
|
List<StaffMemberModel> staffList = [];
|
||||||
if (json['assigned_to_staff'] != null) {
|
|
||||||
final staffList = json['assigned_to_staff'] as List;
|
// Gestione del JSON proveniente dal Join nidificato (es. task_assignments -> staff_members)
|
||||||
parsedAssignedToStaff = staffList
|
if (map['task_assignments'] != null) {
|
||||||
.map((s) => StaffMemberModel.fromMap(s as Map<String, dynamic>))
|
staffList = (map['task_assignments'] as List)
|
||||||
|
.map((a) => a['staff_members'])
|
||||||
|
.where((s) => s != null)
|
||||||
|
.map((s) => StaffMemberModel.fromMap(s))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
// Gestione del JSON piatto (se mai lo userai in altre chiamate RPC o viste)
|
||||||
|
else if (map['assigned_to_staff'] != null) {
|
||||||
|
staffList = (map['assigned_to_staff'] as List)
|
||||||
|
.map((s) => StaffMemberModel.fromMap(s))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return TaskModel(
|
return TaskModel(
|
||||||
id: json['id'] as String?,
|
id: map['id'] as String?,
|
||||||
companyId: json['company_id'] as String?,
|
companyId: map['company_id'] as String?,
|
||||||
title: json['title'] as String? ?? '',
|
title: map['title'] as String? ?? '',
|
||||||
description: json['description'] as String?,
|
description: map['description'] as String?,
|
||||||
assignedToIds: parsedAssignedToIds,
|
assignedToIds: parsedAssignedToIds,
|
||||||
assignedToStaff: parsedAssignedToStaff,
|
assignedToStaff: staffList,
|
||||||
createdById: json['created_by_id'] as String?,
|
createdById: map['created_by_id'] as String?,
|
||||||
dueDate: json['due_date'] != null
|
dueDate: map['due_date'] != null
|
||||||
? DateTime.parse(json['due_date'] as String).toLocal()
|
? DateTime.parse(map['due_date'] as String).toLocal()
|
||||||
: null,
|
: null,
|
||||||
status: TaskStatusExtension.fromString(json['status'] as String?),
|
status: TaskStatusExtension.fromString(map['status'] as String?),
|
||||||
createdAt: json['created_at'] != null
|
createdAt: map['created_at'] != null
|
||||||
? DateTime.parse(json['created_at'] as String).toLocal()
|
? DateTime.parse(map['created_at'] as String).toLocal()
|
||||||
: null,
|
: null,
|
||||||
storeId: json['store_id'] as String?,
|
storeId: map['store_id'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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