refactor providers e basi per spedizioni
This commit is contained in:
395
lib/features/master_data/providers/ui/provider_form_screen.dart
Normal file
395
lib/features/master_data/providers/ui/provider_form_screen.dart
Normal file
@@ -0,0 +1,395 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/features/master_data/providers/blocs/provider_form_cubit.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_location_model.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_role.dart';
|
||||
import 'package:flux/features/master_data/providers/ui/provider_location_dialog.dart';
|
||||
|
||||
class ProviderFormScreen extends StatefulWidget {
|
||||
final ProviderModel? existingProvider;
|
||||
|
||||
const ProviderFormScreen({super.key, this.existingProvider});
|
||||
|
||||
@override
|
||||
State<ProviderFormScreen> createState() => _ProviderFormScreenState();
|
||||
}
|
||||
|
||||
class _ProviderFormScreenState extends State<ProviderFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Controllers per i campi di testo
|
||||
late final TextEditingController _nameCtrl;
|
||||
late final TextEditingController _businessNameCtrl;
|
||||
late final TextEditingController _vatCtrl;
|
||||
late final TextEditingController _cfCtrl;
|
||||
late final TextEditingController _sdiCtrl;
|
||||
late final TextEditingController _pecCtrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final p = widget.existingProvider;
|
||||
|
||||
_nameCtrl = TextEditingController(text: p?.name);
|
||||
_businessNameCtrl = TextEditingController(text: p?.businessName);
|
||||
_vatCtrl = TextEditingController(text: p?.vatNumber);
|
||||
_cfCtrl = TextEditingController(text: p?.fiscalCode);
|
||||
_sdiCtrl = TextEditingController(text: p?.sdiCode);
|
||||
_pecCtrl = TextEditingController(text: p?.emailPec);
|
||||
|
||||
// Inizializziamo il Cubit appena la schermata si apre
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Recupero il companyId dall'utente loggato (Vigile Urbano)
|
||||
final companyId = context
|
||||
.read<SessionCubit>()
|
||||
.state
|
||||
.currentStore!
|
||||
.companyId;
|
||||
|
||||
context.read<ProviderFormCubit>().initForm(
|
||||
companyId: companyId,
|
||||
existingProvider: widget.existingProvider,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_businessNameCtrl.dispose();
|
||||
_vatCtrl.dispose();
|
||||
_cfCtrl.dispose();
|
||||
_sdiCtrl.dispose();
|
||||
_pecCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _flushControllers() {
|
||||
context.read<ProviderFormCubit>().updateFields(
|
||||
name: _nameCtrl.text.trim(),
|
||||
businessName: _businessNameCtrl.text.trim(),
|
||||
vatNumber: _vatCtrl.text.trim(),
|
||||
fiscalCode: _cfCtrl.text.trim(),
|
||||
sdiCode: _sdiCtrl.text.trim(),
|
||||
emailPec: _pecCtrl.text.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEditing = widget.existingProvider != null;
|
||||
|
||||
return BlocConsumer<ProviderFormCubit, ProviderFormState>(
|
||||
listenWhen: (previous, current) => previous.status != current.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == ProviderFormStatus.success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Fornitore salvato con successo!')),
|
||||
);
|
||||
Navigator.of(context).pop(); // Torna alla lista
|
||||
} else if (state.status == ProviderFormStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Errore di salvataggio'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.status == ProviderFormStatus.loading &&
|
||||
state.provider.id == null) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isEditing ? 'Modifica Fornitore' : 'Nuovo Fornitore'),
|
||||
actions: [
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_flushControllers();
|
||||
context.read<ProviderFormCubit>().save();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('Salva'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildGeneralCard(context, state),
|
||||
const SizedBox(height: 24),
|
||||
_buildRolesCard(context, state),
|
||||
const SizedBox(height: 24),
|
||||
_buildFiscalCard(context),
|
||||
const SizedBox(height: 24),
|
||||
_buildStoresCard(context, state),
|
||||
const SizedBox(height: 24),
|
||||
_buildLocationsCard(context, state),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- CARD 1: DATI GENERALI ---
|
||||
Widget _buildGeneralCard(BuildContext context, ProviderFormState state) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Dati Generali',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nome Fornitore (Display Name) *',
|
||||
prefixIcon: Icon(Icons.storefront),
|
||||
),
|
||||
validator: (val) =>
|
||||
val == null || val.isEmpty ? 'Campo obbligatorio' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Volendo qui puoi aggiungere lo Switch per "Attivo/Inattivo"
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- CARD 2: RUOLI (I CHIPS NINJA) ---
|
||||
Widget _buildRolesCard(BuildContext context, ProviderFormState state) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Ruoli e Servizi',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(
|
||||
'Seleziona cosa fa questo fornitore (puoi sceglierne più di uno):',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: ProviderRole.values.map((role) {
|
||||
final isSelected = state.provider.roles.contains(role);
|
||||
return FilterChip(
|
||||
label: Text(role.displayValue),
|
||||
selected: isSelected,
|
||||
selectedColor: role.color.withValues(alpha: 0.2),
|
||||
checkmarkColor: role.color,
|
||||
side: BorderSide(color: role.color.withValues(alpha: 0.3)),
|
||||
onSelected: (bool selected) {
|
||||
context.read<ProviderFormCubit>().toggleRole(role);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- CARD 3: DATI FISCALI ---
|
||||
Widget _buildFiscalCard(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Dati Fiscali (Per DDT e Fatturazione)',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _businessNameCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ragione Sociale (es. Tech SpA)',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _vatCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Partita IVA'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _cfCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Codice Fiscale',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _sdiCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Codice SDI'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _pecCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Email PEC'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- CARD 4: NEGOZI ABILITATI ---
|
||||
Widget _buildStoresCard(BuildContext context, ProviderFormState state) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Negozi Abilitati',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(
|
||||
'In quali punti vendita deve apparire questo fornitore?',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (state.availableStores.isEmpty)
|
||||
const Center(
|
||||
child: Text(
|
||||
'Nessun negozio trovato.',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
)
|
||||
else
|
||||
...state.availableStores.map((storeMap) {
|
||||
final storeId = storeMap['id'] as String;
|
||||
final storeName = storeMap['name'] as String;
|
||||
final isEnabled = state.selectedStoreIds.contains(storeId);
|
||||
|
||||
return SwitchListTile(
|
||||
title: Text(storeName),
|
||||
value: isEnabled,
|
||||
onChanged: (bool val) {
|
||||
context.read<ProviderFormCubit>().toggleStore(storeId);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocationsCard(BuildContext context, ProviderFormState state) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Sedi e Laboratori',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
onPressed: () async {
|
||||
ProviderFormCubit providerFormCubit = context
|
||||
.read<ProviderFormCubit>();
|
||||
final res = await showDialog<ProviderLocationModel?>(
|
||||
context: context,
|
||||
builder: (context) => const ProviderLocationDialog(),
|
||||
);
|
||||
if (res != null) {
|
||||
// Chiama il cubit per aggiungere localmente
|
||||
providerFormCubit.addLocationLocal(res);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add_location_alt),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (state.localLocations.isEmpty)
|
||||
const Text(
|
||||
'Nessun indirizzo di spedizione inserito.',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
)
|
||||
else
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: state.localLocations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final loc = state.localLocations[index];
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
loc.isMain ? Icons.star : Icons.location_on,
|
||||
color: loc.isMain ? Colors.amber : null,
|
||||
),
|
||||
title: Text(loc.name),
|
||||
subtitle: Text(
|
||||
'${loc.address}, ${loc.city} (${loc.province})',
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () => context
|
||||
.read<ProviderFormCubit>()
|
||||
.removeLocationLocal(index),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
|
||||
|
||||
class ProviderFormSheet extends StatefulWidget {
|
||||
final ProviderModel? initialProvider;
|
||||
|
||||
const ProviderFormSheet({super.key, this.initialProvider});
|
||||
|
||||
@override
|
||||
State<ProviderFormSheet> createState() => _ProviderFormSheetState();
|
||||
}
|
||||
|
||||
class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
||||
late TextEditingController _nameController;
|
||||
late bool _landline;
|
||||
late bool _mobile;
|
||||
late bool _energy;
|
||||
late bool _insurance;
|
||||
late bool _entertainment;
|
||||
late bool _financing;
|
||||
late bool _telepass;
|
||||
late bool _other;
|
||||
late bool _isActive;
|
||||
final List<String> _tempSelectedStoreIds =
|
||||
[]; // Per gestire la selezione temporanea dei negozi
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final p = widget.initialProvider;
|
||||
for (final store in p?.associatedStores ?? []) {
|
||||
_tempSelectedStoreIds.add(store.id!);
|
||||
}
|
||||
_nameController = TextEditingController(text: p?.name ?? '');
|
||||
_landline = p?.landline ?? false;
|
||||
_mobile = p?.mobile ?? false;
|
||||
_energy = p?.energy ?? false;
|
||||
_insurance = p?.insurance ?? false;
|
||||
_entertainment = p?.entertainment ?? false;
|
||||
_financing = p?.financing ?? false;
|
||||
_telepass = p?.telepass ?? false;
|
||||
_other = p?.other ?? false;
|
||||
_isActive = p?.isActive ?? true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _save() {
|
||||
if (_nameController.text.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
final cubit = context.read<ProvidersCubit>();
|
||||
final provider = ProviderModel(
|
||||
id: widget.initialProvider?.id, // Se nullo, Supabase farà insert
|
||||
name: _nameController.text.trim(),
|
||||
landline: _landline,
|
||||
mobile: _mobile,
|
||||
energy: _energy,
|
||||
insurance: _insurance,
|
||||
entertainment: _entertainment,
|
||||
financing: _financing,
|
||||
telepass: _telepass,
|
||||
other: _other,
|
||||
isActive: _isActive,
|
||||
companyId:
|
||||
'', // Verrà ignorato dal toMap e gestito dal Cubit/SessionBloc se hai messo la logica lì
|
||||
);
|
||||
cubit.saveProvider(provider, _tempSelectedStoreIds);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(
|
||||
context,
|
||||
).viewInsets.bottom, // Gestisce la tastiera
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.initialProvider == null
|
||||
? "Nuovo Provider"
|
||||
: "Modifica Provider",
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Nome Gestore/Brand",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Servizi Abilitati",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Energia (Luce/Gas)",
|
||||
_energy,
|
||||
(v) => setState(() => _energy = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Telefonia Fissa",
|
||||
_landline,
|
||||
(v) => setState(() => _landline = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Telefonia Mobile",
|
||||
_mobile,
|
||||
(v) => setState(() => _mobile = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Assicurazioni",
|
||||
_insurance,
|
||||
(v) => setState(() => _insurance = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Intrattenimento",
|
||||
_entertainment,
|
||||
(v) => setState(() => _entertainment = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Finanziamenti",
|
||||
_financing,
|
||||
(v) => setState(() => _financing = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Telepass",
|
||||
_telepass,
|
||||
(v) => setState(() => _telepass = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Altro/Accessori",
|
||||
_other,
|
||||
(v) => setState(() => _other = v),
|
||||
),
|
||||
const Divider(),
|
||||
_buildSwitch(
|
||||
"Stato Attivo",
|
||||
_isActive,
|
||||
(v) => setState(() => _isActive = v),
|
||||
),
|
||||
const Divider(),
|
||||
const Text(
|
||||
"Abilita nei Negozi",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Qui usiamo un BlocBuilder per prendere la lista di tutti i negozi della company
|
||||
BlocBuilder<StoreCubit, StoreState>(
|
||||
builder: (context, storeState) {
|
||||
return Column(
|
||||
children: storeState.stores.map((store) {
|
||||
final isAssociated = _tempSelectedStoreIds.contains(
|
||||
store.id,
|
||||
);
|
||||
return CheckboxListTile(
|
||||
title: Text(store.name),
|
||||
value: isAssociated,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
if (val == true) {
|
||||
_tempSelectedStoreIds.add(store.id!);
|
||||
} else {
|
||||
_tempSelectedStoreIds.remove(store.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(50),
|
||||
),
|
||||
onPressed: _save,
|
||||
child: const Text("SALVA ANAGRAFICA"),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitch(String title, bool value, Function(bool) onChanged) {
|
||||
return SwitchListTile(
|
||||
title: Text(title),
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
}
|
||||
}
|
||||
221
lib/features/master_data/providers/ui/provider_list_screen.dart
Normal file
221
lib/features/master_data/providers/ui/provider_list_screen.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/routes/routes.dart';
|
||||
import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_role.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ProviderListScreen extends StatefulWidget {
|
||||
const ProviderListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ProviderListScreen> createState() => _ProviderListScreenState();
|
||||
}
|
||||
|
||||
class _ProviderListScreenState extends State<ProviderListScreen> {
|
||||
// Filtro attivo (null = tutti)
|
||||
ProviderRole? _selectedFilter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Chiamiamo il refresh quando entriamo (il currentStore serve per caricare quelli giusti)
|
||||
final storeId = context.read<SessionCubit>().state.currentStore?.id;
|
||||
if (storeId != null) {
|
||||
context.read<ProviderListCubit>().loadProviders(storeId);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = MediaQuery.of(context).size.width > 800;
|
||||
|
||||
// --- COSTRUIAMO I CHIP DEI FILTRI CON I COLORI ---
|
||||
final filterChipsWidgets = [
|
||||
FilterChip(
|
||||
label: const Text(
|
||||
'Tutti',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
selected: _selectedFilter == null,
|
||||
onSelected: (val) => setState(() => _selectedFilter = null),
|
||||
),
|
||||
...ProviderRole.values.map((role) {
|
||||
return FilterChip(
|
||||
label: Text(role.displayValue),
|
||||
selected: _selectedFilter == role,
|
||||
// Un po' di trasparenza al colore selezionato per non accecare
|
||||
selectedColor: role.color.withValues(alpha: 0.2),
|
||||
checkmarkColor: role.color,
|
||||
// Bordo leggermente colorato per dare un hint visuale anche da spento
|
||||
side: BorderSide(color: role.color.withValues(alpha: 0.3)),
|
||||
onSelected: (val) {
|
||||
setState(() => _selectedFilter = val ? role : null);
|
||||
},
|
||||
);
|
||||
}),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Gestione Fornitori')),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => context.pushNamed(Routes.providerForm),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Nuovo Fornitore'),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// --- BARRA DEI FILTRI INTELLIGENTE ---
|
||||
if (isDesktop)
|
||||
// Desktop: Wrap multilinea con un bel padding
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: filterChipsWidgets,
|
||||
),
|
||||
)
|
||||
else
|
||||
// Mobile: Scorrimento orizzontale compatto
|
||||
SizedBox(
|
||||
height: 60,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
itemCount: filterChipsWidgets.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||
itemBuilder: (context, index) => filterChipsWidgets[index],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
// --- LISTA FORNITORI ---
|
||||
Expanded(
|
||||
child: BlocBuilder<ProviderListCubit, ProviderListState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == ProviderListStatus.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state.status == ProviderListStatus.failure) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Errore: ${state.errorMessage}',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final displayList = _selectedFilter == null
|
||||
? state.providers
|
||||
: state.providers
|
||||
.where((p) => p.roles.contains(_selectedFilter))
|
||||
.toList();
|
||||
|
||||
if (displayList.isEmpty) {
|
||||
return const Center(child: Text('Nessun fornitore trovato.'));
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: displayList.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final provider = displayList[index];
|
||||
|
||||
// --- I CHIP COLORATI DELLA LISTA ---
|
||||
final roleChips = Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: provider.roles
|
||||
.map(
|
||||
(r) => Chip(
|
||||
label: Text(
|
||||
r.displayValue,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: r.color.withValues(
|
||||
alpha: 0.9,
|
||||
), // Testo colorato!
|
||||
),
|
||||
),
|
||||
backgroundColor: r.color.withValues(
|
||||
alpha: 0.1,
|
||||
), // Sfondo pastello
|
||||
side: BorderSide(
|
||||
color: r.color.withValues(alpha: 0.2),
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
provider.name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: provider.isActive ? null : Colors.grey,
|
||||
decoration: provider.isActive
|
||||
? null
|
||||
: TextDecoration.lineThrough,
|
||||
),
|
||||
),
|
||||
if (isDesktop) ...[
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: roleChips),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: isDesktop
|
||||
? null
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: roleChips,
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
await context.pushNamed(
|
||||
Routes.providerForm,
|
||||
extra: provider,
|
||||
);
|
||||
if (context.mounted) {
|
||||
final storeId = context
|
||||
.read<SessionCubit>()
|
||||
.state
|
||||
.currentStore
|
||||
?.id;
|
||||
if (storeId != null) {
|
||||
context.read<ProviderListCubit>().loadProviders(
|
||||
storeId,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/provider_location_model.dart';
|
||||
|
||||
class ProviderLocationDialog extends StatefulWidget {
|
||||
final ProviderLocationModel? initialLocation;
|
||||
|
||||
const ProviderLocationDialog({super.key, this.initialLocation});
|
||||
|
||||
@override
|
||||
State<ProviderLocationDialog> createState() => _ProviderLocationDialogState();
|
||||
}
|
||||
|
||||
class _ProviderLocationDialogState extends State<ProviderLocationDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _nameCtrl;
|
||||
late final TextEditingController _addressCtrl;
|
||||
late final TextEditingController _cityCtrl;
|
||||
late final TextEditingController _zipCtrl;
|
||||
late final TextEditingController _provCtrl;
|
||||
late final TextEditingController _contactCtrl;
|
||||
bool _isMain = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final l = widget.initialLocation;
|
||||
_nameCtrl = TextEditingController(text: l?.name);
|
||||
_addressCtrl = TextEditingController(text: l?.address);
|
||||
_cityCtrl = TextEditingController(text: l?.city);
|
||||
_zipCtrl = TextEditingController(text: l?.zipCode);
|
||||
_provCtrl = TextEditingController(text: l?.province);
|
||||
_contactCtrl = TextEditingController(text: l?.contactPerson);
|
||||
_isMain = l?.isMain ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
widget.initialLocation == null
|
||||
? 'Aggiungi Sede/Laboratorio'
|
||||
: 'Modifica Sede',
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _nameCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nome Sede (es. Laboratorio Sud) *',
|
||||
),
|
||||
validator: (v) => v!.isEmpty ? 'Obbligatorio' : null,
|
||||
),
|
||||
TextFormField(
|
||||
controller: _addressCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Indirizzo *'),
|
||||
validator: (v) => v!.isEmpty ? 'Obbligatorio' : null,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
controller: _cityCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Città *'),
|
||||
validator: (v) => v!.isEmpty ? 'Obbligatorio' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _provCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Prov.'),
|
||||
maxLength: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextFormField(
|
||||
controller: _zipCtrl,
|
||||
decoration: const InputDecoration(labelText: 'CAP *'),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) => v!.isEmpty ? 'Obbligatorio' : null,
|
||||
),
|
||||
TextFormField(
|
||||
controller: _contactCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Referente (opzionale)',
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Sede Principale'),
|
||||
value: _isMain,
|
||||
onChanged: (v) => setState(() => _isMain = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annulla'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
ProviderLocationModel newLocation = ProviderLocationModel(
|
||||
id: widget.initialLocation?.id,
|
||||
name: _nameCtrl.text.trim(),
|
||||
address: _addressCtrl.text.trim(),
|
||||
city: _cityCtrl.text.trim(),
|
||||
zipCode: _zipCtrl.text.trim(),
|
||||
province: _provCtrl.text.trim().toUpperCase(),
|
||||
contactPerson: _contactCtrl.text.trim(),
|
||||
isMain: _isMain,
|
||||
companyId: widget.initialLocation?.companyId ?? '',
|
||||
providerId: widget.initialLocation?.providerId ?? '',
|
||||
);
|
||||
// Restituiamo una mappa o un modello parziale (senza ID e FK che gestirà il Cubit)
|
||||
Navigator.pop(context, newLocation);
|
||||
}
|
||||
},
|
||||
child: const Text('Conferma'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||
import 'package:flux/features/master_data/providers/ui/provider_form_sheet.dart';
|
||||
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
|
||||
|
||||
class ProvidersMasterDataScreen extends StatefulWidget {
|
||||
const ProvidersMasterDataScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ProvidersMasterDataScreen> createState() =>
|
||||
_ProvidersMasterDataScreenState();
|
||||
}
|
||||
|
||||
class _ProvidersMasterDataScreenState extends State<ProvidersMasterDataScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Anagrafica Provider")),
|
||||
body: BlocBuilder<ProvidersCubit, ProvidersState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading && state.allProviders.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state.allProviders.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Un'icona grande e stilizzata
|
||||
Icon(
|
||||
Icons.handshake_outlined,
|
||||
size: 80,
|
||||
color: Colors.indigo.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
"Nessun Provider configurato",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
"Aggiungi i partner con cui collabori (es. Enel, WindTre, ecc.) per poter gestire i servizi e i mandati nei tuoi negozi.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// Un bel bottone centrato per chi non vuole usare il FAB in basso
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showProviderForm(context, null),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("AGGIUNGI IL PRIMO PROVIDER"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: state.allProviders.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final provider = state.allProviders[index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: provider.isActive
|
||||
? Colors.green.shade100
|
||||
: Colors.grey.shade300,
|
||||
child: Icon(
|
||||
Icons.business,
|
||||
color: provider.isActive ? Colors.green : Colors.grey,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
provider.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: _buildCardSubtitle(
|
||||
provider,
|
||||
), // Una funzione che costruisce il sottotitolo con i badge
|
||||
trailing: const Icon(Icons.edit_outlined),
|
||||
onTap: () => _showProviderForm(context, provider),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showProviderForm(context, null),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardSubtitle(ProviderModel provider) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildProviderBadges(provider), // I badge che abbiamo fatto prima
|
||||
const SizedBox(height: 4),
|
||||
BlocBuilder<ProvidersCubit, ProvidersState>(
|
||||
builder: (context, state) {
|
||||
// Un piccolo testo che indica il numero di store associati
|
||||
// Nota: Dovrai assicurarti che il Cubit carichi queste info
|
||||
return Text(
|
||||
"Disponibile in ${provider.associatedStores.length} negozi",
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.indigo.withValues(alpha: 0.7),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Visualizza i servizi abilitati per quel provider nella lista
|
||||
Widget _buildProviderBadges(ProviderModel p) {
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (p.landline || p.mobile) _smallTag("📞 Tel", Colors.blue),
|
||||
if (p.energy) _smallTag("⚡ Energy", Colors.orange),
|
||||
if (p.insurance) _smallTag("🛡️ Assic", Colors.teal),
|
||||
if (p.entertainment) _smallTag("📺 Ent", Colors.red),
|
||||
if (p.financing) _smallTag("💰 Fin", Colors.purple),
|
||||
if (p.telepass) _smallTag("🏎️ Telepass", Colors.yellow),
|
||||
if (p.other) _smallTag("📦 Altro", Colors.grey),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _smallTag(String label, Color color) {
|
||||
return Text(
|
||||
label,
|
||||
style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.w600),
|
||||
);
|
||||
}
|
||||
|
||||
// DIALOG PER INSERIMENTO/MODIFICA
|
||||
void _showProviderForm(BuildContext context, ProviderModel? provider) {
|
||||
final providersCubit = context.read<ProvidersCubit>();
|
||||
final storeCubit = context.read<StoreCubit>();
|
||||
// Implementeremo qui il form con i vari SwitchListTile
|
||||
// Per ora facciamo un segnaposto o passiamo a scriverlo seriamente
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (modalContext) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: providersCubit),
|
||||
BlocProvider.value(value: storeCubit),
|
||||
],
|
||||
child: ProviderFormSheet(initialProvider: provider),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user