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

@@ -29,6 +29,7 @@ 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/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/ui/stores_screen.dart'; import 'package:flux/features/master_data/store/ui/stores_screen.dart';
import 'package:flux/features/notes/models/note_model.dart'; import 'package:flux/features/notes/models/note_model.dart';
import 'package:flux/features/notes/ui/notes_form_screen.dart'; import 'package:flux/features/notes/ui/notes_form_screen.dart';
@@ -171,7 +172,11 @@ class AppRouter {
path: path:
'stores', // Sistemata l'inversione path/name -> /master-data/stores 'stores', // Sistemata l'inversione path/name -> /master-data/stores
name: Routes.stores, name: Routes.stores,
builder: (context, state) => const StoresScreen(), builder: (context, state) {
context.read<ProviderListCubit>().loadAllProviders();
context.read<StoreCubit>().loadStores();
return const StoresScreen();
},
), ),
GoRoute( GoRoute(
path: 'company-settings', // -> /master-data/company-settings path: 'company-settings', // -> /master-data/company-settings

View File

@@ -21,6 +21,7 @@ class FluxTextField extends StatefulWidget {
final TextCapitalization? textCapitalization; final TextCapitalization? textCapitalization;
final bool? autocorrect; final bool? autocorrect;
final bool? enabled; final bool? enabled;
final Iterable<String>? autofillHints;
const FluxTextField({ const FluxTextField({
super.key, // Usiamo super.key per Flutter moderno super.key, // Usiamo super.key per Flutter moderno
@@ -41,6 +42,7 @@ class FluxTextField extends StatefulWidget {
this.textCapitalization, this.textCapitalization,
this.autocorrect, this.autocorrect,
this.enabled = true, this.enabled = true,
this.autofillHints,
}); });
@override @override
@@ -118,6 +120,7 @@ class _FluxTextFieldState extends State<FluxTextField> {
textCapitalization: widget.textCapitalization ?? TextCapitalization.none, textCapitalization: widget.textCapitalization ?? TextCapitalization.none,
enabled: widget.enabled, enabled: widget.enabled,
autofillHints: widget.autofillHints,
); );
} }
} }

View File

@@ -16,7 +16,8 @@ class AuthCubit extends Cubit<AuthState> {
emit(state.copyWith(isLoginMode: !state.isLoginMode)); emit(state.copyWith(isLoginMode: !state.isLoginMode));
} }
Future<void> submitAuth(String email, String password) async { Future<bool> submitAuth(String email, String password) async {
// <-- Modificato in bool
// Partiamo puliti: via vecchi messaggi ed errori // Partiamo puliti: via vecchi messaggi ed errori
emit(state.copyWith(status: AuthStatus.loading)); emit(state.copyWith(status: AuthStatus.loading));
@@ -27,9 +28,17 @@ class AuthCubit extends Cubit<AuthState> {
email: email, email: email,
password: password, password: password,
); );
// NESSUN EMIT DI SUCCESS!
// Supabase lancerà l'evento 'signedIn', il SessionCubit lo catturerà // Il login è andato a buon fine!
// e il GoRouter ci cambierà pagina. Noi stiamo a guardare il caricamento. emit(
AuthState(
status: AuthStatus.initial,
isLoginMode: true,
errorMessage: null,
infoMessage: null,
),
);
return true;
} else { } else {
// --- LOGICA SIGNUP --- // --- LOGICA SIGNUP ---
final AuthResponse res = await _supabase.auth.signUp( final AuthResponse res = await _supabase.auth.signUp(
@@ -38,7 +47,6 @@ class AuthCubit extends Cubit<AuthState> {
); );
if (res.session == null) { if (res.session == null) {
// Caso: Conferma Email attivata su Supabase
emit( emit(
state.copyWith( state.copyWith(
status: AuthStatus.initial, status: AuthStatus.initial,
@@ -48,16 +56,24 @@ class AuthCubit extends Cubit<AuthState> {
), ),
); );
} else { } else {
// Caso: Autologin post-registrazione (Conferma email disattivata)
// 1. Fermiamo il frullino!
emit(state.copyWith(status: AuthStatus.initial)); emit(state.copyWith(status: AuthStatus.initial));
// 2. Svegliamo il SessionCubit!
GetIt.I<SessionCubit>().initializeSession(); GetIt.I<SessionCubit>().initializeSession();
} }
// Se non è null, ha fatto il login automatico. Stessa cosa di sopra, ci pensa il SessionCubit.
// Anche la registrazione è andata a buon fine!
emit(
AuthState(
status: AuthStatus.initial,
isLoginMode: true,
errorMessage: null,
infoMessage: null,
),
);
return true;
} }
} on AuthException catch (e) { } on AuthException catch (e) {
emit(state.copyWith(status: AuthStatus.failure, errorMessage: e.message)); emit(state.copyWith(status: AuthStatus.failure, errorMessage: e.message));
return false; // <-- Il login è fallito
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
@@ -65,6 +81,7 @@ class AuthCubit extends Cubit<AuthState> {
errorMessage: "Errore imprevisto: $e", errorMessage: "Errore imprevisto: $e",
), ),
); );
return false; // <-- Il login è fallito
} }
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/extensions.dart';
@@ -24,14 +25,18 @@ class _AuthScreenState extends State<AuthScreen> {
super.dispose(); super.dispose();
} }
void _submit() { void _submit() async {
// Chiudiamo la tastiera per fare pulizia a schermo // Chiudiamo la tastiera per fare pulizia a schermo
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
context.read<AuthCubit>().submitAuth( final isSuccess = await context.read<AuthCubit>().submitAuth(
_emailController.text.trim(), _emailController.text.trim(),
_passwordController.text.trim(), _passwordController.text.trim(),
); );
if (isSuccess) {
TextInput.finishAutofillContext();
}
} }
@override @override
@@ -69,125 +74,133 @@ class _AuthScreenState extends State<AuthScreen> {
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column( child: AutofillGroup(
mainAxisAlignment: MainAxisAlignment.center, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
// --- LOGO FLUX --- children: [
const FluxLogoAuto(height: 80), // --- LOGO FLUX ---
const SizedBox(height: 60), const FluxLogoAuto(height: 80),
const SizedBox(height: 60),
// --- TITOLO DINAMICO --- // --- TITOLO DINAMICO ---
Text( Text(
state.isLoginMode state.isLoginMode
? context.l10n.authScreenWelcomeBack ? context.l10n.authScreenWelcomeBack
: context.l10n.authScreenCreateAccount, : context.l10n.authScreenCreateAccount,
style: TextStyle( style: TextStyle(
color: context.primaryText, color: context.primaryText,
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
letterSpacing: 1.5, letterSpacing: 1.5,
),
), ),
), const SizedBox(height: 8),
const SizedBox(height: 8), Text(
Text( state.isLoginMode
state.isLoginMode ? context.l10n.authScreenLoginToManageYourBusiness
? context.l10n.authScreenLoginToManageYourBusiness : context
: context .l10n
.l10n .authScreenStartTodayToDigitalizeYourStore,
.authScreenStartTodayToDigitalizeYourStore, textAlign: TextAlign.center,
textAlign: TextAlign.center, style: TextStyle(color: context.secondaryText),
style: TextStyle(color: context.secondaryText), ),
), const SizedBox(height: 40),
const SizedBox(height: 40),
// --- CAMPI INPUT --- // --- CAMPI INPUT ---
FluxTextField( FluxTextField(
label: context.l10n.authScreenBusinessEmail, label: context.l10n.authScreenBusinessEmail,
icon: Icons.email_outlined, icon: Icons.email_outlined,
controller: _emailController, controller: _emailController,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
), autofillHints: const [
const SizedBox(height: 20), AutofillHints.email,
FluxTextField( AutofillHints.username,
label: 'Password', ],
icon: Icons.lock_outline, ),
isPassword: true, // Magia del FluxTextField! const SizedBox(height: 20),
controller: _passwordController, FluxTextField(
onSubmitted: (_) => label: 'Password',
_submit(), // Se lo supporti nel tuo widget custom icon: Icons.lock_outline,
), isPassword: true, // Magia del FluxTextField!
controller: _passwordController,
autofillHints: const [AutofillHints.password],
onSubmitted: (_) =>
_submit(), // Se lo supporti nel tuo widget custom
),
const SizedBox(height: 40), const SizedBox(height: 40),
// --- BOTTONE PRINCIPALE --- // --- BOTTONE PRINCIPALE ---
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
height: 56, height: 56,
child: ElevatedButton( child: ElevatedButton(
onPressed: isLoading ? null : _submit, onPressed: isLoading ? null : _submit,
child: isLoading child: isLoading
? const SizedBox( ? const SizedBox(
height: 24, height: 24,
width: 24, width: 24,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
color: Colors.white, color: Colors.white,
),
)
: Text(
state.isLoginMode
? context.l10n.authScreenLogin
: context.l10n.authScreenSignUp,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
), ),
) ),
: Text( ),
state.isLoginMode
? context.l10n.authScreenLogin // --- SWITCH LOGIN/SIGNUP ---
: context.l10n.authScreenSignUp, const SizedBox(height: 24),
style: const TextStyle( TextButton(
onPressed: isLoading
? null
: () => context.read<AuthCubit>().toggleMode(),
child: RichText(
text: TextSpan(
text: state.isLoginMode
? context.l10n.authScreenDontHaveAccount
: context.l10n.authScreenAlreadyHaveAccount,
style: TextStyle(color: context.secondaryText),
children: [
TextSpan(
text: state.isLoginMode
? context.l10n.authScreenSignUp
: context.l10n.authScreenLogin,
style: TextStyle(
color: context.accent,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ],
),
// --- SWITCH LOGIN/SIGNUP ---
const SizedBox(height: 24),
TextButton(
onPressed: isLoading
? null
: () => context.read<AuthCubit>().toggleMode(),
child: RichText(
text: TextSpan(
text: state.isLoginMode
? context.l10n.authScreenDontHaveAccount
: context.l10n.authScreenAlreadyHaveAccount,
style: TextStyle(color: context.secondaryText),
children: [
TextSpan(
text: state.isLoginMode
? context.l10n.authScreenSignUp
: context.l10n.authScreenLogin,
style: TextStyle(
color: context.accent,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
if (state.isLoginMode) ...[
const SizedBox(height: 24),
TextButton(
onPressed: () => context
.read<AuthCubit>()
.requestPasswordReset(_emailController.text.trim()),
child: Text(
context.l10n.authScreenForgotPassword,
style: TextStyle(
color: context.accent,
fontWeight: FontWeight.bold,
), ),
), ),
), ),
if (state.isLoginMode) ...[
const SizedBox(height: 24),
TextButton(
onPressed: () =>
context.read<AuthCubit>().requestPasswordReset(
_emailController.text.trim(),
),
child: Text(
context.l10n.authScreenForgotPassword,
style: TextStyle(
color: context.accent,
fontWeight: FontWeight.bold,
),
),
),
],
], ],
], ),
), ),
), ),
), ),

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/blocs/session/session_cubit.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/widgets/flux_text_field.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/staff/models/staff_member_model.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -17,18 +17,16 @@ class StaffScreen extends StatefulWidget {
class _StaffScreenState extends State<StaffScreen> { class _StaffScreenState extends State<StaffScreen> {
String? _selectedStoreId; String? _selectedStoreId;
bool _showAllCompanyStaff = true; // Partiamo con la vista globale bool _showAllCompanyStaff = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Carichiamo subito tutto
context.read<StaffCubit>().loadAllStaff(); context.read<StaffCubit>().loadAllStaff();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 1. Peschiamo chi siamo noi e che poteri abbiamo
final myRole = context final myRole = context
.read<SessionCubit>() .read<SessionCubit>()
.state .state
@@ -36,12 +34,12 @@ class _StaffScreenState extends State<StaffScreen> {
?.systemRole; ?.systemRole;
final canManageStaff = final canManageStaff =
myRole == SystemRole.admin || myRole == SystemRole.manager; myRole == SystemRole.admin || myRole == SystemRole.manager;
return Scaffold( return Scaffold(
backgroundColor: context.background, backgroundColor: context.background,
appBar: AppBar( appBar: AppBar(
title: const Text("Anagrafica Personale"), title: const Text("Anagrafica Personale"),
actions: [ actions: [
// Toggle per vista Azienda / Negozio
Padding( Padding(
padding: const EdgeInsets.only(right: 16), padding: const EdgeInsets.only(right: 16),
child: FilterChip( child: FilterChip(
@@ -66,7 +64,7 @@ class _StaffScreenState extends State<StaffScreen> {
}, },
child: Column( child: Column(
children: [ children: [
// --- BARRA FILTRO NEGOZIO (Visibile solo se non 'Tutta l'Azienda') --- // BARRA FILTRO NEGOZIO
AnimatedContainer( AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
height: _showAllCompanyStaff ? 0 : 80, height: _showAllCompanyStaff ? 0 : 80,
@@ -75,7 +73,7 @@ class _StaffScreenState extends State<StaffScreen> {
: _buildStoreSelector(), : _buildStoreSelector(),
), ),
// --- LISTA PERSONALE --- // LISTA PERSONALE
Expanded( Expanded(
child: BlocBuilder<StaffCubit, StaffState>( child: BlocBuilder<StaffCubit, StaffState>(
builder: (context, state) { builder: (context, state) {
@@ -87,17 +85,14 @@ class _StaffScreenState extends State<StaffScreen> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (list.isEmpty) { if (list.isEmpty) return _buildEmptyState();
return _buildEmptyState();
}
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: list.length, itemCount: list.length,
separatorBuilder: (_, _) => const SizedBox(height: 12), separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final member = list[index]; return _buildStaffCard(list[index]);
return _buildStaffCard(member);
}, },
); );
}, },
@@ -118,7 +113,6 @@ class _StaffScreenState extends State<StaffScreen> {
Widget _buildStoreSelector() { Widget _buildStoreSelector() {
return BlocBuilder<StoreCubit, StoreState>( return BlocBuilder<StoreCubit, StoreState>(
// Assumendo tu abbia uno StoreCubit
builder: (context, state) { builder: (context, state) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -146,6 +140,8 @@ class _StaffScreenState extends State<StaffScreen> {
?.systemRole; ?.systemRole;
final canManageStaff = final canManageStaff =
myRole == SystemRole.admin || myRole == SystemRole.manager; myRole == SystemRole.admin || myRole == SystemRole.manager;
final hasEmail = member.email != null && member.email!.trim().isNotEmpty;
return Card( return Card(
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -156,7 +152,10 @@ class _StaffScreenState extends State<StaffScreen> {
contentPadding: const EdgeInsets.all(12), contentPadding: const EdgeInsets.all(12),
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: context.accent.withValues(alpha: 0.1), 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( title: Text(
member.name, member.name,
@@ -165,55 +164,65 @@ class _StaffScreenState extends State<StaffScreen> {
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (member.email != null && member.email!.isNotEmpty) if (hasEmail) Text(member.email!),
Text(member.email!),
Text( Text(
member.phoneNumber != null && member.phoneNumber!.isNotEmpty member.phoneNumber != null &&
member.phoneNumber!.trim().isNotEmpty
? member.phoneNumber! ? member.phoneNumber!
: "Nessun telefono", : "Nessun telefono",
), ),
], if (member.jobTitle != null &&
), member.jobTitle!.trim().isNotEmpty) ...[
trailing: Row( const SizedBox(height: 4),
mainAxisSize: MainAxisSize.min, Text(
children: [ 'Qualifica: ${member.jobTitle!}',
if (member.jobTitle != null && member.jobTitle!.isNotEmpty) ...[ style: TextStyle(
Text('Qualifica: ${member.jobTitle!}'), color: context.accent,
const SizedBox(width: 8), fontWeight: FontWeight.w500,
],
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!,
);
},
), ),
),
], ],
], ],
), ),
// 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: () => onTap: () =>
canManageStaff ? _openStaffForm(context, member: member) : null, canManageStaff ? _openStaffForm(context, member: member) : null,
), ),
@@ -226,7 +235,6 @@ class _StaffScreenState extends State<StaffScreen> {
final phoneController = TextEditingController(text: member?.phoneNumber); final phoneController = TextEditingController(text: member?.phoneNumber);
final jobTitleController = TextEditingController(text: member?.jobTitle); final jobTitleController = TextEditingController(text: member?.jobTitle);
// Variabili di stato per il BottomSheet
SystemRole selectedRole = member?.systemRole ?? SystemRole.user; SystemRole selectedRole = member?.systemRole ?? SystemRole.user;
List<String> tempSelectedStores = List<String> tempSelectedStores =
context context
@@ -263,7 +271,7 @@ class _StaffScreenState extends State<StaffScreen> {
children: [ children: [
Text( Text(
member == null member == null
? "Invita Collaboratore" // Cambiato il titolo per chiarezza! ? "Invita Collaboratore"
: "Modifica Collaboratore", : "Modifica Collaboratore",
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
@@ -279,16 +287,13 @@ class _StaffScreenState extends State<StaffScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Reso visivamente obbligatorio se è un nuovo utente
FluxTextField( FluxTextField(
controller: emailController, controller: emailController,
label: member == null label: member == null
? "Email (Obbligatoria per invito)*" ? "Email (Obbligatoria per invito)*"
: "Email", : "Email",
icon: Icons.email, icon: Icons.email,
enabled: enabled: member == null,
member ==
null, // UX: Di solito l'email non si cambia dopo l'invito
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -299,7 +304,6 @@ class _StaffScreenState extends State<StaffScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// --- NOVITÀ: SCELTA DEL RUOLO E MANSIONE ---
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -317,9 +321,8 @@ class _StaffScreenState extends State<StaffScreen> {
); );
}).toList(), }).toList(),
onChanged: (val) { onChanged: (val) {
if (val != null) { if (val != null)
setModalState(() => selectedRole = val); setModalState(() => selectedRole = val);
}
}, },
), ),
), ),
@@ -382,7 +385,6 @@ class _StaffScreenState extends State<StaffScreen> {
height: 50, height: 50,
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
// Validazione di base per i nuovi inviti
if (member == null && if (member == null &&
emailController.text.trim().isEmpty) { emailController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -396,7 +398,7 @@ class _StaffScreenState extends State<StaffScreen> {
} }
final updatedMember = StaffMemberModel( final updatedMember = StaffMemberModel(
id: member?.id, // Sarà null se è nuovo id: member?.id,
name: nameController.text.trim(), name: nameController.text.trim(),
email: emailController.text.trim(), email: emailController.text.trim(),
phoneNumber: phoneController.text.trim(), phoneNumber: phoneController.text.trim(),
@@ -410,17 +412,12 @@ class _StaffScreenState extends State<StaffScreen> {
userId: GetIt.I.get<SessionCubit>().state.user!.id, userId: GetIt.I.get<SessionCubit>().state.user!.id,
); );
// --- IL BIVIO LOGICO MAGICO ---
if (member == null) { 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( context.read<StaffCubit>().inviteStaffMember(
member: updatedMember, member: updatedMember,
selectedStoreIds: tempSelectedStores, selectedStoreIds: tempSelectedStores,
); );
} else { } else {
// 2. UTENTE ESISTENTE -> Modifica classica
context.read<StaffCubit>().saveStaffWithStores( context.read<StaffCubit>().saveStaffWithStores(
member: updatedMember, member: updatedMember,
selectedStoreIds: tempSelectedStores, 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( title: Text(
widget.store.name, widget.store.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
subtitle: Text( subtitle: Text(
@@ -65,44 +67,43 @@ class _StoreCardState extends State<StoreCard> {
// context.read<StoreBloc>().add(ToggleStoreStatus(store.id, val)); // context.read<StoreBloc>().add(ToggleStoreStatus(store.id, val));
}, },
), ),
onTap: () => _openStoreForm(context, store: widget.store),
), ),
const Divider(height: 1), const Divider(height: 1),
Padding( Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(8),
child: Row( child: SingleChildScrollView(
mainAxisAlignment: MainAxisAlignment.spaceBetween, scrollDirection: Axis.horizontal,
children: [ child: Row(
// Mostra quanti dipendenti ci sono (usando lo StaffCubit) mainAxisAlignment: MainAxisAlignment.spaceBetween,
BlocBuilder<StoreCubit, StoreState>( children: [
builder: (context, storeState) { // Mostra quanti dipendenti ci sono (usando lo StaffCubit)
final staffCount = BlocBuilder<StoreCubit, StoreState>(
storeState.staffByStore[widget.store.id]?.length ?? 0; builder: (context, storeState) {
return Row( final staffCount =
children: [ storeState.staffByStore[widget.store.id]?.length ?? 0;
ActionChip( return Row(
avatar: const Icon(Icons.people, size: 16), children: [
label: Text("$staffCount Dipendenti"), ActionChip(
onPressed: () => _manageStoreStaff(widget.store), avatar: const Icon(Icons.people, size: 16),
), label: Text("$staffCount Dipendenti"),
const SizedBox(width: 16), onPressed: () => _manageStoreStaff(widget.store),
ActionChip(
avatar: const Icon(Icons.handshake, size: 16),
label: Text(
"${widget.store.associatedProviders.length} Providers",
), ),
onPressed: () => _manageStoreProviders(widget.store), const SizedBox(width: 16),
), ActionChip(
], avatar: const Icon(Icons.handshake, size: 16),
); label: Text(
}, "${widget.store.associatedProviders.length} Providers",
), ),
const SizedBox(width: 16), onPressed: () =>
TextButton.icon( _manageStoreProviders(widget.store),
onPressed: () => _openStoreForm(context, store: widget.store), ),
icon: const Icon(Icons.edit, size: 18), ],
label: const Text("Modifica"), );
), },
], ),
],
),
), ),
), ),
], ],

View File

@@ -248,89 +248,121 @@ class _NoteFormScreenState extends State<NoteFormScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// --- HEADER DEL POST-IT (Tavolozza + Azioni) --- // --- HEADER DEL POST-IT (Tavolozza + Azioni) ---
Row( LayoutBuilder(
children: [ builder: (context, constraints) {
// Tavolozza Colori // 1. Capiamo quanto spazio reale ha la finestra in questo momento
Expanded( final isNarrow = constraints.maxWidth < 500;
child: SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _noteColors.length,
itemBuilder: (context, index) {
final colorHex = _noteColors[index];
final isSelected = _selectedColor == colorHex;
final c = Color(
int.parse(
'FF${colorHex.replaceAll('#', '')}',
radix: 16,
),
);
return GestureDetector( // 2. Adattiamo la dimensione dei cerchi
onTap: () { final double circleSize = isNarrow ? 32.0 : 40.0;
setState(() => _selectedColor = colorHex);
_triggerAutoSave(); // -- PREPARIAMO IL BLOCCO COLORI --
}, final colorPalette = SizedBox(
child: Container( height: circleSize,
margin: const EdgeInsets.only(right: 12), child: ListView.builder(
width: 40, scrollDirection: Axis.horizontal,
decoration: BoxDecoration( itemCount: _noteColors.length,
color: c, itemBuilder: (context, index) {
shape: BoxShape.circle, final colorHex = _noteColors[index];
border: Border.all( final isSelected = _selectedColor == colorHex;
color: isSelected final c = Color(
? Colors.black54 int.parse(
: Colors.black12, 'FF${colorHex.replaceAll('#', '')}',
width: isSelected ? 3 : 1, radix: 16,
), ),
);
return GestureDetector(
onTap: () {
setState(() => _selectedColor = colorHex);
_triggerAutoSave();
},
child: Container(
margin: const EdgeInsets.only(right: 12),
width: circleSize,
decoration: BoxDecoration(
color: c,
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? Colors.black54
: Colors.black12,
width: isSelected ? 3 : 1,
), ),
child: isSelected
? const Icon(
Icons.check,
color: Colors.black54,
size: 20,
)
: null,
), ),
); child: isSelected
? Icon(
Icons.check,
color: Colors.black54,
size: isNarrow ? 16 : 20,
)
: null,
),
);
},
),
);
// -- PREPARIAMO IL BLOCCO AZIONI --
final actionButtons = Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(
Icons.delete_outline,
color: Colors.black87,
),
tooltip: 'Elimina',
onPressed: _deleteNote,
),
IconButton(
icon: Icon(
_isPinned
? Icons.push_pin
: Icons.push_pin_outlined,
color: Colors.black87,
),
tooltip: _isPinned
? 'Rimuovi in alto'
: 'Fissa in alto',
onPressed: () {
setState(() => _isPinned = !_isPinned);
_triggerAutoSave();
}, },
), ),
), IconButton(
), icon: const Icon(
const SizedBox(width: 16), Icons.ios_share,
color: Colors.black87,
),
tooltip: 'Esporta',
onPressed: _exportNote,
),
],
);
// Azioni spostate dentro la nota! // 3. DECIDIAMO IL LAYOUT FINALE IN BASE ALLO SPAZIO REALE
IconButton( if (isNarrow) {
icon: const Icon( return Column(
Icons.delete_outline, crossAxisAlignment: CrossAxisAlignment.end,
color: Colors.black87, children: [
), actionButtons,
tooltip: 'Elimina',
onPressed: _deleteNote, const SizedBox(height: 8),
), colorPalette,
IconButton( ],
icon: Icon( );
_isPinned ? Icons.push_pin : Icons.push_pin_outlined, }
color: Colors.black87,
), // Layout "Largo" (Finestra intera)
tooltip: _isPinned return Row(
? 'Rimuovi in alto' children: [
: 'Fissa in alto', Expanded(child: colorPalette),
onPressed: () { const SizedBox(width: 16),
setState(() => _isPinned = !_isPinned); actionButtons,
_triggerAutoSave(); ],
}, );
), },
IconButton(
icon: const Icon(
Icons.ios_share,
color: Colors.black87,
),
tooltip: 'Esporta',
onPressed: _exportNote,
),
],
), ),
const SizedBox(height: 32), const SizedBox(height: 32),

View File

@@ -483,36 +483,39 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
icon: Icons.design_services, icon: Icons.design_services,
themeColor: Colors.deepOrange, themeColor: Colors.deepOrange,
children: [ children: [
Row( SingleChildScrollView(
children: [ scrollDirection: Axis.horizontal,
ChoiceChip( child: Row(
label: const Text('Privato (Domestico)'), children: [
selected: !state.operation.isBusiness, ChoiceChip(
selectedColor: Colors.blue.withValues(alpha: 0.2), label: const Text('Privato (Domestico)'),
checkmarkColor: Colors.blue.shade700, selected: !state.operation.isBusiness,
onSelected: (selected) { selectedColor: Colors.blue.withValues(alpha: 0.2),
if (selected) { checkmarkColor: Colors.blue.shade700,
context.read<OperationFormCubit>().updateFields( onSelected: (selected) {
isBusiness: false, if (selected) {
); context.read<OperationFormCubit>().updateFields(
} isBusiness: false,
}, );
), }
const SizedBox(width: 12), },
ChoiceChip( ),
label: const Text('Business (P.IVA)'), const SizedBox(width: 12),
selected: state.operation.isBusiness, ChoiceChip(
selectedColor: Colors.orange.withValues(alpha: 0.2), label: const Text('Business (P.IVA)'),
checkmarkColor: Colors.orange.shade700, selected: state.operation.isBusiness,
onSelected: (selected) { selectedColor: Colors.orange.withValues(alpha: 0.2),
if (selected) { checkmarkColor: Colors.orange.shade700,
context.read<OperationFormCubit>().updateFields( onSelected: (selected) {
isBusiness: true, if (selected) {
); context.read<OperationFormCubit>().updateFields(
} isBusiness: true,
}, );
), }
], },
),
],
),
), ),
const Divider(height: 32), const Divider(height: 32),
Wrap( Wrap(

View File

@@ -8,151 +8,163 @@ class DocumentSequenceSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final year = DateTime.now().year;
return BlocBuilder<DocumentSequenceCubit, DocumentSequenceState>( return BlocBuilder<DocumentSequenceCubit, DocumentSequenceState>(
builder: (context, state) { builder: (context, state) {
if (state.status == DocumentSequenceStatus.loading) { if (state.status == DocumentSequenceStatus.loading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return LayoutBuilder(
return Column( builder: ((context, constraints) {
crossAxisAlignment: CrossAxisAlignment.start, final isLargeScreen = constraints.maxWidth >= 600;
children: [ return _buildMainContent(
Padding( state: state,
padding: const EdgeInsets.symmetric(vertical: 16.0), isLargeScreen: isLargeScreen,
child: Text( context: context,
"Protocolli e Numerazione", );
style: Theme.of( }),
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
// Invece di mappare state.sequences, mappiamo i documenti supportati
...DocumentType.values.map((docType) {
// Cerchiamo se c'è già una configurazione nello stato per questo documento
final existingList = state.sequences
.where((s) => s.docType == docType.name)
.toList();
final existingSeq = existingList.isNotEmpty
? existingList.first
: null;
// Se esiste usiamo i suoi valori, altrimenti i default
final prefix = existingSeq?.prefix ?? docType.defaultPrefix;
final nextValue = existingSeq?.nextValue ?? 1;
// Anteprima dinamica (aggiornata a 4 zeri come nel DB!)
final preview =
"${prefix.isNotEmpty ? '$prefix-' : ''}$year-${nextValue.toString().padLeft(4, '0')}";
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
docType.label,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
initialValue: prefix,
decoration: const InputDecoration(
labelText: 'Prefisso',
hintText: 'es. TCK',
),
onChanged: (val) => context
.read<DocumentSequenceCubit>()
.updateLocalSequence(
docType.name,
prefix: val,
),
),
),
const SizedBox(width: 16),
Expanded(
flex: 3,
child: TextFormField(
initialValue: nextValue.toString(),
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Prossimo Numero',
),
onChanged: (val) => context
.read<DocumentSequenceCubit>()
.updateLocalSequence(
docType.name,
nextValue: int.tryParse(val) ?? 1,
),
),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors
.grey
.shade100, // Se hai un tema scuro potresti voler usare Theme.of(context).colorScheme.surfaceContainer
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(
Icons.visibility,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 8),
Text(
"Anteprima prossimo: ",
style: TextStyle(
color: Colors
.grey
.shade700, // Idem per la dark mode
fontSize: 12,
),
),
Text(
preview,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
),
],
),
),
],
),
),
);
}),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () =>
context.read<DocumentSequenceCubit>().saveSequences(),
icon: const Icon(Icons.save),
label: const Text("SALVA PROTOCOLLI"),
),
),
],
); );
}, },
); );
} }
Widget _buildMainContent({
required BuildContext context,
required DocumentSequenceState state,
required bool isLargeScreen,
}) {
final year = DateTime.now().year;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text(
"Protocolli e Numerazione",
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
),
// Invece di mappare state.sequences, mappiamo i documenti supportati
...DocumentType.values.map((docType) {
// Cerchiamo se c'è già una configurazione nello stato per questo documento
final existingList = state.sequences
.where((s) => s.docType == docType.name)
.toList();
final existingSeq = existingList.isNotEmpty
? existingList.first
: null;
// Se esiste usiamo i suoi valori, altrimenti i default
final prefix = existingSeq?.prefix ?? docType.defaultPrefix;
final nextValue = existingSeq?.nextValue ?? 1;
// Anteprima dinamica (aggiornata a 4 zeri come nel DB!)
final preview =
"${prefix.isNotEmpty ? '$prefix-' : ''}$year-${nextValue.toString().padLeft(4, '0')}";
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
docType.label,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
initialValue: prefix,
decoration: const InputDecoration(
labelText: 'Prefisso',
hintText: 'es. TCK',
),
onChanged: (val) => context
.read<DocumentSequenceCubit>()
.updateLocalSequence(docType.name, prefix: val),
),
),
const SizedBox(width: 16),
Expanded(
flex: 3,
child: TextFormField(
initialValue: nextValue.toString(),
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Prossimo Numero',
),
onChanged: (val) => context
.read<DocumentSequenceCubit>()
.updateLocalSequence(
docType.name,
nextValue: int.tryParse(val) ?? 1,
),
),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(
Icons.visibility,
size: 16,
color: Colors.grey,
),
const SizedBox(width: 8),
Text(
"Anteprima prossimo: ",
style: TextStyle(
color:
Colors.grey.shade700, // Idem per la dark mode
fontSize: 12,
),
),
Flexible(
child: Text(
preview,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
),
),
],
),
),
],
),
),
);
}),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () =>
context.read<DocumentSequenceCubit>().saveSequences(),
icon: const Icon(Icons.save),
label: const Text("SALVA PROTOCOLLI"),
),
),
],
);
}
} }

View File

@@ -54,9 +54,11 @@ class SettingsScreen extends StatelessWidget {
children: [ children: [
const Icon(Icons.person, color: FluxColors.primaryBlue), const Icon(Icons.person, color: FluxColors.primaryBlue),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Flexible(
'Modalità utente singolo (dispositivo personale)', child: Text(
style: Theme.of(context).textTheme.titleLarge, 'Modalità utente singolo (dispositivo personale)',
style: Theme.of(context).textTheme.titleLarge,
),
), ),
], ],
), ),

View File

@@ -784,6 +784,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
children: [ children: [
Expanded( Expanded(
child: DropdownButtonFormField<TicketType>( child: DropdownButtonFormField<TicketType>(
isExpanded: true,
initialValue: ticket.ticketType, initialValue: ticket.ticketType,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Tipo Lavorazione', labelText: 'Tipo Lavorazione',
@@ -804,6 +805,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: DropdownButtonFormField<TicketStatus>( child: DropdownButtonFormField<TicketStatus>(
isExpanded: true,
initialValue: ticket.ticketStatus, initialValue: ticket.ticketStatus,
decoration: const InputDecoration(labelText: 'Stato Attuale'), decoration: const InputDecoration(labelText: 'Stato Attuale'),
items: TicketStatus.values items: TicketStatus.values
@@ -1001,11 +1003,15 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
child: Icon(icon, color: themeColor), child: Icon(icon, color: themeColor),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Expanded(
title, child: Text(
style: Theme.of(context).textTheme.titleLarge?.copyWith( title,
fontWeight: FontWeight.bold, maxLines: 1,
color: themeColor, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: themeColor,
),
), ),
), ),
], ],

View File

@@ -133,29 +133,44 @@ class TicketList extends StatelessWidget {
horizontal: 16.0, horizontal: 16.0,
vertical: 8.0, vertical: 8.0,
), ),
child: Row( // ECCO LA MAGIA: Wrap invece di Row!
child: Wrap(
alignment: WrapAlignment.spaceBetween, // Sostituisce lo Spacer!
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8.0, // Spazio orizzontale
runSpacing: 8.0, // Spazio verticale se va a capo
children: [ children: [
IconButton( // BLOCCO 1: Icona e Contatore
icon: const Icon(Icons.close),
onPressed: () =>
context.read<TicketListCubit>().clearSelection(),
),
Text(
'${state.selectedTickets.length} selezionati',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const Spacer(),
Row( Row(
mainAxisSize: MainAxisSize
.min, // Fondamentale per non occupare tutto il Wrap
children: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () =>
context.read<TicketListCubit>().clearSelection(),
),
Text(
'${state.selectedTickets.length} selezionati',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
// BLOCCO 2: I Bottoni (Un altro Wrap per farli andare a capo tra loro se serve!)
Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.end,
children: [ children: [
FilledButton.icon( FilledButton.icon(
onPressed: () => _setStatusClosed(context), onPressed: () => _setStatusClosed(context),
icon: const Icon(Icons.approval), icon: const Icon(Icons.approval),
label: const Text('Riconsegna'), label: const Text('Riconsegna'),
), ),
const SizedBox(width: 8),
FilledButton.icon( FilledButton.icon(
onPressed: () => _showShippingModal(context), onPressed: () => _showShippingModal(context),
icon: const Icon(Icons.local_shipping), icon: const Icon(Icons.local_shipping),

View File

@@ -306,12 +306,14 @@ class _GlobalUpdateCheckerState extends State<GlobalUpdateChecker> {
size: 32, size: 32,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Flexible(
kIsWeb child: Text(
? "Aggiornamento" kIsWeb
: "Aggiornamento Obbligatorio", ? "Aggiornamento"
style: Theme.of(context).textTheme.titleLarge : "Aggiornamento Obbligatorio",
?.copyWith(fontWeight: FontWeight.bold), style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.bold),
),
), ),
], ],
), ),
@@ -351,12 +353,10 @@ class _GlobalUpdateCheckerState extends State<GlobalUpdateChecker> {
onPressed: () async { onPressed: () async {
if (_updateUrl != null) { if (_updateUrl != null) {
final url = Uri.parse(_updateUrl!); final url = Uri.parse(_updateUrl!);
if (await canLaunchUrl(url)) { await launchUrl(
await launchUrl( url,
url, mode: LaunchMode.externalApplication,
mode: LaunchMode.externalApplication, );
);
}
} }
}, },
), ),