This commit is contained in:
2026-04-20 23:52:00 +02:00
parent c5b5b76bd6
commit a19fd1104f
37 changed files with 1546 additions and 428 deletions

View File

@@ -1,58 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
part 'auth_events.dart';
part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final _supabase = GetIt.instance<SupabaseClient>();
AuthBloc()
: super(const AuthState(status: AuthStatus.initial, isLoginMode: true)) {
on<ToggleAuthMode>(
(event, emit) => emit(state.copyWith(isLoginMode: !state.isLoginMode)),
);
on<LoginRequested>((event, emit) async {
emit(state.copyWith(status: AuthStatus.loading));
try {
if (state.isLoginMode) {
// --- LOGICA LOGIN ---
await _supabase.auth.signInWithPassword(
email: event.email,
password: event.password,
);
// Non serve emettere success qui, ci pensa il SessionBloc!
} else {
// --- LOGICA SIGNUP ---
await _supabase.auth.signUp(
email: event.email,
password: event.password,
);
// Nota: Se Supabase richiede conferma email, l'utente non sarà
// loggato subito. Gestiamolo con un messaggio.
emit(
state.copyWith(
status: AuthStatus.success,
error: "Controlla la tua email per confermare l'account!",
),
);
}
} on AuthException catch (e) {
emit(state.copyWith(status: AuthStatus.failure, error: e.message));
} catch (e) {
emit(
state.copyWith(
status: AuthStatus.failure,
error: "Errore imprevisto: $e",
),
);
}
});
on<LogoutRequested>((event, emit) async {
await _supabase.auth.signOut();
});
}
}

View File

@@ -0,0 +1,59 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
part 'auth_state.dart';
class AuthCubit extends Cubit<AuthState> {
final _supabase = GetIt.instance<SupabaseClient>();
AuthCubit() : super(const AuthState());
void toggleMode() {
emit(state.copyWith(isLoginMode: !state.isLoginMode));
}
Future<void> submitAuth(String email, String password) async {
// Partiamo puliti: via vecchi messaggi ed errori
emit(state.copyWith(status: AuthStatus.loading));
try {
if (state.isLoginMode) {
// --- LOGICA LOGIN ---
await _supabase.auth.signInWithPassword(
email: email,
password: password,
);
// NESSUN EMIT DI SUCCESS!
// Supabase lancerà l'evento 'signedIn', il SessionCubit lo catturerà
// e il GoRouter ci cambierà pagina. Noi stiamo a guardare il caricamento.
} else {
// --- LOGICA SIGNUP ---
final AuthResponse res = await _supabase.auth.signUp(
email: email,
password: password,
);
// Se la sessione è null, significa che Supabase ha inviato l'email di conferma
if (res.session == null) {
emit(
state.copyWith(
status: AuthStatus.initial,
infoMessage: "Controlla la tua email per confermare l'account!",
),
);
}
// Se non è null, ha fatto il login automatico. Stessa cosa di sopra, ci pensa il SessionCubit.
}
} on AuthException catch (e) {
emit(state.copyWith(status: AuthStatus.failure, errorMessage: e.message));
} catch (e) {
emit(
state.copyWith(
status: AuthStatus.failure,
errorMessage: "Errore imprevisto: $e",
),
);
}
}
}

View File

@@ -1,21 +0,0 @@
part of 'auth_bloc.dart';
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
class ToggleAuthMode extends AuthEvent {} // Passa da Login a Registrazione
class LoginRequested extends AuthEvent {
final String email;
final String password;
const LoginRequested({required this.email, required this.password});
@override
List<Object?> get props => [email, password];
}
class LogoutRequested extends AuthEvent {} // Logout

View File

@@ -1,26 +1,35 @@
part of 'auth_bloc.dart';
part of 'auth_cubit.dart';
enum AuthStatus { initial, loading, success, failure }
enum AuthStatus { initial, loading, failure }
class AuthState extends Equatable {
final AuthStatus status;
final bool isLoginMode;
final String? errorMessage;
final String? infoMessage;
const AuthState({
required this.status,
this.error,
required this.isLoginMode,
this.status = AuthStatus.initial,
this.isLoginMode = true,
this.errorMessage,
this.infoMessage,
});
final AuthStatus status;
final String? error;
final bool isLoginMode;
@override
List<Object?> get props => [status, error, isLoginMode];
AuthState copyWith({AuthStatus? status, String? error, bool? isLoginMode}) {
AuthState copyWith({
AuthStatus? status,
bool? isLoginMode,
String? errorMessage,
String? infoMessage,
}) {
return AuthState(
status: status ?? this.status,
error: error,
isLoginMode: isLoginMode ?? this.isLoginMode,
// Se non passo esplicitamente un errore, lo resetto per evitare che rimanga bloccato a schermo
errorMessage: errorMessage,
infoMessage: infoMessage,
);
}
@override
List<Object?> get props => [status, isLoginMode, errorMessage, infoMessage];
}

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/widgets/flux_logo.dart';
import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:flux/features/auth/bloc/auth_bloc.dart';
import 'package:flux/features/auth/bloc/auth_cubit.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@@ -15,7 +15,6 @@ class AuthScreen extends StatefulWidget {
class _AuthScreenState extends State<AuthScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _isPassword = true;
@override
void dispose() {
@@ -24,19 +23,43 @@ class _AuthScreenState extends State<AuthScreen> {
super.dispose();
}
void _submit() {
// Chiudiamo la tastiera per fare pulizia a schermo
FocusScope.of(context).unfocus();
context.read<AuthCubit>().submitAuth(
_emailController.text.trim(),
_passwordController.text.trim(),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocConsumer<AuthBloc, AuthState>(
body: BlocConsumer<AuthCubit, AuthState>(
// Ottimizzazione: Ridisegniamo la UI solo quando cambia lo status o la modalità
listenWhen: (previous, current) =>
previous.errorMessage != current.errorMessage ||
previous.infoMessage != current.infoMessage,
listener: (context, state) {
if (state.status == AuthStatus.failure) {
// Mostriamo l'errore se c'è
if (state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.error ?? 'Errore di autenticazione'),
content: Text(state.errorMessage!),
backgroundColor: Colors.redAccent,
),
);
}
// Mostriamo il messaggio info (es. Conferma Email)
if (state.infoMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.infoMessage!),
backgroundColor: Colors.blueAccent, // O context.accent
),
);
}
},
builder: (context, state) {
final isLoading = state.status == AuthStatus.loading;
@@ -49,7 +72,7 @@ class _AuthScreenState extends State<AuthScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
// --- LOGO FLUX ---
FluxLogoAuto(height: 80),
const FluxLogoAuto(height: 80),
const SizedBox(height: 60),
// --- TITOLO DINAMICO ---
@@ -77,15 +100,15 @@ class _AuthScreenState extends State<AuthScreen> {
label: 'Email Aziendale',
icon: Icons.email_outlined,
controller: _emailController,
keyboardType: TextInputType.emailAddress,
// TODO: Aggiungi nel tuo FluxTextField la gestione del keyboardType se non c'è già!
),
const SizedBox(height: 20),
FluxTextField(
label: 'Password',
icon: Icons.lock_outline,
isPassword: true,
isPassword: true, // Magia del FluxTextField!
controller: _passwordController,
onSubmitted: (_) => _submit(),
// onSubmitted: (_) => _submit(), // Se lo supporti nel tuo widget custom
),
const SizedBox(height: 40),
@@ -95,7 +118,7 @@ class _AuthScreenState extends State<AuthScreen> {
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: isLoading ? null : () => _submit(),
onPressed: isLoading ? null : _submit,
child: isLoading
? const SizedBox(
height: 24,
@@ -105,7 +128,12 @@ class _AuthScreenState extends State<AuthScreen> {
color: Colors.white,
),
)
: Text(state.isLoginMode ? 'ACCEDI' : 'REGISTRATI'),
: Text(
state.isLoginMode ? 'ACCEDI' : 'REGISTRATI',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
@@ -114,9 +142,7 @@ class _AuthScreenState extends State<AuthScreen> {
TextButton(
onPressed: isLoading
? null
: () {
context.read<AuthBloc>().add(ToggleAuthMode());
},
: () => context.read<AuthCubit>().toggleMode(),
child: RichText(
text: TextSpan(
text: state.isLoginMode
@@ -144,13 +170,4 @@ class _AuthScreenState extends State<AuthScreen> {
),
);
}
void _submit() {
context.read<AuthBloc>().add(
LoginRequested(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
),
);
}
}

View File

@@ -9,11 +9,11 @@ class CompanyRepository {
// .select().single() trasforma la risposta nell'oggetto appena inserito
final response = await _supabase
.from('company')
.insert(company.toJson())
.insert(company.toMap())
.select()
.single();
return CompanyModel.fromJson(response);
return CompanyModel.fromMap(response);
} on PostgrestException catch (e) {
throw e.message;
} catch (e) {
@@ -30,7 +30,7 @@ class CompanyRepository {
.eq('user_id', userId as Object)
.maybeSingle();
return response != null ? CompanyModel.fromJson(response) : null;
return response != null ? CompanyModel.fromMap(response) : null;
} catch (e) {
return null;
}

View File

@@ -1,9 +1,50 @@
import 'package:equatable/equatable.dart';
// ===================================================================
// ENUMS (Paranoia Mode per la lettura dal DB)
// ===================================================================
enum SubscriptionTier {
free,
pro,
premium,
platinum;
static SubscriptionTier fromString(String? value) {
return SubscriptionTier.values.firstWhere(
(e) => e.name == value,
orElse: () => SubscriptionTier.free, // Fallback di sicurezza
);
}
}
enum SubscriptionStatus {
trialing,
active,
pastDue,
canceled;
static SubscriptionStatus fromString(String? value) {
if (value == null) return SubscriptionStatus.canceled;
// Normalizziamo 'past_due' dal DB in 'pastdue' per il match con l'Enum
final normalizedValue = value.replaceAll('_', '').toLowerCase();
return SubscriptionStatus.values.firstWhere(
(e) => e.name.toLowerCase() == normalizedValue,
orElse: () => SubscriptionStatus.canceled,
);
}
}
// ===================================================================
// IL MODELLO ESATTO
// ===================================================================
class CompanyModel extends Equatable {
final String id;
final String? id;
final DateTime? createdAt;
final String userId;
final String userId; // Nel DB è user_id (chiave esterna su auth.users)
// Dati Anagrafici e Fatturazione
final String ragioneSociale;
final String indirizzo;
final String cap;
@@ -12,12 +53,21 @@ class CompanyModel extends Equatable {
final String partitaIva;
final String codiceFiscale;
final String codiceUnivoco;
final bool isPaid;
final DateTime? paymentExpiration;
final String companyLogo;
// Stato Pagamenti (Ibride: manuale + Stripe)
final bool isPaid;
final DateTime? paymentExpiration;
// Campi SaaS Stripe/Automazioni
final SubscriptionTier subscriptionTier;
final SubscriptionStatus subscriptionStatus;
final DateTime? trialEndsAt;
final String? stripeCustomerId;
final String? stripeSubscriptionId;
const CompanyModel({
this.id = '',
this.id,
this.createdAt,
required this.userId,
required this.ragioneSociale,
@@ -28,48 +78,16 @@ class CompanyModel extends Equatable {
required this.partitaIva,
required this.codiceFiscale,
required this.codiceUnivoco,
this.companyLogo = '',
this.isPaid = false,
this.paymentExpiration,
this.companyLogo = '',
this.subscriptionTier = SubscriptionTier.free,
this.subscriptionStatus = SubscriptionStatus.trialing,
this.trialEndsAt,
this.stripeCustomerId,
this.stripeSubscriptionId,
});
factory CompanyModel.fromJson(Map<String, dynamic> json) {
return CompanyModel(
id: json['id'] as String,
createdAt: DateTime.parse(json['created_at']),
userId: json['user_id'] as String,
ragioneSociale: json['ragione_sociale'],
indirizzo: json['indirizzo'],
cap: json['cap'],
citta: json['citta'],
provincia: json['provincia'],
partitaIva: json['partita_iva'],
codiceFiscale: json['codice_fiscale'],
codiceUnivoco: json['codice_univoco'],
isPaid: json['is_paid'] ?? false,
paymentExpiration: json['payment_expiration'] != null
? DateTime.parse(json['payment_expiration'])
: null,
companyLogo: json['company_logo'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'ragione_sociale': ragioneSociale,
'indirizzo': indirizzo,
'cap': cap,
'citta': citta,
'provincia': provincia,
'partita_iva': partitaIva,
'codice_fiscale': codiceFiscale,
'codice_univoco': codiceUnivoco,
'is_paid': isPaid,
'payment_expiration': paymentExpiration?.toIso8601String(),
'company_logo': companyLogo,
};
}
CompanyModel copyWith({
String? id,
DateTime? createdAt,
@@ -82,26 +100,198 @@ class CompanyModel extends Equatable {
String? partitaIva,
String? codiceFiscale,
String? codiceUnivoco,
String? companyLogo,
bool? isPaid,
DateTime? paymentExpiration,
String? companyLogo,
}) => CompanyModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
userId: userId ?? this.userId,
ragioneSociale: ragioneSociale ?? this.ragioneSociale,
indirizzo: indirizzo ?? this.indirizzo,
cap: cap ?? this.cap,
citta: citta ?? this.citta,
provincia: provincia ?? this.provincia,
partitaIva: partitaIva ?? this.partitaIva,
codiceFiscale: codiceFiscale ?? this.codiceFiscale,
codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco,
isPaid: isPaid ?? this.isPaid,
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
companyLogo: companyLogo ?? this.companyLogo,
);
SubscriptionTier? subscriptionTier,
SubscriptionStatus? subscriptionStatus,
DateTime? trialEndsAt,
String? stripeCustomerId,
String? stripeSubscriptionId,
}) {
return CompanyModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
userId: userId ?? this.userId,
ragioneSociale: ragioneSociale ?? this.ragioneSociale,
indirizzo: indirizzo ?? this.indirizzo,
cap: cap ?? this.cap,
citta: citta ?? this.citta,
provincia: provincia ?? this.provincia,
partitaIva: partitaIva ?? this.partitaIva,
codiceFiscale: codiceFiscale ?? this.codiceFiscale,
codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco,
companyLogo: companyLogo ?? this.companyLogo,
isPaid: isPaid ?? this.isPaid,
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
subscriptionTier: subscriptionTier ?? this.subscriptionTier,
subscriptionStatus: subscriptionStatus ?? this.subscriptionStatus,
trialEndsAt: trialEndsAt ?? this.trialEndsAt,
stripeCustomerId: stripeCustomerId ?? this.stripeCustomerId,
stripeSubscriptionId: stripeSubscriptionId ?? this.stripeSubscriptionId,
);
}
factory CompanyModel.fromMap(Map<String, dynamic> map) {
return CompanyModel(
id: map['id'] as String?,
createdAt: map['created_at'] != null
? DateTime.tryParse(map['created_at'])
: null,
userId: map['user_id'] ?? '',
ragioneSociale: map['ragione_sociale'] ?? '',
indirizzo: map['indirizzo'] ?? '',
cap: map['cap'] ?? '',
citta: map['citta'] ?? '',
provincia: map['provincia'] ?? '',
partitaIva: map['partita_iva'] ?? '',
codiceFiscale: map['codice_fiscale'] ?? '',
codiceUnivoco: map['codice_univoco'] ?? '',
companyLogo: map['company_logo'] ?? '',
isPaid: map['is_paid'] ?? false,
paymentExpiration: map['payment_expiration'] != null
? DateTime.tryParse(map['payment_expiration'])
: null,
subscriptionTier: SubscriptionTier.fromString(map['subscription_tier']),
subscriptionStatus: SubscriptionStatus.fromString(
map['subscription_status'],
),
trialEndsAt: map['trial_ends_at'] != null
? DateTime.tryParse(map['trial_ends_at'])
: null,
stripeCustomerId: map['stripe_customer_id'],
stripeSubscriptionId: map['stripe_subscription_id'],
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
// created_at è gestito dal DB di default, di solito non si passa nell'insert
'user_id': userId,
'ragione_sociale': ragioneSociale,
'indirizzo': indirizzo,
'cap': cap,
'citta': citta,
'provincia': provincia,
'partita_iva': partitaIva,
'codice_fiscale': codiceFiscale,
'codice_univoco': codiceUnivoco,
'company_logo': companyLogo,
'is_paid': isPaid,
if (paymentExpiration != null)
'payment_expiration': paymentExpiration!.toIso8601String(),
'subscription_tier': subscriptionTier.name,
'subscription_status': subscriptionStatus == SubscriptionStatus.pastDue
? 'past_due'
: subscriptionStatus.name,
if (trialEndsAt != null) 'trial_ends_at': trialEndsAt!.toIso8601String(),
if (stripeCustomerId != null) 'stripe_customer_id': stripeCustomerId,
if (stripeSubscriptionId != null)
'stripe_subscription_id': stripeSubscriptionId,
};
}
@override
List<Object?> get props => [id, userId, ragioneSociale, partitaIva, isPaid];
List<Object?> get props => [
id,
createdAt,
userId,
ragioneSociale,
indirizzo,
cap,
citta,
provincia,
partitaIva,
codiceFiscale,
codiceUnivoco,
companyLogo,
isPaid,
paymentExpiration,
subscriptionTier,
subscriptionStatus,
trialEndsAt,
stripeCustomerId,
stripeSubscriptionId,
];
}
// ===================================================================
// BUSINESS LOGIC: I LIMITI DEI TIER
// ===================================================================
extension CompanyLimits on CompanyModel {
int get maxStores {
switch (subscriptionTier) {
case SubscriptionTier.free:
return 1;
case SubscriptionTier.pro:
return 1;
case SubscriptionTier.premium:
return 10;
case SubscriptionTier.platinum:
return 30;
}
}
int get maxStaffMembers {
switch (subscriptionTier) {
case SubscriptionTier.free:
return 2;
case SubscriptionTier.pro:
return 10;
case SubscriptionTier.premium:
return 50;
case SubscriptionTier.platinum:
return 150;
}
}
int get maxServicesPerMonth {
switch (subscriptionTier) {
case SubscriptionTier.free:
return 50;
case SubscriptionTier.pro:
return 1000;
case SubscriptionTier.premium:
return 10000;
case SubscriptionTier.platinum:
return 30000;
}
}
int get maxStorageGb {
switch (subscriptionTier) {
case SubscriptionTier.free:
return 0; // 500MB = 0.5 GB, magari qui usi i MB interi
case SubscriptionTier.pro:
return 3;
case SubscriptionTier.premium:
return 30;
case SubscriptionTier.platinum:
return 100;
}
}
/// Verifica generale: L'utente ha i permessi per usare l'app?
bool get hasActiveAccess {
// 1. Priorità all'override manuale (is_paid e payment_expiration)
if (isPaid) {
if (paymentExpiration == null)
return true; // Pagato "a vita" o senza scadenza
if (DateTime.now().isBefore(paymentExpiration!)) return true;
}
// 2. Controllo SaaS (Stripe/Subscription)
if (subscriptionStatus == SubscriptionStatus.active) return true;
// 3. Controllo Trial
if (subscriptionStatus == SubscriptionStatus.trialing &&
trialEndsAt != null) {
return DateTime.now().isBefore(trialEndsAt!);
}
// Scaduto, past_due, o cancellato
return false;
}
}

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/auth/bloc/auth_bloc.dart';
import 'package:flux/features/auth/bloc/auth_cubit.dart';
import 'package:flux/features/company/bloc/company_bloc.dart';
import 'package:flux/core/blocs/session/session_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/company/models/company_model.dart';

View File

@@ -1,7 +1,7 @@
import 'dart:async'; // Serve per il Timer del debounce
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:get_it/get_it.dart';

View File

@@ -1,5 +1,5 @@
import 'package:file_picker/file_picker.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/string_extensions.dart';
import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:get_it/get_it.dart';

View File

@@ -1,6 +1,6 @@
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/blocs/session/session_cubit.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/customers/blocs/customer_cubit.dart';
import 'package:flux/features/customers/models/customer_model.dart';

View File

@@ -1,6 +1,6 @@
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/blocs/session/session_cubit.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/home/ui/dashboard_adaptive_grid.dart';

View File

@@ -1,8 +1,8 @@
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/blocs/session/session_cubit.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/auth/bloc/auth_bloc.dart';
import 'package:flux/features/auth/bloc/auth_cubit.dart';
import 'package:flux/features/master_data/master_data_hub_content.dart';
import 'package:flux/features/services/blocs/services_cubit.dart';
import 'package:flux/features/services/ui/services_screen.dart';

View File

@@ -1,7 +1,7 @@
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/master_data/products/data/product_repository.dart';
import 'package:flux/features/master_data/products/models/brand_model.dart';
import 'package:flux/features/master_data/products/models/model_model.dart';

View File

@@ -1,6 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/master_data/providers/data/provider_repository.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';
import 'package:get_it/get_it.dart';

View File

@@ -1,6 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/master_data/staff/data/staff_repository.dart';
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';

View File

@@ -1,47 +1,101 @@
import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/string_extensions.dart'; // Assicurati che il percorso sia corretto
// L'Enum magico e blindato per il sistema
enum SystemRole {
admin,
manager,
user;
// Helper per convertire dal DB a Dart in sicurezza
static SystemRole fromString(String? value) {
return SystemRole.values.firstWhere(
(e) => e.name == value,
orElse: () => SystemRole.user, // Fallback paranoico al livello più basso
);
}
}
class StaffMemberModel extends Equatable {
final String? id;
final String name;
final String email;
final String phone;
final bool isActive;
final String companyId;
final String storeId;
final String userId;
final String name;
final String surname;
final String?
jobTitle; // Testo libero! Il cliente ci scrive quello che vuole.
final SystemRole systemRole; // ENUM! Il sistema non si frega.
const StaffMemberModel({
this.id,
required this.name,
this.email = '',
this.phone = '',
this.isActive = true,
required this.companyId,
required this.storeId,
required this.userId,
required this.name,
required this.surname,
this.jobTitle,
this.systemRole = SystemRole.user, // Sicurezza di default
});
StaffMemberModel copyWith({
String? id,
String? companyId,
String? storeId,
String? userId,
String? name,
String? surname,
String? jobTitle,
SystemRole? systemRole,
}) {
return StaffMemberModel(
id: id ?? this.id,
companyId: companyId ?? this.companyId,
storeId: storeId ?? this.storeId,
userId: userId ?? this.userId,
name: name ?? this.name,
surname: surname ?? this.surname,
jobTitle: jobTitle ?? this.jobTitle,
systemRole: systemRole ?? this.systemRole,
);
}
factory StaffMemberModel.fromMap(Map<String, dynamic> map) {
return StaffMemberModel(
id: map['id'],
// Applichiamo il tuo myFormat per visualizzare i nomi correttamente
name: (map['name'] as String).myFormat(),
// L'email la teniamo lowercase per standard tecnico
email: (map['email'] as String? ?? '').toLowerCase().trim(),
phone: (map['phone'] as String? ?? '').trim(),
isActive: map['is_active'] ?? true,
companyId: map['company_id'],
id: map['id'] as String?,
companyId: map['company_id'] ?? '',
storeId: map['store_id'] ?? '',
userId: map['user_id'] ?? '',
name: map['name'] ?? '',
surname: map['surname'] ?? '',
jobTitle: map['job_title'] as String?, // Semplice stringa
systemRole: SystemRole.fromString(
map['system_role'],
), // Lettura tipizzata
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'name': name.toLowerCase().trim(), // Salviamo pulito per le query
'email': email.toLowerCase().trim(),
'phone': phone.trim(),
'is_active': isActive,
'company_id': companyId,
'store_id': storeId,
'user_id': userId,
'name': name,
'surname': surname,
if (jobTitle != null) 'job_title': jobTitle,
'system_role': systemRole.name, // Trasforma SystemRole.admin in 'admin'
};
}
@override
List<Object?> get props => [id, name, email, phone, isActive, companyId];
List<Object?> get props => [
id,
companyId,
storeId,
userId,
name,
surname,
jobTitle,
systemRole,
];
}

View File

@@ -1,6 +1,6 @@
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/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

View File

@@ -1,6 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart';
import 'package:flux/features/master_data/staff/data/staff_repository.dart';
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';
import 'package:flux/core/blocs/session/session_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';

View File

@@ -1,6 +1,6 @@
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/blocs/session/session_cubit.dart';
import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';

View File

@@ -0,0 +1,90 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/data/core_repository.dart';
import 'package:flux/features/company/models/company_model.dart';
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';
import 'package:flux/features/onboarding/blocs/onboarding_state.dart';
class OnboardingCubit extends Cubit<OnboardingState> {
final CoreRepository _repository;
final SessionCubit _sessionCubit;
OnboardingCubit(this._sessionCubit, this._repository)
: super(OnboardingState(step: _sessionCubit.state.onboardingStep));
// --- STEP 1: REGISTRAZIONE AZIENDA ---
Future<void> saveCompany(CompanyModel company) async {
emit(state.copyWith(isLoading: true));
try {
// Il repository restituisce il modello creato con l'ID di Supabase
final savedCompany = await _repository.createCompany(company);
emit(
state.copyWith(
isLoading: false,
step: OnboardingStep.store,
companyId: savedCompany.id,
),
);
} catch (e) {
emit(
state.copyWith(
isLoading: false,
error: "Errore salvataggio azienda: $e",
),
);
}
}
// --- STEP 2: REGISTRAZIONE PRIMO NEGOZIO ---
Future<void> saveStore(StoreModel store) async {
if (state.companyId == null) return;
emit(state.copyWith(isLoading: true));
try {
// Iniettiamo forzatamente il companyId ottenuto dallo step precedente
final storeToSave = store.copyWith(companyId: state.companyId);
final savedStore = await _repository.createStore(storeToSave);
emit(
state.copyWith(
isLoading: false,
step: OnboardingStep.staff,
storeId: savedStore.id,
),
);
} catch (e) {
emit(
state.copyWith(isLoading: false, error: "Errore salvataggio store: $e"),
);
}
}
// --- STEP 3: REGISTRAZIONE PROFILO STAFF (PAZIENTE ZERO) ---
Future<void> saveStaff(StaffMemberModel staff) async {
if (state.companyId == null || state.storeId == null) return;
emit(state.copyWith(isLoading: true));
try {
// PARANOIA MODE: Forziamo i legami e il ruolo di sistema 'admin'
final staffToSave = staff.copyWith(
companyId: state.companyId!,
storeId: state.storeId!,
userId: _sessionCubit.state.user!.id, // Dall'utente loggato in Supabase
systemRole: SystemRole.admin, // Blindato!
);
await _repository.createStaffMember(staffToSave);
emit(state.copyWith(isLoading: false, step: OnboardingStep.completed));
// Svegliamo il SessionCubit: lui ricalcolerà tutto e aprirà la Dashboard
await _sessionCubit.initializeSession();
} catch (e) {
emit(
state.copyWith(isLoading: false, error: "Errore creazione profilo: $e"),
);
}
}
}

View File

@@ -0,0 +1,37 @@
import 'package:equatable/equatable.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
class OnboardingState extends Equatable {
final OnboardingStep step;
final bool isLoading;
final String? error;
final String? companyId; // Salvato dopo lo Step 1
final String? storeId; // Salvato dopo lo Step 2
const OnboardingState({
required this.step,
this.isLoading = false,
this.error,
this.companyId,
this.storeId,
});
OnboardingState copyWith({
OnboardingStep? step,
bool? isLoading,
String? error,
String? companyId,
String? storeId,
}) {
return OnboardingState(
step: step ?? this.step,
isLoading: isLoading ?? this.isLoading,
error: error, // Se non passato, resettiamo l'errore
companyId: companyId ?? this.companyId,
storeId: storeId ?? this.storeId,
);
}
@override
List<Object?> get props => [step, isLoading, error, companyId, storeId];
}

View File

@@ -0,0 +1,355 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/company/models/company_model.dart';
import 'package:flux/features/master_data/store/models/store_model.dart';
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
// Sostituisci con il percorso corretto della tua FluxTextField
import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:flux/features/onboarding/blocs/onboarding_state.dart';
class OnboardingScreen extends StatefulWidget {
const OnboardingScreen({super.key});
@override
State<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends State<OnboardingScreen> {
late PageController _pageController;
// --- CHIAVI DEI FORM (Per la validazione indipendente di ogni step) ---
final _companyFormKey = GlobalKey<FormState>();
final _storeFormKey = GlobalKey<FormState>();
final _staffFormKey = GlobalKey<FormState>();
// --- CONTROLLERS: STEP 1 (Company) ---
final _companyNameCtrl = TextEditingController();
final _companyVatCtrl = TextEditingController();
// --- CONTROLLERS: STEP 2 (Store) ---
final _storeNameCtrl = TextEditingController();
final _storeAddressCtrl = TextEditingController();
// --- CONTROLLERS: STEP 3 (Staff) ---
final _staffFirstNameCtrl = TextEditingController();
final _staffLastNameCtrl = TextEditingController();
final _staffJobTitleCtrl = TextEditingController();
@override
void initState() {
super.initState();
// Calcoliamo la pagina iniziale in base allo step salvato nel Cubit
final initialStep = context.read<OnboardingCubit>().state.step;
_pageController = PageController(initialPage: _getPageIndex(initialStep));
}
@override
void dispose() {
_pageController.dispose();
_companyNameCtrl.dispose();
_companyVatCtrl.dispose();
_storeNameCtrl.dispose();
_storeAddressCtrl.dispose();
_staffFirstNameCtrl.dispose();
_staffLastNameCtrl.dispose();
_staffJobTitleCtrl.dispose();
super.dispose();
}
int _getPageIndex(OnboardingStep step) {
switch (step) {
case OnboardingStep.company:
return 0;
case OnboardingStep.store:
return 1;
case OnboardingStep.staff:
return 2;
default:
return 0;
}
}
// Validatore generico riutilizzabile
String? _requireValidator(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Campo obbligatorio';
}
return null;
}
@override
Widget build(BuildContext context) {
return BlocConsumer<OnboardingCubit, OnboardingState>(
// Ascoltiamo i cambi di stato per animare la pagina e mostrare errori
listenWhen: (previous, current) =>
previous.step != current.step || previous.error != current.error,
listener: (context, state) {
// Gestione Errori
if (state.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error!), backgroundColor: Colors.red),
);
}
// Se ha finito, non animiamo nulla: il GoRouter prenderà il controllo a breve!
if (state.step == OnboardingStep.completed) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Configurazione completata! Benvenuto a bordo 🚀"),
backgroundColor: Colors.green,
),
);
return;
}
// Animazione cambio pagina
if (state.step != OnboardingStep.completed) {
final targetPage = _getPageIndex(state.step);
if (_pageController.hasClients &&
_pageController.page?.round() != targetPage) {
_pageController.animateToPage(
targetPage,
duration: const Duration(milliseconds: 600),
curve: Curves.easeInOutCubic, // Animazione super fluida
);
}
}
},
builder: (context, state) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
// IL PAGEVIEW CORAZZATO
PageView(
controller: _pageController,
physics:
const NeverScrollableScrollPhysics(), // Vietato lo swipe manuale!
children: [
_buildCompanyForm(context, state),
_buildStoreForm(context, state),
_buildStaffForm(context, state),
],
),
// OVERLAY CARICAMENTO
if (state.isLoading)
Container(
color: Colors.black.withValues(alpha: 0.4),
child: const Center(child: CircularProgressIndicator()),
),
],
),
),
);
},
);
}
// =========================================================================
// SCHERMATE DEI SINGOLI STEP
// =========================================================================
Widget _buildCompanyForm(BuildContext context, OnboardingState state) {
return Padding(
padding: const EdgeInsets.all(32.0),
child: Form(
key: _companyFormKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
"Iniziamo! 🏢",
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"Inserisci i dati della tua attività per configurare il tuo ambiente FLUX.",
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 48),
FluxTextField(
label: 'Ragione Sociale / Nome Azienda',
controller: _companyNameCtrl,
validator: _requireValidator,
),
const SizedBox(height: 16),
FluxTextField(
label: 'Partita IVA',
controller: _companyVatCtrl,
validator: _requireValidator,
),
const Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () {
if (_companyFormKey.currentState!.validate()) {
// MOCK DI ESEMPIO: Sostituisci con il tuo vero CompanyModel
final newCompany = CompanyModel(
ownerId: '', // Questo lo gestirà o ignorerà il Cubit
name: _companyNameCtrl.text.trim(),
vatNumber: _companyVatCtrl.text.trim(),
);
context.read<OnboardingCubit>().saveCompany(newCompany);
}
},
child: const Text(
"Salva e Prosegui",
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildStoreForm(BuildContext context, OnboardingState state) {
return Padding(
padding: const EdgeInsets.all(32.0),
child: Form(
key: _storeFormKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
"Il tuo Negozio 🏪",
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"Dove si trova il tuo punto vendita principale? (Potrai aggiungerne altri in seguito).",
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 48),
FluxTextField(
label: 'Nome Negozio (es. Sede Centrale)',
controller: _storeNameCtrl,
validator: _requireValidator,
),
const SizedBox(height: 16),
FluxTextField(
label: 'Indirizzo completo',
controller: _storeAddressCtrl,
validator: _requireValidator,
),
const Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () {
if (_storeFormKey.currentState!.validate()) {
final newStore = StoreModel(
companyId: '', // Iniettato dal Cubit
name: _storeNameCtrl.text.trim(),
address: _storeAddressCtrl.text.trim(),
isActive: true,
);
context.read<OnboardingCubit>().saveStore(newStore);
}
},
child: const Text(
"Salva Negozio",
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
],
),
),
);
}
Widget _buildStaffForm(BuildContext context, OnboardingState state) {
return Padding(
padding: const EdgeInsets.all(32.0),
child: Form(
key: _staffFormKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
"Il tuo Profilo 👤",
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"Ultimo step! Crea il tuo profilo operativo per iniziare a usare FLUX.",
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 48),
FluxTextField(
label: 'Nome',
controller: _staffFirstNameCtrl,
validator: _requireValidator,
),
const SizedBox(height: 16),
FluxTextField(
label: 'Cognome',
controller: _staffLastNameCtrl,
validator: _requireValidator,
),
const SizedBox(height: 16),
FluxTextField(
label: 'Etichetta Ruolo (es. Titolare, Manager)',
controller: _staffJobTitleCtrl,
// Il jobTitle può anche essere opzionale, decidi tu!
),
const Spacer(),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.black, // O il tuo context.accent
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () {
if (_staffFormKey.currentState!.validate()) {
final newStaff = StaffMemberModel(
companyId: '', // Iniettato dal Cubit
storeId: '', // Iniettato dal Cubit
userId: '', // Iniettato dal Cubit
name: _staffFirstNameCtrl.text.trim(),
surname: _staffLastNameCtrl.text.trim(),
jobTitle: _staffJobTitleCtrl.text.trim(),
// systemRole non viene passato qui: la Paranoia Mode del Cubit forzerà "admin"
);
context.read<OnboardingCubit>().saveStaff(newStaff);
}
},
child: const Text(
"Entra in FLUX",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 16),
],
),
),
);
}
}

View File

@@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
import 'package:flux/features/onboarding/blocs/onboarding_state.dart';
// Importa i tuoi file (cubit, modelli, ecc.)
class OnboardingScreen extends StatefulWidget {
const OnboardingScreen({super.key});
@override
State<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends State<OnboardingScreen> {
late PageController _pageController;
@override
void initState() {
super.initState();
// Inizializziamo il controller sulla pagina giusta.
// L'indice parte da 0. company=0, store=1, staff=2.
final initialStep = context.read<OnboardingCubit>().state.step;
_pageController = PageController(initialPage: _getPageIndex(initialStep));
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
int _getPageIndex(OnboardingStep step) {
switch (step) {
case OnboardingStep.company:
return 0;
case OnboardingStep.store:
return 1;
case OnboardingStep.staff:
return 2;
default:
return 0;
}
}
@override
Widget build(BuildContext context) {
return BlocConsumer<OnboardingCubit, OnboardingState>(
// Ascoltiamo solo quando cambia lo step per animare la pagina
listenWhen: (previous, current) => previous.step != current.step,
listener: (context, state) {
if (state.step == OnboardingStep.completed) {
// Il SessionCubit prenderà il controllo e farà il redirect,
// qui potremmo mostrare un bel toast di successo.
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Configurazione completata! 🚀")),
);
return;
}
final targetPage = _getPageIndex(state.step);
_pageController.animateToPage(
targetPage,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
builder: (context, state) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
// Il PageView disabilita lo scorrimento manuale con NeverScrollableScrollPhysics
PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildCompanyForm(context, state),
_buildStoreForm(context, state),
_buildStaffForm(
context,
state,
), // Qui c'è la magia paranoica
],
),
// Overlay di caricamento universale
if (state.isLoading)
Container(
color: Colors.black.withValues(alpha: 0.5),
child: const Center(child: CircularProgressIndicator()),
),
],
),
),
);
},
);
}
// --- I METODI DEI FORM ---
// (Nella realtà li metterai in file separati o widget custom per pulizia)
Widget _buildCompanyForm(BuildContext context, OnboardingState state) {
// Controller e chiavi del form...
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
"Step 1: La tua Azienda",
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text("Inserisci i dati della tua attività."),
const SizedBox(height: 32),
// Esempio usando la tua FluxTextField
FluxTextField(label: 'Ragione Sociale / Nome Azienda'),
const SizedBox(height: 16),
FluxTextField(label: 'Partita IVA'),
const Spacer(),
ElevatedButton(
onPressed: () {
// 1. Valida il form
// 2. Crea il CompanyModel
// 3. Chiama il Cubit:
// context.read<OnboardingCubit>().saveCompany(newCompany);
},
child: const Text("Salva e prosegui"),
),
],
),
);
}
Widget _buildStoreForm(BuildContext context, OnboardingState state) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
"Step 2: Il primo Negozio",
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text("Dove si trova il tuo punto vendita principale?"),
const SizedBox(height: 32),
FluxTextField(label: 'Nome Negozio (es. Sede Centrale)'),
const SizedBox(height: 16),
FluxTextField(label: 'Indirizzo'),
const Spacer(),
ElevatedButton(
onPressed: () {
// context.read<OnboardingCubit>().saveStore(newStore);
},
child: const Text("Salva Negozio"),
),
],
),
);
}
Widget _buildStaffForm(BuildContext context, OnboardingState state) {
// NOTA PARANOICA: Qui chiediamo jobTitle (Testo libero),
// ma NON diamo modo di scegliere il system_role!
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
"Step 3: Il tuo Profilo",
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text("Crea il tuo profilo operativo per iniziare a lavorare."),
const SizedBox(height: 32),
FluxTextField(label: 'Nome'),
const SizedBox(height: 16),
FluxTextField(label: 'Cognome'),
const SizedBox(height: 16),
// TESTO LIBERO! Il cliente ci scrive quello che vuole ("CEO", "Boss", "Stagista")
FluxTextField(label: 'Ruolo / Etichetta (es. Titolare)'),
const Spacer(),
ElevatedButton(
onPressed: () {
// Quando chiami il Cubit, passi i dati della UI.
// Ti ricordi? L'OnboardingCubit forzerà `system_role = SystemRole.admin`
// dietro le quinte!
// context.read<OnboardingCubit>().saveStaff(newStaff);
},
child: const Text("Inizia a usare Flux!"),
),
],
),
);
}
}

View File

@@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart';
import 'package:file_picker/file_picker.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/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/string_extensions.dart';
import 'package:flux/features/services/data/services_repository.dart';
import 'package:flux/features/services/models/energy_service_model.dart';

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:flux/features/services/models/service_file_model.dart';

View File

@@ -1,6 +1,6 @@
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/blocs/session/session_cubit.dart';
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart';
import 'package:flux/features/services/data/services_repository.dart';

View File

@@ -1,6 +1,6 @@
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/blocs/session/session_cubit.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
import 'package:flux/features/services/blocs/services_cubit.dart';
import 'package:flux/features/services/models/service_model.dart';