Convertito StoreBloc in StoreCubit per coerenza con il resto del progetto. Sistemata la logica di assegnazione dipendenti nel modal dei negozi. Utilizzato il doppio BlocBuilder per garantire la reattività tra StaffCubit e StoreCubit. Reviewed-on: http://catelliub.zapto.org:3000/brontomark/flux/pulls/1 Co-authored-by: Mark M2 Macbook <marco@catelli.it> Co-committed-by: Mark M2 Macbook <marco@catelli.it>
316 lines
11 KiB
Dart
316 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:flux/core/blocs/session/session_bloc.dart';
|
|
import 'package:flux/core/theme/theme.dart';
|
|
import 'package:flux/core/widgets/flux_text_field.dart';
|
|
import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; // Tuo percorso
|
|
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
|
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
|
|
|
|
class StaffScreen extends StatefulWidget {
|
|
const StaffScreen({super.key});
|
|
|
|
@override
|
|
State<StaffScreen> createState() => _StaffScreenState();
|
|
}
|
|
|
|
class _StaffScreenState extends State<StaffScreen> {
|
|
String? _selectedStoreId;
|
|
bool _showAllCompanyStaff = true; // Partiamo con la vista globale
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Carichiamo subito tutto
|
|
context.read<StaffCubit>().loadAllStaff();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: context.background,
|
|
appBar: AppBar(
|
|
title: const Text("Anagrafica Personale"),
|
|
actions: [
|
|
// Toggle per vista Azienda / Negozio
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 16),
|
|
child: FilterChip(
|
|
label: const Text("Tutta l'Azienda"),
|
|
selected: _showAllCompanyStaff,
|
|
onSelected: (val) => setState(() => _showAllCompanyStaff = val),
|
|
selectedColor: context.accent.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// --- BARRA FILTRO NEGOZIO (Visibile solo se non 'Tutta l'Azienda') ---
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
height: _showAllCompanyStaff ? 0 : 80,
|
|
child: _showAllCompanyStaff
|
|
? const SizedBox()
|
|
: _buildStoreSelector(),
|
|
),
|
|
|
|
// --- LISTA PERSONALE ---
|
|
Expanded(
|
|
child: BlocBuilder<StaffCubit, StaffState>(
|
|
builder: (context, state) {
|
|
final list = _showAllCompanyStaff
|
|
? state.allStaff
|
|
: (state.staffByStore[_selectedStoreId] ?? []);
|
|
|
|
if (state.isLoading && list.isEmpty) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (list.isEmpty) {
|
|
return _buildEmptyState();
|
|
}
|
|
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: list.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
|
itemBuilder: (context, index) {
|
|
final member = list[index];
|
|
return _buildStaffCard(member);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
onPressed: () => _openStaffForm(context),
|
|
label: const Text("Aggiungi"),
|
|
icon: const Icon(Icons.person_add_alt_1),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStoreSelector() {
|
|
return BlocBuilder<StoreCubit, StoreState>(
|
|
// Assumendo tu abbia uno StoreCubit
|
|
builder: (context, state) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: DropdownButtonFormField<String>(
|
|
initialValue: _selectedStoreId,
|
|
decoration: const InputDecoration(labelText: "Filtra per Negozio"),
|
|
items: state.stores
|
|
.map((s) => DropdownMenuItem(value: s.id, child: Text(s.nome)))
|
|
.toList(),
|
|
onChanged: (id) {
|
|
setState(() => _selectedStoreId = id);
|
|
if (id != null) context.read<StaffCubit>().loadStaffForStore(id);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildStaffCard(StaffMemberModel member) {
|
|
return Card(
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
side: BorderSide(color: context.accent.withValues(alpha: 0.1)),
|
|
),
|
|
child: ListTile(
|
|
contentPadding: const EdgeInsets.all(12),
|
|
leading: CircleAvatar(
|
|
backgroundColor: context.accent.withValues(alpha: 0.1),
|
|
child: Text(member.name[0], style: TextStyle(color: context.accent)),
|
|
),
|
|
title: Text(
|
|
member.name,
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (member.email.isNotEmpty) Text(member.email),
|
|
Text(member.phone.isNotEmpty ? member.phone : "Nessun telefono"),
|
|
],
|
|
),
|
|
trailing: const Icon(Icons.edit_note),
|
|
onTap: () => _openStaffForm(context, member: member),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _openStaffForm(BuildContext context, {StaffMemberModel? member}) {
|
|
final nameController = TextEditingController(text: member?.name);
|
|
final emailController = TextEditingController(text: member?.email);
|
|
final phoneController = TextEditingController(text: member?.phone);
|
|
|
|
// 1. Inizializziamo la lista temporanea attingendo dallo stato del Cubit
|
|
// Usiamo storesByStaff (la mappa che indicizza i negozi per ogni ID dipendente)
|
|
List<String> tempSelectedStores =
|
|
context
|
|
.read<StaffCubit>()
|
|
.state
|
|
.storesByStaff[member?.id]
|
|
?.map((s) => s.id!)
|
|
.toList() ??
|
|
[];
|
|
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (context) => StatefulBuilder(
|
|
// <--- QUESTO è il segreto per le Chip
|
|
builder: (context, setModalState) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).scaffoldBackgroundColor,
|
|
borderRadius: const BorderRadius.vertical(
|
|
top: Radius.circular(24),
|
|
),
|
|
),
|
|
padding: EdgeInsets.only(
|
|
top: 24,
|
|
left: 24,
|
|
right: 24,
|
|
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
member == null
|
|
? "Nuovo Collaboratore"
|
|
: "Modifica Collaboratore",
|
|
style: const TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
FluxTextField(
|
|
controller: nameController,
|
|
label: "Nome e Cognome",
|
|
icon: Icons.person,
|
|
),
|
|
const SizedBox(height: 16),
|
|
FluxTextField(
|
|
controller: emailController,
|
|
label: "Email",
|
|
icon: Icons.email,
|
|
),
|
|
const SizedBox(height: 16),
|
|
FluxTextField(
|
|
controller: phoneController,
|
|
label: "Telefono",
|
|
icon: Icons.phone,
|
|
),
|
|
const SizedBox(height: 24),
|
|
const Text(
|
|
"Assegna ai Negozi",
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// --- SELETTORE NEGOZI (CHIPS) ---
|
|
// Qui usiamo il BlocBuilder per i negozi, ma il setModalState per il refresh
|
|
BlocBuilder<StoreCubit, StoreState>(
|
|
builder: (context, storeState) {
|
|
if (storeState.status == StoreStatus.loading) {
|
|
return const CircularProgressIndicator();
|
|
}
|
|
return Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: storeState.stores.map((store) {
|
|
final isSelected = tempSelectedStores.contains(
|
|
store.id,
|
|
);
|
|
return FilterChip(
|
|
label: Text(store.nome),
|
|
selected: isSelected,
|
|
onSelected: (selected) {
|
|
// IMPORTANTE: setModalState aggiorna l'UI del BottomSheet
|
|
setModalState(() {
|
|
if (selected) {
|
|
tempSelectedStores.add(store.id!);
|
|
} else {
|
|
tempSelectedStores.remove(store.id);
|
|
}
|
|
});
|
|
},
|
|
selectedColor: context.accent.withValues(
|
|
alpha: 0.2,
|
|
),
|
|
checkmarkColor: context.accent,
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
final companyId = context
|
|
.read<SessionBloc>()
|
|
.state
|
|
.company!
|
|
.id;
|
|
|
|
final updatedMember = StaffMemberModel(
|
|
id: member?.id,
|
|
name: nameController.text,
|
|
email: emailController.text,
|
|
phone: phoneController.text,
|
|
companyId: companyId,
|
|
);
|
|
|
|
// Chiamiamo il metodo atomico nel Cubit
|
|
context.read<StaffCubit>().saveStaffWithStores(
|
|
member: updatedMember,
|
|
selectedStoreIds: tempSelectedStores,
|
|
);
|
|
|
|
Navigator.pop(context);
|
|
},
|
|
child: const Text("SALVA COLLABORATORE"),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.people_outline, size: 64, color: context.secondaryText),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
"Nessun membro trovato",
|
|
style: TextStyle(color: context.secondaryText),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|