feat-insert-service #5
@@ -9,9 +9,8 @@ import 'package:flux/features/home/ui/home_screen.dart';
|
|||||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||||
import 'package:flux/features/master_data/store/ui/create_store_screen.dart';
|
import 'package:flux/features/master_data/store/ui/create_store_screen.dart';
|
||||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
import 'package:flux/features/services/data/services_repository.dart';
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@@ -83,14 +82,15 @@ class AppRouter {
|
|||||||
path: '/service-form',
|
path: '/service-form',
|
||||||
name: 'service-form',
|
name: 'service-form',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// Recuperiamo il serviceId dai parametri della query (es: /service-form?serviceId=123)
|
// Recuperiamo l'oggetto se passato tramite 'extra'
|
||||||
|
final existingService = state.extra as ServiceModel?;
|
||||||
|
// Recuperiamo l'ID se presente nell'URL
|
||||||
final serviceId = state.uri.queryParameters['serviceId'];
|
final serviceId = state.uri.queryParameters['serviceId'];
|
||||||
if (serviceId != null) {
|
|
||||||
context.read<ServicesCubit>().initServiceForm(
|
return ServiceFormScreen(
|
||||||
serviceId: serviceId,
|
serviceId: serviceId ?? existingService?.id,
|
||||||
);
|
existingService: existingService,
|
||||||
}
|
);
|
||||||
return ServiceFormScreen();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
Future<void> createCustomer(CustomerModel customer) async {
|
Future<void> createCustomer(CustomerModel customer) async {
|
||||||
emit(state.copyWith(status: CustomerStatus.loading));
|
emit(state.copyWith(status: CustomerStatus.loading));
|
||||||
try {
|
try {
|
||||||
final newCustomer = await _repository.createCustomer(customer);
|
final newCustomer = await _repository.saveCustomer(customer);
|
||||||
|
|
||||||
// Aggiorniamo la lista locale aggiungendo il nuovo cliente in cima
|
// Aggiorniamo la lista locale aggiungendo il nuovo cliente in cima
|
||||||
final updatedList = List<CustomerModel>.from(state.customers)
|
final updatedList = List<CustomerModel>.from(state.customers)
|
||||||
@@ -128,6 +128,29 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<CustomerModel?> quickCreateCustomer({
|
||||||
|
required String name,
|
||||||
|
String? phone,
|
||||||
|
String? email,
|
||||||
|
}) async {
|
||||||
|
final newCustomer = CustomerModel(
|
||||||
|
nome: name,
|
||||||
|
telefono: phone ?? '',
|
||||||
|
email: email ?? '',
|
||||||
|
companyId: _sessionBloc.state.company!.id,
|
||||||
|
note: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final saved = await _repository.saveCustomer(newCustomer);
|
||||||
|
// Lo aggiungiamo in cima ai suggerimenti
|
||||||
|
emit(state.copyWith(customers: [saved, ...state.customers]));
|
||||||
|
return saved;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pulizia della memoria quando il Cubit viene distrutto
|
// Pulizia della memoria quando il Cubit viene distrutto
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ class CustomerRepository {
|
|||||||
final SupabaseClient _client = GetIt.I<SupabaseClient>();
|
final SupabaseClient _client = GetIt.I<SupabaseClient>();
|
||||||
|
|
||||||
// Crea un nuovo cliente
|
// Crea un nuovo cliente
|
||||||
Future<CustomerModel> createCustomer(CustomerModel customer) async {
|
Future<CustomerModel> saveCustomer(CustomerModel customer) async {
|
||||||
try {
|
try {
|
||||||
final response = await _client
|
final response = await _client
|
||||||
.from('customer')
|
.from('customer')
|
||||||
.insert(customer.toJson())
|
.upsert(customer.toJson())
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
return CustomerModel.fromJson(response);
|
return CustomerModel.fromJson(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw 'Errore durante la creazione del cliente: $e';
|
throw 'Errore durante il salvataggio del cliente: $e';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/features/customers/blocs/customer_cubit.dart';
|
import 'package:flux/features/customers/blocs/customer_cubit.dart';
|
||||||
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
|
import 'package:flux/features/customers/ui/quick_customer_dialog.dart';
|
||||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
|
|
||||||
class CustomerSearchSheet extends StatefulWidget {
|
class CustomerSearchSheet extends StatefulWidget {
|
||||||
@@ -86,18 +88,29 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
|
|||||||
// --- TASTO NUOVO CLIENTE ---
|
// --- TASTO NUOVO CLIENTE ---
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: OutlinedButton.icon(
|
child: IconButton(
|
||||||
onPressed: () {
|
|
||||||
// TODO: Naviga alla pagina "Crea Cliente".
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.person_add),
|
icon: const Icon(Icons.person_add),
|
||||||
label: const Text("Crea Nuovo Cliente"),
|
onPressed: () async {
|
||||||
style: OutlinedButton.styleFrom(
|
final servicesCubit = context.read<ServicesCubit>();
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
// Apriamo la dialog passando la query attuale
|
||||||
shape: RoundedRectangleBorder(
|
final CustomerModel? nuovoCliente = await showDialog(
|
||||||
borderRadius: BorderRadius.circular(12),
|
context: context,
|
||||||
),
|
builder: (context) => QuickCustomerDialog(
|
||||||
),
|
initialQuery: _searchController.text,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nuovoCliente != null) {
|
||||||
|
servicesCubit.updateField(
|
||||||
|
customerId: nuovoCliente.id,
|
||||||
|
customerDisplayName: nuovoCliente.nome,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_searchController.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|||||||
117
lib/features/customers/ui/quick_customer_dialog.dart
Normal file
117
lib/features/customers/ui/quick_customer_dialog.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/features/customers/blocs/customer_cubit.dart';
|
||||||
|
|
||||||
|
class QuickCustomerDialog extends StatefulWidget {
|
||||||
|
final String initialQuery;
|
||||||
|
|
||||||
|
const QuickCustomerDialog({super.key, required this.initialQuery});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<QuickCustomerDialog> createState() => _QuickCustomerDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickCustomerDialogState extends State<QuickCustomerDialog> {
|
||||||
|
late final TextEditingController _nameCtrl;
|
||||||
|
final _phoneCtrl = TextEditingController();
|
||||||
|
final _emailCtrl = TextEditingController();
|
||||||
|
final _noteCtrl = TextEditingController();
|
||||||
|
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Prendiamo tutta la stringa nuda e cruda!
|
||||||
|
_nameCtrl = TextEditingController(text: widget.initialQuery.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameCtrl.dispose();
|
||||||
|
_phoneCtrl.dispose();
|
||||||
|
_emailCtrl.dispose();
|
||||||
|
_noteCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
final NavigatorState navigator = Navigator.of(context);
|
||||||
|
if (_nameCtrl.text.isEmpty) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
// Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti)
|
||||||
|
final newCustomer = await context.read<CustomerCubit>().quickCreateCustomer(
|
||||||
|
name: _nameCtrl.text.trim(),
|
||||||
|
phone: _phoneCtrl.text.trim(),
|
||||||
|
// Aggiungi questi se li hai inseriti nel tuo CustomerCubit:
|
||||||
|
// email: _emailCtrl.text.trim(),
|
||||||
|
// note: _noteCtrl.text.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
navigator.pop(newCustomer); // Restituiamo il cliente creato
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text("Nuovo Cliente Rapido"),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _nameCtrl,
|
||||||
|
autofocus: true, // Focus immediato!
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Nome / Ragione Sociale *",
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _phoneCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: "Telefono"),
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _emailCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: "Email"),
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _noteCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: "Note rapide"),
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text("Annulla"),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _save,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text("Salva e Usa"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
|
import 'package:flux/features/auth/bloc/auth_bloc.dart';
|
||||||
import 'package:flux/features/master_data/master_data_hub_content.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/blocs/services_cubit.dart';
|
||||||
import 'package:flux/features/services/ui/services_screen.dart';
|
import 'package:flux/features/services/ui/services_screen.dart';
|
||||||
import 'dashboard_content.dart'; // Importiamo il contenuto della dashboard
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'dashboard_content.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
@@ -21,8 +23,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Caricamento "silenzioso" all'avvio dell'app
|
|
||||||
// Usiamo WidgetsBinding per assicurarci che il contesto sia pronto
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<ServicesCubit>().loadServices();
|
context.read<ServicesCubit>().loadServices();
|
||||||
});
|
});
|
||||||
@@ -34,15 +34,31 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
// Se lo schermo è più largo di 900px usiamo il layout Desktop
|
|
||||||
final bool isLargeScreen = constraints.maxWidth > 900;
|
final bool isLargeScreen = constraints.maxWidth > 900;
|
||||||
|
final bool veryLargeScreen = constraints.maxWidth > 1200;
|
||||||
|
final bool isMenuExtended = veryLargeScreen ? true : _extendRailway;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
// --- APPBAR (Solo Mobile) ---
|
||||||
|
appBar: isLargeScreen
|
||||||
|
? null
|
||||||
|
: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
'FLUX Gestionale',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
child: _buildUserMenu(context, isExtended: false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
// --- SIDEBAR (Desktop) ---
|
// --- SIDEBAR (Desktop) ---
|
||||||
if (isLargeScreen)
|
if (isLargeScreen) _buildDesktopSidebar(isMenuExtended),
|
||||||
_buildNavigationRail(constraints.maxWidth > 1200),
|
|
||||||
|
|
||||||
// --- CONTENUTO DINAMICO ---
|
// --- CONTENUTO DINAMICO ---
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -61,7 +77,209 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BOTTOM NAVIGATION BAR (Mobile) ---
|
// ===========================================================================
|
||||||
|
// COMPONENTI UI
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// Costruisce l'intera colonna laterale (Rail + Menu Utente in fondo)
|
||||||
|
Widget _buildDesktopSidebar(bool isExtended) {
|
||||||
|
return MouseRegion(
|
||||||
|
// Spostiamo qui la logica dell'hover!
|
||||||
|
onEnter: (_) => setState(() => _extendRailway = true),
|
||||||
|
onExit: (_) => setState(() => _extendRailway = false),
|
||||||
|
child: Container(
|
||||||
|
color: context.background, // Mantiene lo stesso colore della Rail
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildNavigationRail(isExtended), // Ora la Rail è "nuda"
|
||||||
|
),
|
||||||
|
// --- AVATAR E MENU IN FONDO ALLA SIDEBAR ---
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 24.0, top: 8.0),
|
||||||
|
child: _buildUserMenu(context, isExtended: isExtended),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNavigationRail(bool isExtended) {
|
||||||
|
return NavigationRail(
|
||||||
|
extended: isExtended,
|
||||||
|
selectedIndex: _selectedIndex,
|
||||||
|
onDestinationSelected: (index) => setState(() => _selectedIndex = index),
|
||||||
|
backgroundColor: Colors
|
||||||
|
.transparent, // Impostato trasparente per prendere il colore del Container padre
|
||||||
|
indicatorColor: context.accent.withValues(alpha: 0.2),
|
||||||
|
leading: _buildRailHeader(isExtended),
|
||||||
|
selectedIconTheme: IconThemeData(color: context.accent, size: 28),
|
||||||
|
unselectedIconTheme: IconThemeData(
|
||||||
|
color: context.secondaryText,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
selectedLabelTextStyle: TextStyle(
|
||||||
|
color: context.accent,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
unselectedLabelTextStyle: TextStyle(color: context.secondaryText),
|
||||||
|
destinations: const [
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.dashboard_outlined),
|
||||||
|
selectedIcon: Icon(Icons.dashboard),
|
||||||
|
label: Text('Dashboard'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.receipt_long_outlined),
|
||||||
|
selectedIcon: Icon(Icons.receipt_long),
|
||||||
|
label: Text('Servizi'),
|
||||||
|
),
|
||||||
|
NavigationRailDestination(
|
||||||
|
icon: Icon(Icons.folder_shared_outlined),
|
||||||
|
selectedIcon: Icon(Icons.folder_shared),
|
||||||
|
label: Text('Anagrafiche'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MENU UTENTE (Il "Pro" Avatar) ---
|
||||||
|
Widget _buildUserMenu(BuildContext context, {required bool isExtended}) {
|
||||||
|
// Il PopupMenuButton gestisce da solo l'apertura a tendina
|
||||||
|
return PopupMenuButton<String>(
|
||||||
|
offset: const Offset(
|
||||||
|
0,
|
||||||
|
-120,
|
||||||
|
), // Apre il menu verso l'alto su desktop se necessario
|
||||||
|
tooltip: 'Profilo e Impostazioni',
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'logout') {
|
||||||
|
_showLogoutDialog(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) => [
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'profile',
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(Icons.person_outline),
|
||||||
|
title: Text('Il mio Profilo'),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuDivider(),
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'logout',
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(Icons.logout, color: Colors.red),
|
||||||
|
title: Text(
|
||||||
|
'Esci',
|
||||||
|
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// L'aspetto del pulsante (Icona tonda o Icona + Nome se esteso)
|
||||||
|
child: isExtended
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
color: context.accent.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
backgroundColor: context.accent,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.person,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
GetIt.I.get<SessionBloc>().state.company?.ragioneSociale ??
|
||||||
|
"Utente",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: CircleAvatar(
|
||||||
|
radius: 18,
|
||||||
|
backgroundColor: context.accent,
|
||||||
|
child: const Icon(Icons.person, color: Colors.white),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DIALOG DI CONFERMA LOGOUT ---
|
||||||
|
void _showLogoutDialog(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.logout, color: Colors.red),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text("Chiudi sessione"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: const Text("Sei sicuro di voler uscire dal gestionale?"),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
|
child: const Text("Annulla"),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(dialogContext); // Chiude la Dialog
|
||||||
|
context.read<AuthBloc>().add(
|
||||||
|
LogoutRequested(),
|
||||||
|
); // Esegue il logout
|
||||||
|
},
|
||||||
|
child: const Text("Esci"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... mantieni gli altri tuoi metodi intatti (_buildRailHeader, _buildPageContent, _buildBottomNavigationBar)
|
||||||
|
|
||||||
|
Widget _buildRailHeader(bool veryLargeScreen) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: veryLargeScreen
|
||||||
|
? null
|
||||||
|
: () => setState(() => _extendRailway = !_extendRailway),
|
||||||
|
child: _extendRailway
|
||||||
|
? Text(
|
||||||
|
'FLUX',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 24,
|
||||||
|
color: context.accent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Icon(Icons.bolt, color: context.accent, size: 32),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBottomNavigationBar(int selectedIndex) {
|
Widget _buildBottomNavigationBar(int selectedIndex) {
|
||||||
return BottomNavigationBar(
|
return BottomNavigationBar(
|
||||||
currentIndex: selectedIndex,
|
currentIndex: selectedIndex,
|
||||||
@@ -85,80 +303,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NAVIGATION RAIL (Desktop) ---
|
|
||||||
Widget _buildNavigationRail(bool veryLargeScreen) {
|
|
||||||
return MouseRegion(
|
|
||||||
onEnter: (_) => setState(() => _extendRailway = true),
|
|
||||||
onExit: (_) => setState(() => _extendRailway = false),
|
|
||||||
child: NavigationRail(
|
|
||||||
// Manteniamo 'extended' dinamico in base alla larghezza per un look Pro
|
|
||||||
extended: veryLargeScreen ? true : _extendRailway,
|
|
||||||
selectedIndex: _selectedIndex,
|
|
||||||
onDestinationSelected: (index) =>
|
|
||||||
setState(() => _selectedIndex = index),
|
|
||||||
backgroundColor: context.background,
|
|
||||||
indicatorColor: context.accent.withValues(alpha: 0.2),
|
|
||||||
|
|
||||||
// Header con il logo FLUX o l'icona bolt
|
|
||||||
leading: _buildRailHeader(veryLargeScreen),
|
|
||||||
|
|
||||||
selectedIconTheme: IconThemeData(color: context.accent, size: 28),
|
|
||||||
unselectedIconTheme: IconThemeData(
|
|
||||||
color: context.secondaryText,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
|
|
||||||
selectedLabelTextStyle: TextStyle(
|
|
||||||
color: context.accent,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
unselectedLabelTextStyle: TextStyle(color: context.secondaryText),
|
|
||||||
|
|
||||||
destinations: const [
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.dashboard_outlined),
|
|
||||||
selectedIcon: Icon(Icons.dashboard),
|
|
||||||
label: Text('Dashboard'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.receipt_long_outlined),
|
|
||||||
selectedIcon: Icon(Icons.receipt_long),
|
|
||||||
label: Text('Servizi'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.folder_shared_outlined),
|
|
||||||
selectedIcon: Icon(Icons.folder_shared),
|
|
||||||
label: Text(
|
|
||||||
'Anagrafiche',
|
|
||||||
), // Questo caricherà il MasterDataHubContent
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRailHeader(bool veryLargeScreen) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: veryLargeScreen
|
|
||||||
? null
|
|
||||||
: () => setState(() => _extendRailway = !_extendRailway),
|
|
||||||
child: _extendRailway
|
|
||||||
? Text(
|
|
||||||
'FLUX',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 24,
|
|
||||||
color: context.accent,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(Icons.bolt, color: context.accent, size: 32),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch tra le sottopagine
|
|
||||||
Widget _buildPageContent(int index, bool isLargeScreen) {
|
Widget _buildPageContent(int index, bool isLargeScreen) {
|
||||||
return IndexedStack(
|
return IndexedStack(
|
||||||
index: index,
|
index: index,
|
||||||
@@ -167,12 +311,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
isLargeScreen: isLargeScreen,
|
isLargeScreen: isLargeScreen,
|
||||||
onTabRequested: (idx) => setState(() => _selectedIndex = 2),
|
onTabRequested: (idx) => setState(() => _selectedIndex = 2),
|
||||||
),
|
),
|
||||||
|
const ServicesScreen(),
|
||||||
ServicesScreen(),
|
|
||||||
|
|
||||||
// L'unico punto di ingresso per tutte le anagrafiche
|
|
||||||
MasterDataHubContent(
|
MasterDataHubContent(
|
||||||
// Qui gestiamo la navigazione "interna" all'hub
|
|
||||||
onOpenPage: (widget) {
|
onOpenPage: (widget) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||||
@@ -123,4 +124,34 @@ class ProductCubit extends Cubit<ProductState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ModelModel?> quickCreateProduct({
|
||||||
|
required String brandName,
|
||||||
|
required String modelName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await loadBrands();
|
||||||
|
BrandModel? brand = state.brands.firstWhereOrNull(
|
||||||
|
(b) => b.name.toLowerCase() == brandName.toLowerCase(),
|
||||||
|
);
|
||||||
|
// 1. Cerchiamo o creiamo il Brand
|
||||||
|
// (Usa una funzione upsert o una ricerca rapida nel repository)
|
||||||
|
brand ??= await _repository.upsertBrand(
|
||||||
|
BrandModel(name: brandName, companyId: _sessionBloc.state.company!.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Creiamo il Modello legato al Brand
|
||||||
|
final newModel = await _repository.upsertModel(
|
||||||
|
ModelModel(brandId: brand.id!, name: modelName),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Aggiorniamo lo stato locale così la lista modelli lo vede subito
|
||||||
|
emit(state.copyWith(models: [newModel, ...state.models]));
|
||||||
|
|
||||||
|
return newModel;
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(errorMessage: "Errore creazione rapida: $e"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ModelModel extends Equatable {
|
|||||||
const ModelModel({
|
const ModelModel({
|
||||||
this.id,
|
this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.nameWithBrand,
|
this.nameWithBrand = '',
|
||||||
required this.brandId,
|
required this.brandId,
|
||||||
this.isActive = true,
|
this.isActive = true,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
|
|||||||
111
lib/features/master_data/products/ui/quick_product_dialog.dart
Normal file
111
lib/features/master_data/products/ui/quick_product_dialog.dart
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
||||||
|
import 'package:flux/features/master_data/products/models/brand_model.dart';
|
||||||
|
|
||||||
|
class QuickProductDialog extends StatefulWidget {
|
||||||
|
final List<BrandModel> existingBrands;
|
||||||
|
|
||||||
|
const QuickProductDialog({super.key, required this.existingBrands});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<QuickProductDialog> createState() => _QuickProductDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickProductDialogState extends State<QuickProductDialog> {
|
||||||
|
final _modelCtrl = TextEditingController();
|
||||||
|
String _selectedBrandName = "";
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
final NavigatorState navigator = Navigator.of(context);
|
||||||
|
if (_selectedBrandName.isEmpty || _modelCtrl.text.isEmpty) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
final newModel = await context.read<ProductCubit>().quickCreateProduct(
|
||||||
|
brandName: _selectedBrandName.trim(),
|
||||||
|
modelName: _modelCtrl.text.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
navigator.pop(newModel); // Restituiamo il modello creato
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text("Nuovo Dispositivo"),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// AUTOCOMPLETE PER IL BRAND LOCALE
|
||||||
|
Autocomplete<String>(
|
||||||
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
|
if (textEditingValue.text.isEmpty) {
|
||||||
|
return const Iterable<String>.empty();
|
||||||
|
}
|
||||||
|
final query = textEditingValue.text.toLowerCase();
|
||||||
|
// Filtriamo i brand che contengono la stringa cercata
|
||||||
|
return widget.existingBrands
|
||||||
|
.map((b) => b.name)
|
||||||
|
.where((name) => name.toLowerCase().contains(query));
|
||||||
|
},
|
||||||
|
onSelected: (String selection) {
|
||||||
|
_selectedBrandName = selection;
|
||||||
|
},
|
||||||
|
fieldViewBuilder:
|
||||||
|
(
|
||||||
|
context,
|
||||||
|
textEditingController,
|
||||||
|
focusNode,
|
||||||
|
onFieldSubmitted,
|
||||||
|
) {
|
||||||
|
return TextField(
|
||||||
|
controller: textEditingController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
autofocus: true,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Marca (es: Apple, Samsung)",
|
||||||
|
hintText: "Inizia a scrivere...",
|
||||||
|
),
|
||||||
|
onChanged: (val) => _selectedBrandName = val,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _modelCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Modello (es: iPhone 15 Pro)",
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onSubmitted: (_) => _save(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text("Annulla"),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _save,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text("Crea"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ class ProviderModel extends Equatable {
|
|||||||
final bool energia;
|
final bool energia;
|
||||||
final bool assicurazioni;
|
final bool assicurazioni;
|
||||||
final bool intrattenimento;
|
final bool intrattenimento;
|
||||||
|
final bool finanziamenti;
|
||||||
final bool altro;
|
final bool altro;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final String companyId;
|
final String companyId;
|
||||||
@@ -22,6 +23,7 @@ class ProviderModel extends Equatable {
|
|||||||
required this.energia,
|
required this.energia,
|
||||||
required this.assicurazioni,
|
required this.assicurazioni,
|
||||||
required this.intrattenimento,
|
required this.intrattenimento,
|
||||||
|
required this.finanziamenti,
|
||||||
required this.altro,
|
required this.altro,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
required this.companyId,
|
required this.companyId,
|
||||||
@@ -48,6 +50,7 @@ class ProviderModel extends Equatable {
|
|||||||
energia: map['energia'] ?? false,
|
energia: map['energia'] ?? false,
|
||||||
assicurazioni: map['assicurazioni'] ?? false,
|
assicurazioni: map['assicurazioni'] ?? false,
|
||||||
intrattenimento: map['intrattenimento'] ?? false,
|
intrattenimento: map['intrattenimento'] ?? false,
|
||||||
|
finanziamenti: map['finanziamenti'] ?? false,
|
||||||
altro: map['altro'] ?? false,
|
altro: map['altro'] ?? false,
|
||||||
isActive: map['is_active'] ?? true,
|
isActive: map['is_active'] ?? true,
|
||||||
companyId: map['company_id'],
|
companyId: map['company_id'],
|
||||||
@@ -63,6 +66,7 @@ class ProviderModel extends Equatable {
|
|||||||
'energia': energia,
|
'energia': energia,
|
||||||
'assicurazioni': assicurazioni,
|
'assicurazioni': assicurazioni,
|
||||||
'intrattenimento': intrattenimento,
|
'intrattenimento': intrattenimento,
|
||||||
|
'finanziamenti': finanziamenti,
|
||||||
'altro': altro,
|
'altro': altro,
|
||||||
'is_active': isActive,
|
'is_active': isActive,
|
||||||
'company_id': companyId,
|
'company_id': companyId,
|
||||||
@@ -84,6 +88,7 @@ class ProviderModel extends Equatable {
|
|||||||
energia,
|
energia,
|
||||||
assicurazioni,
|
assicurazioni,
|
||||||
intrattenimento,
|
intrattenimento,
|
||||||
|
finanziamenti,
|
||||||
altro,
|
altro,
|
||||||
isActive,
|
isActive,
|
||||||
companyId,
|
companyId,
|
||||||
@@ -98,6 +103,7 @@ class ProviderModel extends Equatable {
|
|||||||
bool? energia,
|
bool? energia,
|
||||||
bool? assicurazioni,
|
bool? assicurazioni,
|
||||||
bool? intrattenimento,
|
bool? intrattenimento,
|
||||||
|
bool? finanziamenti,
|
||||||
bool? altro,
|
bool? altro,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
String? companyId,
|
String? companyId,
|
||||||
@@ -111,6 +117,7 @@ class ProviderModel extends Equatable {
|
|||||||
energia: energia ?? this.energia,
|
energia: energia ?? this.energia,
|
||||||
assicurazioni: assicurazioni ?? this.assicurazioni,
|
assicurazioni: assicurazioni ?? this.assicurazioni,
|
||||||
intrattenimento: intrattenimento ?? this.intrattenimento,
|
intrattenimento: intrattenimento ?? this.intrattenimento,
|
||||||
|
finanziamenti: finanziamenti ?? this.finanziamenti,
|
||||||
altro: altro ?? this.altro,
|
altro: altro ?? this.altro,
|
||||||
isActive: isActive ?? this.isActive,
|
isActive: isActive ?? this.isActive,
|
||||||
companyId: companyId ?? this.companyId,
|
companyId: companyId ?? this.companyId,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
|||||||
late bool _energia;
|
late bool _energia;
|
||||||
late bool _assicurazioni;
|
late bool _assicurazioni;
|
||||||
late bool _intrattenimento;
|
late bool _intrattenimento;
|
||||||
|
late bool _finanziamenti;
|
||||||
late bool _altro;
|
late bool _altro;
|
||||||
late bool _isActive;
|
late bool _isActive;
|
||||||
final List<String> _tempSelectedStoreIds =
|
final List<String> _tempSelectedStoreIds =
|
||||||
@@ -38,6 +39,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
|||||||
_energia = p?.energia ?? false;
|
_energia = p?.energia ?? false;
|
||||||
_assicurazioni = p?.assicurazioni ?? false;
|
_assicurazioni = p?.assicurazioni ?? false;
|
||||||
_intrattenimento = p?.intrattenimento ?? false;
|
_intrattenimento = p?.intrattenimento ?? false;
|
||||||
|
_finanziamenti = p?.finanziamenti ?? false;
|
||||||
_altro = p?.altro ?? false;
|
_altro = p?.altro ?? false;
|
||||||
_isActive = p?.isActive ?? true;
|
_isActive = p?.isActive ?? true;
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
|||||||
energia: _energia,
|
energia: _energia,
|
||||||
assicurazioni: _assicurazioni,
|
assicurazioni: _assicurazioni,
|
||||||
intrattenimento: _intrattenimento,
|
intrattenimento: _intrattenimento,
|
||||||
|
finanziamenti: _finanziamenti,
|
||||||
altro: _altro,
|
altro: _altro,
|
||||||
isActive: _isActive,
|
isActive: _isActive,
|
||||||
companyId:
|
companyId:
|
||||||
@@ -130,6 +133,11 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
|||||||
_intrattenimento,
|
_intrattenimento,
|
||||||
(v) => setState(() => _intrattenimento = v),
|
(v) => setState(() => _intrattenimento = v),
|
||||||
),
|
),
|
||||||
|
_buildSwitch(
|
||||||
|
"Finanziamenti",
|
||||||
|
_finanziamenti,
|
||||||
|
(v) => setState(() => _finanziamenti = v),
|
||||||
|
),
|
||||||
_buildSwitch(
|
_buildSwitch(
|
||||||
"Altro/Accessori",
|
"Altro/Accessori",
|
||||||
_altro,
|
_altro,
|
||||||
|
|||||||
@@ -199,22 +199,24 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
|
|
||||||
// --- PERSISTENZA ---
|
// --- PERSISTENZA ---
|
||||||
|
|
||||||
Future<void> saveCurrentService() async {
|
Future<void> saveCurrentService({required bool isBozza}) async {
|
||||||
if (state.currentService == null) return;
|
if (state.currentService == null) return;
|
||||||
|
|
||||||
emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null));
|
emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null));
|
||||||
try {
|
try {
|
||||||
// Usiamo il repository corazzato che abbiamo scritto prima
|
// 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente
|
||||||
await _repository.saveFullService(state.currentService!);
|
final serviceToSave = state.currentService!.copyWith(isBozza: isBozza);
|
||||||
|
|
||||||
await loadServices(refresh: true);
|
// 2. Salvataggio corazzato
|
||||||
// Reset della bozza e ricaricamento lista
|
await _repository.saveFullService(serviceToSave);
|
||||||
|
|
||||||
|
// 3. Reset e ricaricamento
|
||||||
emit(state.copyWith(status: ServicesStatus.saved, currentService: null));
|
emit(state.copyWith(status: ServicesStatus.saved, currentService: null));
|
||||||
|
await loadServices(refresh: true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: ServicesStatus.failure,
|
status: ServicesStatus.failure,
|
||||||
|
|
||||||
errorMessage: e.toString(),
|
errorMessage: e.toString(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import '../models/service_model.dart';
|
import '../models/service_model.dart';
|
||||||
|
|
||||||
@@ -187,4 +188,28 @@ class ServicesRepository {
|
|||||||
]; // Fallback se non c'è ancora storia
|
]; // Fallback se non c'è ancora storia
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> uploadAttachment({
|
||||||
|
required String serviceId,
|
||||||
|
required String fileName,
|
||||||
|
required Uint8List fileBytes,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 1. Upload fisico nel bucket 'service_documents'
|
||||||
|
final path = '$serviceId/$fileName';
|
||||||
|
await _supabase.storage
|
||||||
|
.from('service_documents')
|
||||||
|
.uploadBinary(path, fileBytes);
|
||||||
|
|
||||||
|
// 2. Registriamo l'esistenza del file nel database
|
||||||
|
await _supabase.from('service_attachment').insert({
|
||||||
|
'service_id': serviceId,
|
||||||
|
'file_path': path,
|
||||||
|
'file_name': fileName,
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw "Errore upload: $e";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ class _EntertainmentList extends StatelessWidget {
|
|||||||
telefoniaFissa: false,
|
telefoniaFissa: false,
|
||||||
telefoniaMobile: false,
|
telefoniaMobile: false,
|
||||||
assicurazioni: false,
|
assicurazioni: false,
|
||||||
|
finanziamenti: false,
|
||||||
altro: false,
|
altro: false,
|
||||||
intrattenimento: false,
|
intrattenimento: false,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ class _FinanceList extends StatelessWidget {
|
|||||||
assicurazioni: false,
|
assicurazioni: false,
|
||||||
altro: false,
|
altro: false,
|
||||||
intrattenimento: false,
|
intrattenimento: false,
|
||||||
|
finanziamenti: false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.nome;
|
.nome;
|
||||||
@@ -292,12 +293,13 @@ class _FinanceFormState extends State<_FinanceForm> {
|
|||||||
// 1. SCELTA ISTITUTO (Solo attivi)
|
// 1. SCELTA ISTITUTO (Solo attivi)
|
||||||
BlocBuilder<ProvidersCubit, ProvidersState>(
|
BlocBuilder<ProvidersCubit, ProvidersState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final finProviders = state
|
final finProviders = state.activeProviders
|
||||||
.activeProviders; // Già filtrati dal caricamento della dialog
|
.where((p) => p.finanziamenti)
|
||||||
|
.toList(); // Già filtrati dal caricamento della dialog
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
initialValue: _selectedProviderId,
|
initialValue: _selectedProviderId,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: "Istituto di Credito",
|
labelText: "Gestore",
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
items: finProviders
|
items: finProviders
|
||||||
|
|||||||
@@ -1,16 +1,46 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen/customer_section.dart';
|
import 'package:flux/features/services/ui/service_form_screen/customer_section.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen/general_info_section.dart';
|
import 'package:flux/features/services/ui/service_form_screen/general_info_section.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen/services_grid.dart';
|
import 'package:flux/features/services/ui/service_form_screen/services_grid.dart';
|
||||||
|
|
||||||
class ServiceFormScreen extends StatelessWidget {
|
class ServiceFormScreen extends StatefulWidget {
|
||||||
const ServiceFormScreen({super.key});
|
final String? serviceId;
|
||||||
|
final ServiceModel? existingService; // <-- AGGIUNTO
|
||||||
|
|
||||||
|
const ServiceFormScreen({
|
||||||
|
super.key,
|
||||||
|
this.serviceId,
|
||||||
|
this.existingService, // <-- AGGIUNTO
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ServiceFormScreen> createState() => _ServiceFormScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// Diamo in pasto al Cubit tutto quello che abbiamo!
|
||||||
|
context.read<ServicesCubit>().initServiceForm(
|
||||||
|
existingService: widget.existingService,
|
||||||
|
serviceId: widget.serviceId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _performSave(BuildContext context, {required bool isBozza}) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
context.read<ServicesCubit>().saveCurrentService(isBozza: isBozza);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<ServicesCubit, ServicesState>(
|
return BlocConsumer<ServicesCubit, ServicesState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.status == ServicesStatus.saved) {
|
if (state.status == ServicesStatus.saved) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -19,91 +49,123 @@ class ServiceFormScreen extends StatelessWidget {
|
|||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Navigator.pop(context); // Torna alla lista di pratiche
|
Navigator.pop(context);
|
||||||
} else if (state.status == ServicesStatus.failure) {
|
} else if (state.status == ServicesStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text("Errore: ${state.errorMessage ?? ''}"),
|
||||||
"Si è verificato un errore ${state.errorMessage ?? ''}",
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text("Nuova Pratica"),
|
|
||||||
actions: [
|
|
||||||
_SaveButton(), // Tasto salva intelligente
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: BlocBuilder<ServicesCubit, ServicesState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
final service = state.currentService;
|
|
||||||
|
|
||||||
// Se la bozza non è ancora inizializzata, mostriamo un loader
|
|
||||||
if (service == null) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// SEZIONE 1: CLIENTE
|
|
||||||
CustomerSection(service: service),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// SEZIONE 2: INFO GENERALI
|
|
||||||
GeneralInfoSection(service: service),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// SEZIONE 3: I MODULI
|
|
||||||
ServicesGrid(service: service),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// TODO SEZIONE 4: ALLEGATI (Da fare)
|
|
||||||
// const _AttachmentsSection(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- COMPONENTI DELLA PAGINA ---
|
|
||||||
|
|
||||||
class _SaveButton extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<ServicesCubit, ServicesState>(
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.status == ServicesStatus.saving) {
|
final service = state.currentService;
|
||||||
return const Padding(
|
final isSaving = state.status == ServicesStatus.saving;
|
||||||
padding: EdgeInsets.all(16.0),
|
final isEditMode = widget.serviceId != null;
|
||||||
child: SizedBox(
|
|
||||||
width: 20,
|
return Scaffold(
|
||||||
height: 20,
|
appBar: AppBar(
|
||||||
child: CircularProgressIndicator(
|
title: Text(isEditMode ? "Modifica Pratica" : "Nuova Pratica"),
|
||||||
strokeWidth: 2,
|
actions: [
|
||||||
color: Colors.white,
|
if (isSaving)
|
||||||
),
|
const Padding(
|
||||||
),
|
padding: EdgeInsets.only(right: 20.0),
|
||||||
);
|
child: Center(
|
||||||
}
|
child: SizedBox(
|
||||||
return IconButton(
|
width: 20,
|
||||||
icon: const Icon(Icons.save),
|
height: 20,
|
||||||
tooltip: "Salva Pratica",
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
onPressed: () {
|
),
|
||||||
context.read<ServicesCubit>().saveCurrentService();
|
),
|
||||||
},
|
)
|
||||||
|
else if (service != null) ...[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_note),
|
||||||
|
tooltip: "Salva come Bozza",
|
||||||
|
onPressed: () => _performSave(context, isBozza: true),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.check_circle_outline,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
tooltip: "Conferma Pratica",
|
||||||
|
onPressed: () => _performSave(context, isBozza: false),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: (service == null)
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CustomerSection(service: service),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
GeneralInfoSection(service: service),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
ServicesGrid(service: service),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// TODO: _AttachmentsSection(),
|
||||||
|
_buildBottomActionButtons(context, isSaving: isSaving),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomActionButtons(
|
||||||
|
BuildContext context, {
|
||||||
|
required bool isSaving,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.edit_note),
|
||||||
|
label: const Text("Salva in Bozza"),
|
||||||
|
onPressed: isSaving
|
||||||
|
? null
|
||||||
|
: () => _performSave(context, isBozza: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.green.shade600,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.check_circle_outline),
|
||||||
|
label: const Text(
|
||||||
|
"CONFERMA PRATICA",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1),
|
||||||
|
),
|
||||||
|
onPressed: isSaving
|
||||||
|
? null
|
||||||
|
: () => _performSave(context, isBozza: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,9 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
|||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => context.pushNamed(
|
onTap: () => context.pushNamed(
|
||||||
'service-form',
|
'service-form',
|
||||||
queryParameters: {'serviceId': service.id},
|
extra: service, // <-- LA MAGIA È QUI: Passa l'oggetto intero!
|
||||||
|
// Teniamo anche il parametro URL per coerenza di routing
|
||||||
|
queryParameters: service.id != null ? {'serviceId': service.id!} : {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user