refinements
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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!);
|
|
||||||
},
|
|
||||||
), */
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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"),
|
);
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user