ottimo punto sembra funzionare tutto, devo solo aggiungere l'aggiunta di un cliente volante, di un modello volante e gestire i file allegati

This commit is contained in:
2026-04-18 19:03:49 +02:00
parent bbb9729ca4
commit e9f3327f31
16 changed files with 665 additions and 96 deletions

View File

@@ -0,0 +1,392 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_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/services/data/services_repository.dart';
import 'package:flux/features/services/models/entertainment_service_model.dart';
import 'package:get_it/get_it.dart';
class EntertainmentServiceDialog extends StatefulWidget {
final List<EntertainmentServiceModel> initialServices;
final String currentStoreId;
const EntertainmentServiceDialog({
super.key,
required this.initialServices,
required this.currentStoreId,
});
@override
State<EntertainmentServiceDialog> createState() =>
_EntertainmentServiceDialogState();
}
class _EntertainmentServiceDialogState
extends State<EntertainmentServiceDialog> {
late List<EntertainmentServiceModel> _tempList;
bool _isAddingNew = false;
@override
void initState() {
super.initState();
_tempList = List.from(widget.initialServices);
// Carichiamo i provider attivi per lo store corrente
context.read<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId,
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(
Icons.movie_filter_outlined,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(_isAddingNew ? "Nuovo Servizio" : "Servizi Intrattenimento"),
],
),
content: AnimatedSize(
duration: const Duration(milliseconds: 300),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: _isAddingNew
? _EntertainmentForm(
// Il form che abbiamo creato prima
onSave: (newService) => setState(() {
_tempList.add(newService);
_isAddingNew = false;
}),
onCancel: () => setState(() => _isAddingNew = false),
)
: BlocBuilder<ProvidersCubit, ProvidersState>(
builder: (context, state) {
// Passiamo allProviders per garantire la visione dello storico
return _EntertainmentList(
services: _tempList,
allProviders: state.allProviders,
onDelete: (index) =>
setState(() => _tempList.removeAt(index)),
onAddTap: () => setState(() => _isAddingNew = true),
);
},
),
),
),
actions: !_isAddingNew
? [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Annulla"),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, _tempList),
child: const Text("Conferma Tutti"),
),
]
: null, // I pulsanti del form sono interni al form stesso
);
}
}
class _EntertainmentList extends StatelessWidget {
final List<EntertainmentServiceModel> services;
final List<ProviderModel> allProviders;
final Function(int) onDelete;
final VoidCallback onAddTap;
const _EntertainmentList({
required this.services,
required this.allProviders,
required this.onDelete,
required this.onAddTap,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (services.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32.0),
child: Text(
"Nessun servizio intrattenimento.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
)
else
Flexible(
child: ListView.separated(
shrinkWrap: true,
itemCount: services.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final s = services[index];
final providerName = allProviders
.firstWhere(
(p) => p.id == s.providerId,
orElse: () => ProviderModel(
id: '',
nome: 'Fornitore Storico',
companyId: '',
isActive: false,
energia: false,
telefoniaFissa: false,
telefoniaMobile: false,
assicurazioni: false,
altro: false,
intrattenimento: false,
),
)
.nome;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
backgroundColor: Colors.purple.shade100,
child: const Icon(
Icons.movie_creation_outlined,
color: Colors.purple,
),
),
title: Text(
"${s.type}$providerName",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
s.constrained
? "Vincolo fino al: ${s.constrainExpiration.day}/${s.constrainExpiration.month}/${s.constrainExpiration.year}"
: "Senza vincoli",
style: TextStyle(
color: s.constrained
? Colors.red.shade700
: Colors.green.shade700,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () => onDelete(index),
),
);
},
),
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: onAddTap,
icon: const Icon(Icons.add),
label: const Text("Aggiungi Servizio"),
),
],
);
}
}
// ---ENTERTAINMENT FORM (MODALE)---
class _EntertainmentForm extends StatefulWidget {
final Function(EntertainmentServiceModel) onSave;
final VoidCallback onCancel;
const _EntertainmentForm({required this.onSave, required this.onCancel});
@override
State<_EntertainmentForm> createState() => _EntertainmentFormState();
}
class _EntertainmentFormState extends State<_EntertainmentForm> {
String? _selectedProviderId;
final TextEditingController _typeController = TextEditingController();
bool _isConstrained = false;
DateTime _expirationDate = DateTime.now().add(
const Duration(days: 365),
); // Default 12 mesi
// Preset rapidi per il vincolo (es: 12, 24 mesi)
int? _selectedPresetMonths;
void _applyPreset(int months) {
setState(() {
_selectedPresetMonths = months;
_isConstrained = true;
final now = DateTime.now();
_expirationDate = DateTime(now.year, now.month + months, now.day);
});
}
Future<void> _pickDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _expirationDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365 * 10)),
);
if (picked != null) {
setState(() {
_expirationDate = picked;
_selectedPresetMonths = null;
_isConstrained = true;
});
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1. GESTORE (Filtro intrattenimento)
BlocBuilder<ProvidersCubit, ProvidersState>(
builder: (context, state) {
final filtered = state.activeProviders
.where((p) => p.intrattenimento)
.toList();
return DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: "Fornitore (es: Sky, TIM)",
border: OutlineInputBorder(),
),
items: filtered
.map(
(p) => DropdownMenuItem(value: p.id, child: Text(p.nome)),
)
.toList(),
onChanged: (val) => setState(() => _selectedProviderId = val),
);
},
),
const SizedBox(height: 16),
// 2. TIPO SERVIZIO (TextField con suggerimenti rapidi sotto)
TextFormField(
controller: _typeController,
decoration: const InputDecoration(
labelText: "Servizio",
hintText: "es: Netflix, DAZN, Disney+",
border: OutlineInputBorder(),
),
onChanged: (val) => setState(() {}),
),
const SizedBox(height: 8),
// Suggerimenti rapidi (Chip)
FutureBuilder<List<String>>(
future: GetIt.I<ServicesRepository>().fetchTopEntertainmentTypes(
GetIt.I<SessionBloc>().state.company!.id,
),
builder: (context, snapshot) {
final suggestions = snapshot.data ?? ["Netflix", "DAZN", "Sky"];
return Wrap(
spacing: 8,
children: suggestions.map((s) {
return ActionChip(
label: Text(s, style: const TextStyle(fontSize: 12)),
onPressed: () => setState(() => _typeController.text = s),
);
}).toList(),
);
},
),
const SizedBox(height: 16),
// 3. VINCOLO CONTRATTUALE
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Vincolo di permanenza",
style: TextStyle(fontWeight: FontWeight.bold),
),
Switch(
value: _isConstrained,
onChanged: (val) => setState(() {
_isConstrained = val;
if (!val) _selectedPresetMonths = null;
}),
),
],
),
if (_isConstrained) ...[
const SizedBox(height: 8),
SegmentedButton<int?>(
segments: const [
ButtonSegment(value: 12, label: Text("12m")),
ButtonSegment(value: 24, label: Text("24m")),
ButtonSegment(
value: null,
label: Icon(Icons.calendar_month, size: 20),
),
],
selected: {_selectedPresetMonths},
onSelectionChanged: (val) {
if (val.first == null) {
_pickDate();
} else {
_applyPreset(val.first!);
}
},
),
const SizedBox(height: 12),
// Box data scadenza vincolo
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.event_busy, size: 18, color: Colors.redAccent),
const SizedBox(width: 8),
Text(
"Scadenza vincolo: ${_expirationDate.day.toString().padLeft(2, '0')}/${_expirationDate.month.toString().padLeft(2, '0')}/${_expirationDate.year}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
],
const SizedBox(height: 24),
// PULSANTI
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: widget.onCancel,
child: const Text("Annulla"),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed:
(_selectedProviderId == null || _typeController.text.isEmpty)
? null
: () => widget.onSave(
EntertainmentServiceModel(
providerId: _selectedProviderId!,
type: _typeController.text,
constrained: _isConstrained,
constrainExpiration: _expirationDate,
),
),
child: const Text("Aggiungi"),
),
],
),
],
);
}
}

View File

@@ -10,45 +10,67 @@ class ServiceFormScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return 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 (Da fare)
GeneralInfoSection(service: service),
const SizedBox(height: 24),
// SEZIONE 3: I MODULI (Da fare)
ServicesGrid(service: service),
const SizedBox(height: 32),
// SEZIONE 4: ALLEGATI (Da fare)
// const _AttachmentsSection(),
],
return BlocListener<ServicesCubit, ServicesState>(
listener: (context, state) {
if (state.status == ServicesStatus.saved) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Pratica salvata con successo!"),
backgroundColor: Colors.green,
),
);
},
Navigator.pop(context); // Torna alla lista di pratiche
} else if (state.status == ServicesStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Si è verificato un errore ${state.errorMessage ?? ''}",
),
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(),
],
),
);
},
),
),
);
}
@@ -61,7 +83,7 @@ class _SaveButton extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<ServicesCubit, ServicesState>(
builder: (context, state) {
if (state.isSaving) {
if (state.status == ServicesStatus.saving) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: SizedBox(
@@ -78,7 +100,6 @@ class _SaveButton extends StatelessWidget {
icon: const Icon(Icons.save),
tooltip: "Salva Pratica",
onPressed: () {
// TODO: Aggiungere una validazione prima di salvare!
context.read<ServicesCubit>().saveCurrentService();
},
);

View File

@@ -3,10 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/services/blocs/services_cubit.dart';
import 'package:flux/features/services/models/energy_service_model.dart';
import 'package:flux/features/services/models/entertainment_service_model.dart';
import 'package:flux/features/services/models/fin_service_model.dart';
import 'package:flux/features/services/models/service_model.dart';
import 'package:flux/features/services/ui/service_form_screen/action_card.dart';
import 'package:flux/features/services/ui/service_form_screen/energy_service_dialog.dart';
import 'package:flux/features/services/ui/service_form_screen/entertainment_service_card.dart';
import 'package:flux/features/services/ui/service_form_screen/finance_service_dialog.dart';
import 'package:flux/features/services/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello
@@ -162,12 +164,25 @@ class ServicesGrid extends StatelessWidget {
},
),
ActionCard(
label: "Contenuti",
label: "Intratten.",
count: service.entertainmentServices.length,
icon: Icons.tv,
color: Colors.redAccent,
onTap: () {
// TODO: Aprire la Dialog Contenuti complessa
icon: Icons.movie_filter_outlined,
color: Colors.purple,
onTap: () async {
final result =
await showDialog<List<EntertainmentServiceModel>>(
context: context,
builder: (context) => EntertainmentServiceDialog(
initialServices: service.entertainmentServices,
currentStoreId: service.storeId,
),
);
if (result != null && context.mounted) {
context
.read<ServicesCubit>()
.updateEntertainmentServices(result);
}
},
),
],