refinements

This commit is contained in:
2026-05-25 12:49:04 +02:00
parent aad9a991c2
commit 9b5d19b926
13 changed files with 609 additions and 529 deletions

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.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/blocs/staff_cubit.dart';
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
import 'package:get_it/get_it.dart';
@@ -17,18 +17,16 @@ class StaffScreen extends StatefulWidget {
class _StaffScreenState extends State<StaffScreen> {
String? _selectedStoreId;
bool _showAllCompanyStaff = true; // Partiamo con la vista globale
bool _showAllCompanyStaff = true;
@override
void initState() {
super.initState();
// Carichiamo subito tutto
context.read<StaffCubit>().loadAllStaff();
}
@override
Widget build(BuildContext context) {
// 1. Peschiamo chi siamo noi e che poteri abbiamo
final myRole = context
.read<SessionCubit>()
.state
@@ -36,12 +34,12 @@ class _StaffScreenState extends State<StaffScreen> {
?.systemRole;
final canManageStaff =
myRole == SystemRole.admin || myRole == SystemRole.manager;
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(
@@ -66,7 +64,7 @@ class _StaffScreenState extends State<StaffScreen> {
},
child: Column(
children: [
// --- BARRA FILTRO NEGOZIO (Visibile solo se non 'Tutta l'Azienda') ---
// BARRA FILTRO NEGOZIO
AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: _showAllCompanyStaff ? 0 : 80,
@@ -75,7 +73,7 @@ class _StaffScreenState extends State<StaffScreen> {
: _buildStoreSelector(),
),
// --- LISTA PERSONALE ---
// LISTA PERSONALE
Expanded(
child: BlocBuilder<StaffCubit, StaffState>(
builder: (context, state) {
@@ -87,17 +85,14 @@ class _StaffScreenState extends State<StaffScreen> {
return const Center(child: CircularProgressIndicator());
}
if (list.isEmpty) {
return _buildEmptyState();
}
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);
return _buildStaffCard(list[index]);
},
);
},
@@ -118,7 +113,6 @@ class _StaffScreenState extends State<StaffScreen> {
Widget _buildStoreSelector() {
return BlocBuilder<StoreCubit, StoreState>(
// Assumendo tu abbia uno StoreCubit
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -146,6 +140,8 @@ class _StaffScreenState extends State<StaffScreen> {
?.systemRole;
final canManageStaff =
myRole == SystemRole.admin || myRole == SystemRole.manager;
final hasEmail = member.email != null && member.email!.trim().isNotEmpty;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
@@ -156,7 +152,10 @@ class _StaffScreenState extends State<StaffScreen> {
contentPadding: const EdgeInsets.all(12),
leading: CircleAvatar(
backgroundColor: context.accent.withValues(alpha: 0.1),
child: Text(member.name[0], style: TextStyle(color: context.accent)),
child: Text(
member.name[0].toUpperCase(),
style: TextStyle(color: context.accent),
),
),
title: Text(
member.name,
@@ -165,55 +164,65 @@ class _StaffScreenState extends State<StaffScreen> {
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (member.email != null && member.email!.isNotEmpty)
Text(member.email!),
if (hasEmail) Text(member.email!),
Text(
member.phoneNumber != null && member.phoneNumber!.isNotEmpty
member.phoneNumber != null &&
member.phoneNumber!.trim().isNotEmpty
? member.phoneNumber!
: "Nessun telefono",
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (member.jobTitle != null && member.jobTitle!.isNotEmpty) ...[
Text('Qualifica: ${member.jobTitle!}'),
const SizedBox(width: 8),
],
if (canManageStaff) ...[
const SizedBox(width: 8),
if (!member.hasJoined)
ElevatedButton.icon(
icon: const Icon(Icons.send),
label: const Text("Re-invia Invito (In Attesa)"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
onPressed: () {
// Chiama la funzione di reset password mascherata da invito
context.read<StaffCubit>().resetPasswordOrResendInviteLink(
member.email!,
);
},
)
else
OutlinedButton.icon(
icon: const Icon(Icons.lock_reset),
label: const Text("Invia Reset Password"),
onPressed: () {
// Chiama LA STESSA IDENTICA FUNZIONE!
context.read<StaffCubit>().resetPasswordOrResendInviteLink(
member.email!,
);
},
if (member.jobTitle != null &&
member.jobTitle!.trim().isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'Qualifica: ${member.jobTitle!}',
style: TextStyle(
color: context.accent,
fontWeight: FontWeight.w500,
),
),
],
],
),
// MODIFICA UX: Menu a tendina per le azioni (Salva spazio e previene overflow)
trailing: canManageStaff && hasEmail
? PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'invite_reset') {
context.read<StaffCubit>().resetPasswordOrResendInviteLink(
member.email!,
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Operazione richiesta, controlla l\'email!',
),
),
);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'invite_reset',
child: Row(
children: [
Icon(
!member.hasJoined ? Icons.send : Icons.lock_reset,
size: 20,
),
const SizedBox(width: 12),
Text(
!member.hasJoined
? "Re-invia Invito"
: "Reset Password",
),
],
),
),
],
)
: null,
onTap: () =>
canManageStaff ? _openStaffForm(context, member: member) : null,
),
@@ -226,7 +235,6 @@ class _StaffScreenState extends State<StaffScreen> {
final phoneController = TextEditingController(text: member?.phoneNumber);
final jobTitleController = TextEditingController(text: member?.jobTitle);
// Variabili di stato per il BottomSheet
SystemRole selectedRole = member?.systemRole ?? SystemRole.user;
List<String> tempSelectedStores =
context
@@ -263,7 +271,7 @@ class _StaffScreenState extends State<StaffScreen> {
children: [
Text(
member == null
? "Invita Collaboratore" // Cambiato il titolo per chiarezza!
? "Invita Collaboratore"
: "Modifica Collaboratore",
style: const TextStyle(
fontSize: 20,
@@ -279,16 +287,13 @@ class _StaffScreenState extends State<StaffScreen> {
),
const SizedBox(height: 16),
// Reso visivamente obbligatorio se è un nuovo utente
FluxTextField(
controller: emailController,
label: member == null
? "Email (Obbligatoria per invito)*"
: "Email",
icon: Icons.email,
enabled:
member ==
null, // UX: Di solito l'email non si cambia dopo l'invito
enabled: member == null,
),
const SizedBox(height: 16),
@@ -299,7 +304,6 @@ class _StaffScreenState extends State<StaffScreen> {
),
const SizedBox(height: 16),
// --- NOVITÀ: SCELTA DEL RUOLO E MANSIONE ---
Row(
children: [
Expanded(
@@ -317,9 +321,8 @@ class _StaffScreenState extends State<StaffScreen> {
);
}).toList(),
onChanged: (val) {
if (val != null) {
if (val != null)
setModalState(() => selectedRole = val);
}
},
),
),
@@ -382,7 +385,6 @@ class _StaffScreenState extends State<StaffScreen> {
height: 50,
child: ElevatedButton(
onPressed: () {
// Validazione di base per i nuovi inviti
if (member == null &&
emailController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -396,7 +398,7 @@ class _StaffScreenState extends State<StaffScreen> {
}
final updatedMember = StaffMemberModel(
id: member?.id, // Sarà null se è nuovo
id: member?.id,
name: nameController.text.trim(),
email: emailController.text.trim(),
phoneNumber: phoneController.text.trim(),
@@ -410,17 +412,12 @@ class _StaffScreenState extends State<StaffScreen> {
userId: GetIt.I.get<SessionCubit>().state.user!.id,
);
// --- IL BIVIO LOGICO MAGICO ---
if (member == null) {
// 1. UTENTE NUOVO -> Chiamiamo la Edge Function
// (Nota: Per i negozi, potresti dover fare una logica a parte nel Cubit
// perché l'ID del database viene generato DOPO che l'Edge Function ha finito)
context.read<StaffCubit>().inviteStaffMember(
member: updatedMember,
selectedStoreIds: tempSelectedStores,
);
} else {
// 2. UTENTE ESISTENTE -> Modifica classica
context.read<StaffCubit>().saveStaffWithStores(
member: updatedMember,
selectedStoreIds: tempSelectedStores,
@@ -434,32 +431,6 @@ class _StaffScreenState extends State<StaffScreen> {
),
),
),
/* const SizedBox(height: 16),
if (!member!.hasJoined)
ElevatedButton.icon(
icon: const Icon(Icons.send),
label: const Text("Re-invia Invito (In Attesa)"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
onPressed: () {
// Chiama la funzione di reset password mascherata da invito
context
.read<StaffCubit>()
.resetPasswordOrResendInviteLink(member.email!);
},
)
else
OutlinedButton.icon(
icon: const Icon(Icons.lock_reset),
label: const Text("Invia Reset Password"),
onPressed: () {
// Chiama LA STESSA IDENTICA FUNZIONE!
context
.read<StaffCubit>()
.resetPasswordOrResendInviteLink(member.email!);
},
), */
],
),
),

View File

@@ -54,6 +54,8 @@ class _StoreCardState extends State<StoreCard> {
),
title: Text(
widget.store.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
@@ -65,44 +67,43 @@ class _StoreCardState extends State<StoreCard> {
// context.read<StoreBloc>().add(ToggleStoreStatus(store.id, val));
},
),
onTap: () => _openStoreForm(context, store: widget.store),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Mostra quanti dipendenti ci sono (usando lo StaffCubit)
BlocBuilder<StoreCubit, StoreState>(
builder: (context, storeState) {
final staffCount =
storeState.staffByStore[widget.store.id]?.length ?? 0;
return Row(
children: [
ActionChip(
avatar: const Icon(Icons.people, size: 16),
label: Text("$staffCount Dipendenti"),
onPressed: () => _manageStoreStaff(widget.store),
),
const SizedBox(width: 16),
ActionChip(
avatar: const Icon(Icons.handshake, size: 16),
label: Text(
"${widget.store.associatedProviders.length} Providers",
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Mostra quanti dipendenti ci sono (usando lo StaffCubit)
BlocBuilder<StoreCubit, StoreState>(
builder: (context, storeState) {
final staffCount =
storeState.staffByStore[widget.store.id]?.length ?? 0;
return Row(
children: [
ActionChip(
avatar: const Icon(Icons.people, size: 16),
label: Text("$staffCount Dipendenti"),
onPressed: () => _manageStoreStaff(widget.store),
),
onPressed: () => _manageStoreProviders(widget.store),
),
],
);
},
),
const SizedBox(width: 16),
TextButton.icon(
onPressed: () => _openStoreForm(context, store: widget.store),
icon: const Icon(Icons.edit, size: 18),
label: const Text("Modifica"),
),
],
const SizedBox(width: 16),
ActionChip(
avatar: const Icon(Icons.handshake, size: 16),
label: Text(
"${widget.store.associatedProviders.length} Providers",
),
onPressed: () =>
_manageStoreProviders(widget.store),
),
],
);
},
),
],
),
),
),
],