diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0fd43e2..b24b47c 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -25,11 +25,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart
index ec5f7c1..b21d693 100644
--- a/lib/core/blocs/session/session_cubit.dart
+++ b/lib/core/blocs/session/session_cubit.dart
@@ -39,8 +39,35 @@ class SessionCubit extends Cubit {
}
try {
+ // 1. CHI È QUESTO UTENTE? (Vediamo se ha un profilo staff, che sia Invitato o Admin)
+ StaffMemberModel? staff = await _repository.getStaffMemberByUserId(
+ user.id,
+ );
+ CompanyModel? company;
+ if (staff != null) {
+ // --- LA MAGIA DEL SENSORE ---
+ if (staff.hasJoined == false) {
+ // È la primissima volta che entra! Aggiorniamo il DB.
+ await _repository.updateStaffMember(staff.id!, {'has_joined': true});
+ // Aggiorniamo anche il nostro modello in memoria per questa sessione
+ staff = staff.copyWith(hasJoined: true);
+ }
+
+ company = await _repository.getCompanyById(staff.companyId);
+ } else {
+ // È l'Admin in onboarding
+ company = await _repository.getCompanyByOwnerId(user.id);
+ }
// 1. Controllo Azienda
- final company = await _repository.getCompanyByOwnerId(user.id);
+
+ if (staff != null) {
+ // L'utente esiste già nel sistema! Carichiamo l'azienda per cui lavora
+ company = await _repository.getCompanyById(staff.companyId);
+ } else {
+ // L'utente non ha profilo. Probabilmente è l'Admin che ha appena
+ // fatto Sign Up e sta iniziando l'Onboarding
+ company = await _repository.getCompanyByOwnerId(user.id);
+ }
if (company == null) {
return emit(
state.copyWith(
@@ -69,7 +96,6 @@ class SessionCubit extends Cubit {
}
// 3. Controllo Staff (Paziente Zero)
- final staff = await _repository.getStaffMemberByUserId(user.id);
if (staff == null) {
return emit(
state.copyWith(
@@ -102,7 +128,7 @@ class SessionCubit extends Cubit {
user: user,
company: company,
currentStore: activeStore,
- currentStaff: staff,
+ currentStaffMember: staff,
onboardingStep: OnboardingStep.none, // Svuotiamo l'onboarding
),
);
diff --git a/lib/core/blocs/session/session_state.dart b/lib/core/blocs/session/session_state.dart
index b215dcb..0bc69b9 100644
--- a/lib/core/blocs/session/session_state.dart
+++ b/lib/core/blocs/session/session_state.dart
@@ -22,7 +22,7 @@ class SessionState extends Equatable {
final User? user; // Utente di Supabase Auth
final CompanyModel? company;
final StoreModel? currentStore;
- final StaffMemberModel? currentStaff;
+ final StaffMemberModel? currentStaffMember;
final OnboardingStep onboardingStep;
final bool isMobileDevice;
@@ -31,7 +31,7 @@ class SessionState extends Equatable {
this.user,
this.company,
this.currentStore,
- this.currentStaff,
+ this.currentStaffMember,
this.onboardingStep = OnboardingStep.none,
this.isMobileDevice = false,
});
@@ -42,7 +42,7 @@ class SessionState extends Equatable {
User? user,
CompanyModel? company,
StoreModel? currentStore,
- StaffMemberModel? currentStaff,
+ StaffMemberModel? currentStaffMember,
OnboardingStep? onboardingStep,
bool? isMobileDevice,
}) {
@@ -51,7 +51,7 @@ class SessionState extends Equatable {
user: user ?? this.user,
company: company ?? this.company,
currentStore: currentStore ?? this.currentStore,
- currentStaff: currentStaff ?? this.currentStaff,
+ currentStaffMember: currentStaffMember ?? this.currentStaffMember,
onboardingStep: onboardingStep ?? this.onboardingStep,
isMobileDevice: isMobileDevice ?? this.isMobileDevice,
);
@@ -63,7 +63,7 @@ class SessionState extends Equatable {
user,
company,
currentStore,
- currentStaff,
+ currentStaffMember,
onboardingStep,
isMobileDevice,
];
diff --git a/lib/core/data/constants.dart b/lib/core/data/constants.dart
new file mode 100644
index 0000000..316b798
--- /dev/null
+++ b/lib/core/data/constants.dart
@@ -0,0 +1,2 @@
+const String resetPasswordUrl =
+ 'https://flux-web-invite.marco-6ba.workers.dev/';
diff --git a/lib/core/data/core_repository.dart b/lib/core/data/core_repository.dart
index eebec0f..192e412 100644
--- a/lib/core/data/core_repository.dart
+++ b/lib/core/data/core_repository.dart
@@ -28,6 +28,21 @@ class CoreRepository {
}
}
+ Future getCompanyById(String companyId) async {
+ try {
+ final response = await _supabase
+ .from('company')
+ .select()
+ .eq('id', companyId)
+ .maybeSingle();
+ if (response == null) return null;
+ return CompanyModel.fromMap(response);
+ } catch (e) {
+ debugPrint('Errore recupero azienda per ID: $e');
+ return null;
+ }
+ }
+
Future> getStoresByCompanyId(String companyId) async {
try {
final response = await _supabase
@@ -108,4 +123,19 @@ class CoreRepository {
throw Exception('Creazione profilo staff fallita: $e');
}
}
+
+ // Assegna un membro a un negozio
+ Future assignStaffToStore(String staffId, String storeId) async {
+ await _supabase.from('staff_in_stores').insert({
+ 'staff_member_id': staffId,
+ 'store_id': storeId,
+ });
+ }
+
+ Future updateStaffMember(
+ String staffId,
+ Map data,
+ ) async {
+ await _supabase.from('staff_member').update(data).eq('id', staffId);
+ }
}
diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart
index e7eb138..ea2c5d3 100644
--- a/lib/core/routes/app_router.dart
+++ b/lib/core/routes/app_router.dart
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
// Importa il tuo SessionCubit e lo State
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/data/core_repository.dart';
+import 'package:flux/core/widgets/set_password_screen.dart';
import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart';
import 'package:flux/features/auth/ui/auth_screen.dart';
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
@@ -33,6 +34,7 @@ class AppRouter {
final sessionState = sessionCubit.state;
final isGoingToLogin = state.matchedLocation == '/login';
final isGoingToOnboarding = state.matchedLocation == '/onboarding';
+ final isGoingToSetPassword = state.matchedLocation == '/set-password';
// Caso 1: L'app si sta ancora avviando.
// Restituiamo null per farlo rimanere sulla SplashScreen del main.dart
@@ -43,7 +45,8 @@ class AppRouter {
// Caso 2: Utente NON loggato.
if (sessionState.status == SessionStatus.unauthenticated) {
// Se sta già andando al login, lascialo andare. Altrimenti, forzalo al login.
- return isGoingToLogin ? null : '/login';
+ if (isGoingToLogin || isGoingToSetPassword) return null;
+ return '/login';
}
// Caso 3: Utente loggato MA manca un pezzo dell'azienda (Flusso Canalizzatore)
@@ -55,13 +58,13 @@ class AppRouter {
// Caso 4: Utente loggato e configurato (Tutto OK!)
if (sessionState.status == SessionStatus.authenticated) {
- // Se per sbaglio cerca di tornare al login o all'onboarding,
- // lo rimbalziamo alla home.
+ // Attenzione: un utente appena invitato viene considerato "loggato"
+ // da Supabase appena clicca il link. Quindi se sta andando su /set-password,
+ // dobbiamo permetterglielo e non rimbalzarlo!
if (isGoingToLogin || isGoingToOnboarding) {
return '/';
}
- // Per tutte le altre rotte (dashboard, clienti, anagrafiche), lascialo passare.
- return null;
+ return null; // Lascia passare per /, /customer, e anche /set-password
}
return null;
@@ -72,6 +75,10 @@ class AppRouter {
//builder: (context, state) => const LoginScreen(),
builder: (context, state) => const AuthScreen(),
),
+ GoRoute(
+ path: '/set-password',
+ builder: (context, state) => const SetPasswordScreen(),
+ ),
GoRoute(
path: '/onboarding',
builder: (context, state) => BlocProvider(
diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart
index a9bafae..f2cbf24 100644
--- a/lib/core/widgets/flux_text_field.dart
+++ b/lib/core/widgets/flux_text_field.dart
@@ -20,6 +20,7 @@ class FluxTextField extends StatefulWidget {
final List? inputFormatters;
final TextCapitalization? textCapitalization;
final bool? autocorrect;
+ final bool? enabled;
const FluxTextField({
super.key, // Usiamo super.key per Flutter moderno
@@ -39,6 +40,7 @@ class FluxTextField extends StatefulWidget {
this.inputFormatters,
this.textCapitalization,
this.autocorrect,
+ this.enabled = true,
});
@override
@@ -115,6 +117,7 @@ class _FluxTextFieldState extends State {
inputFormatters: widget.inputFormatters,
textCapitalization: widget.textCapitalization ?? TextCapitalization.none,
+ enabled: widget.enabled,
);
}
}
diff --git a/lib/core/widgets/set_password_screen.dart b/lib/core/widgets/set_password_screen.dart
new file mode 100644
index 0000000..33e54b9
--- /dev/null
+++ b/lib/core/widgets/set_password_screen.dart
@@ -0,0 +1,121 @@
+import 'package:flutter/material.dart';
+import 'package:flux/core/widgets/flux_text_field.dart';
+import 'package:get_it/get_it.dart';
+import 'package:supabase_flutter/supabase_flutter.dart';
+import 'package:go_router/go_router.dart';
+
+class SetPasswordScreen extends StatefulWidget {
+ const SetPasswordScreen({super.key});
+
+ @override
+ State createState() => _SetPasswordScreenState();
+}
+
+class _SetPasswordScreenState extends State {
+ final _passwordCtrl = TextEditingController();
+ bool _isLoading = false;
+
+ @override
+ void dispose() {
+ _passwordCtrl.dispose();
+ super.dispose();
+ }
+
+ Future _savePassword() async {
+ final newPassword = _passwordCtrl.text.trim();
+ if (newPassword.length < 6) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text("La password deve avere almeno 6 caratteri"),
+ ),
+ );
+ return;
+ }
+
+ setState(() => _isLoading = true);
+
+ try {
+ // 1. Aggiorniamo la password dell'utente (che Supabase ha già loggato grazie al link della mail)
+ await GetIt.I.get().auth.updateUser(
+ UserAttributes(password: newPassword),
+ );
+
+ // 2. Finito! Lo mandiamo alla home o facciamo ricaricare la sessione al SessionCubit
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text("Password impostata! Benvenuto a bordo 🚀"),
+ ),
+ );
+ context.go('/'); // Rimandiamo al router principale
+ }
+ } on AuthException catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text("Errore Auth: ${e.message}")));
+ }
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text("Errore: $e")));
+ }
+ } finally {
+ if (mounted) setState(() => _isLoading = false);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text("Benvenuto in FLUX!"),
+ automaticallyImplyLeading:
+ false, // Non può tornare indietro, deve mettere la password!
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ const Icon(Icons.lock_reset, size: 80, color: Colors.blueAccent),
+ const SizedBox(height: 24),
+ const Text(
+ "Imposta la tua Password",
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ const Text(
+ "Hai accettato l'invito. Scegli una password sicura per accedere in futuro.",
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.grey),
+ ),
+ const SizedBox(height: 32),
+ FluxTextField(
+ controller: _passwordCtrl,
+ label: "Nuova Password",
+ icon: Icons.lock,
+ isPassword: true,
+ ),
+ const SizedBox(height: 32),
+ ElevatedButton(
+ onPressed: _isLoading ? null : _savePassword,
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ ),
+ child: _isLoading
+ ? const CircularProgressIndicator(color: Colors.white)
+ : const Text(
+ "SALVA E INIZIA",
+ style: TextStyle(fontSize: 16),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart
index 6f062fe..53411e0 100644
--- a/lib/features/auth/bloc/auth_cubit.dart
+++ b/lib/features/auth/bloc/auth_cubit.dart
@@ -1,6 +1,7 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
+import 'package:flux/core/data/constants.dart';
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
part 'auth_state.dart';
@@ -64,6 +65,28 @@ class AuthCubit extends Cubit {
}
}
+ Future requestPasswordReset(String email) async {
+ if (email.isEmpty) {
+ emit(
+ state.copyWith(
+ status: AuthStatus.failure,
+ errorMessage: 'Devi inserire l\'indirizzo email',
+ ),
+ );
+ return;
+ }
+ await _supabase.auth.resetPasswordForEmail(
+ email,
+ redirectTo: resetPasswordUrl,
+ );
+ emit(
+ state.copyWith(
+ status: AuthStatus.pwResetSent,
+ infoMessage: "Email per reset password inviata a $email!",
+ ),
+ );
+ }
+
Future requestLogout() async {
await _supabase.auth.signOut();
emit(state.copyWith(status: AuthStatus.initial));
diff --git a/lib/features/auth/bloc/auth_state.dart b/lib/features/auth/bloc/auth_state.dart
index e491094..f3c237e 100644
--- a/lib/features/auth/bloc/auth_state.dart
+++ b/lib/features/auth/bloc/auth_state.dart
@@ -1,6 +1,6 @@
part of 'auth_cubit.dart';
-enum AuthStatus { initial, loading, failure }
+enum AuthStatus { initial, pwResetSent, loading, failure }
class AuthState extends Equatable {
final AuthStatus status;
diff --git a/lib/features/auth/ui/auth_screen.dart b/lib/features/auth/ui/auth_screen.dart
index 00543d9..f6327d8 100644
--- a/lib/features/auth/ui/auth_screen.dart
+++ b/lib/features/auth/ui/auth_screen.dart
@@ -162,6 +162,21 @@ class _AuthScreenState extends State {
),
),
),
+ if (state.isLoginMode) ...[
+ const SizedBox(height: 24),
+ TextButton(
+ onPressed: () => context
+ .read()
+ .requestPasswordReset(_emailController.text.trim()),
+ child: Text(
+ 'Pw dimenticata/Invito scaduto?',
+ style: TextStyle(
+ color: context.accent,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ],
],
),
),
diff --git a/lib/features/master_data/staff/blocs/staff_cubit.dart b/lib/features/master_data/staff/blocs/staff_cubit.dart
index 9c37a93..548addb 100644
--- a/lib/features/master_data/staff/blocs/staff_cubit.dart
+++ b/lib/features/master_data/staff/blocs/staff_cubit.dart
@@ -16,7 +16,7 @@ class StaffCubit extends Cubit {
// Carica tutto lo staff della compagnia
Future loadAllStaff() async {
- emit(state.copyWith(isLoading: true, error: null));
+ emit(state.copyWith(status: StaffStatus.loading, error: null));
try {
final staff = await _repository.getStaffMembers(
_sessionCubit.state.company!.id!,
@@ -27,18 +27,19 @@ class StaffCubit extends Cubit {
}
emit(
state.copyWith(
+ status: StaffStatus.success,
allStaff: staff,
- isLoading: false,
storesByStaff: storesByStaff,
),
);
} catch (e) {
- emit(state.copyWith(isLoading: false, error: e.toString()));
+ emit(state.copyWith(status: StaffStatus.error, error: e.toString()));
}
}
Future> loadStoresByStaff(String staffId) async {
try {
+ emit(state.copyWith(error: null));
return await _repository.getStaffMemberStore(staffId);
} catch (e) {
emit(state.copyWith(error: e.toString()));
@@ -48,6 +49,7 @@ class StaffCubit extends Cubit {
// Carica lo staff di uno specifico negozio e aggiorna la mappa
Future loadStaffForStore(String storeId) async {
+ emit(state.copyWith(error: null));
try {
final staffInStore = await _repository.getStaffMembersInStore(storeId);
final newMap = Map>.from(
@@ -56,48 +58,87 @@ class StaffCubit extends Cubit {
newMap[storeId] = staffInStore;
emit(state.copyWith(staffByStore: newMap));
} catch (e) {
- // Qui potresti gestire l'errore silenziosamente per non bloccare tutta l'UI
+ emit(state.copyWith(status: StaffStatus.error, error: e.toString()));
}
}
// Salva o aggiorna un membro
Future saveStaffMember(StaffMemberModel member) async {
- emit(state.copyWith(isLoading: true));
+ emit(state.copyWith(status: StaffStatus.loading, error: null));
try {
await _repository.saveStaffMember(member);
await loadAllStaff(); // Ricarichiamo la lista aggiornata
} catch (e) {
- emit(state.copyWith(isLoading: false, error: e.toString()));
+ emit(state.copyWith(status: StaffStatus.error, error: e.toString()));
+ }
+ }
+
+ Future inviteStaffMember({
+ required StaffMemberModel member,
+ required List selectedStoreIds,
+ }) async {
+ emit(state.copyWith(status: StaffStatus.loading, error: null));
+
+ try {
+ // 1. Invitiamo il membro e ci facciamo dare l'ID
+ final newStaffId = await _repository.inviteStaffMember(member);
+
+ // 2. Assegniamo i negozi uno ad uno (usando il metodo che avevi già nel repo!)
+ if (selectedStoreIds.isNotEmpty) {
+ final List assignTasks = [];
+ for (var storeId in selectedStoreIds) {
+ assignTasks.add(_repository.assignStaffToStore(newStaffId, storeId));
+ }
+ await Future.wait(assignTasks); // In parallelo per la massima velocità
+ }
+
+ // 3. Ricarichiamo la lista globale così la UI si aggiorna
+ await loadAllStaff();
+ // (Nota: se hai un loadStaffForStore o loadAllStaff, chiamalo qui per rinfrescare lo stato)
+
+ emit(state.copyWith(status: StaffStatus.success));
+ } catch (e) {
+ emit(state.copyWith(status: StaffStatus.error, error: e.toString()));
}
}
// Associa un dipendente a un negozio
Future assignMemberToStore(String staffId, String storeId) async {
try {
- await _repository.assignToStore(staffId, storeId);
+ await _repository.assignStaffToStore(staffId, storeId);
final stuffStores = await loadStoresByStaff(staffId);
final Map> storesByStaff = Map.from(
state.storesByStaff,
);
storesByStaff[staffId] = stuffStores;
- emit(state.copyWith(storesByStaff: storesByStaff));
+ emit(state.copyWith(storesByStaff: storesByStaff, error: null));
} catch (e) {
- emit(state.copyWith(error: "Errore nell'assegnazione: $e"));
+ emit(
+ state.copyWith(
+ status: StaffStatus.error,
+ error: "Errore nell'assegnazione: $e",
+ ),
+ );
}
}
// Rimuove un dipendente da un negozio
Future removeMemberFromStore(String staffId, String storeId) async {
try {
- await _repository.removeFromStore(staffId, storeId);
+ await _repository.removeStaffFromStore(staffId, storeId);
final stuffStores = await loadStoresByStaff(staffId);
final Map> storesByStaff = Map.from(
state.storesByStaff,
);
storesByStaff[staffId] = stuffStores;
- emit(state.copyWith(storesByStaff: storesByStaff));
+ emit(state.copyWith(storesByStaff: storesByStaff, error: null));
} catch (e) {
- emit(state.copyWith(error: "Errore nella rimozione: $e"));
+ emit(
+ state.copyWith(
+ status: StaffStatus.error,
+ error: "Errore nella rimozione: $e",
+ ),
+ );
}
}
@@ -105,7 +146,7 @@ class StaffCubit extends Cubit {
required StaffMemberModel member,
required List selectedStoreIds,
}) async {
- emit(state.copyWith(isLoading: true));
+ emit(state.copyWith(status: StaffStatus.loading, error: null));
try {
// 1. Salva o aggiorna l'anagrafica (ci serve l'ID)
// Se è un nuovo membro, Supabase ci restituirà l'ID generato
@@ -120,7 +161,7 @@ class StaffCubit extends Cubit {
if (selectedStoreIds.isNotEmpty) {
await Future.wait(
selectedStoreIds.map(
- (storeId) => _repository.assignToStore(staffId, storeId),
+ (storeId) => _repository.assignStaffToStore(staffId, storeId),
),
);
}
@@ -128,9 +169,9 @@ class StaffCubit extends Cubit {
// 3. Rinfresca i dati
await loadAllStaff();
- emit(state.copyWith(isLoading: false));
+ emit(state.copyWith(status: StaffStatus.success));
} catch (e) {
- emit(state.copyWith(isLoading: false, error: e.toString()));
+ emit(state.copyWith(status: StaffStatus.error, error: e.toString()));
}
}
}
diff --git a/lib/features/master_data/staff/blocs/staff_state.dart b/lib/features/master_data/staff/blocs/staff_state.dart
index a8fbfd4..5ef9055 100644
--- a/lib/features/master_data/staff/blocs/staff_state.dart
+++ b/lib/features/master_data/staff/blocs/staff_state.dart
@@ -1,42 +1,44 @@
part of 'staff_cubit.dart';
+enum StaffStatus { initial, loading, success, error }
+
class StaffState extends Equatable {
+ final StaffStatus status;
final List allStaff;
final Map> storesByStaff;
final Map> staffByStore;
- final bool isLoading;
final String? error;
const StaffState({
+ this.status = StaffStatus.initial,
this.allStaff = const [],
this.storesByStaff = const {},
this.staffByStore = const {},
- this.isLoading = false,
this.error,
});
StaffState copyWith({
+ StaffStatus? status,
List? allStaff,
Map>? storesByStaff,
Map>? staffByStore,
- bool? isLoading,
String? error,
}) {
return StaffState(
+ status: status ?? this.status,
allStaff: allStaff ?? this.allStaff,
storesByStaff: storesByStaff ?? this.storesByStaff,
staffByStore: staffByStore ?? this.staffByStore,
- isLoading: isLoading ?? this.isLoading,
error: error,
);
}
@override
List