refactor providers e basi per spedizioni

This commit is contained in:
2026-05-15 10:12:05 +02:00
parent ad35f641b3
commit f19f19a279
21 changed files with 1542 additions and 830 deletions

View 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),
),
);
},
),
],
),
),
);
}
}

View File

@@ -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,
);
}
}

View 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,
);
}
}
},
);
},
);
},
),
),
],
),
);
}
}

View File

@@ -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'),
),
],
);
}
}

View File

@@ -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),
),
);
}
}