refined document sequence management

This commit is contained in:
2026-05-16 09:04:18 +02:00
parent b5ccb0428d
commit a166992b04
6 changed files with 170 additions and 78 deletions

View File

@@ -49,19 +49,4 @@ class TicketsShipmentRepository {
throw ('Errore durante la creazione della spedizione: $e'); throw ('Errore durante la creazione della spedizione: $e');
} }
} }
Future<String> getNextAutoDocumentNumber() async {
try {
final response = await _supabase
.from('document_sequences')
.select('*')
.eq('company_id', _companyId)
.eq('document_type', DocumentType.shipment.name)
.single();
return DocumentSequence.fromMap(response);
} catch (e) {
throw ('Errore recupero numero documento: $e');
}
}
} }

View File

@@ -1,68 +1,93 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart';
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart'; import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:get_it/get_it.dart';
class DocumentSequenceState { enum DocumentSequenceStatus { initial, loading, success, failure }
class DocumentSequenceState extends Equatable {
final DocumentSequenceStatus status;
final List<DocumentSequence> sequences; final List<DocumentSequence> sequences;
final bool isLoading;
final String? error; final String? error;
DocumentSequenceState({ const DocumentSequenceState({
this.sequences = const [], this.sequences = const [],
this.isLoading = false,
this.error, this.error,
this.status = DocumentSequenceStatus.initial,
}); });
DocumentSequenceState copyWith({
List<DocumentSequence>? sequences,
String? error,
DocumentSequenceStatus? status,
}) {
return DocumentSequenceState(
sequences: sequences ?? this.sequences,
error: error ?? this.error,
status: status ?? this.status,
);
}
@override
List<Object?> get props => [sequences, error, status];
} }
class DocumentSequenceCubit extends Cubit<DocumentSequenceState> { class DocumentSequenceCubit extends Cubit<DocumentSequenceState> {
final String companyId; final String companyId;
final _supabase = Supabase.instance.client; final _repository = GetIt.I.get<DocumentSequenceRepository>();
DocumentSequenceCubit(this.companyId) : super(DocumentSequenceState()); DocumentSequenceCubit(this.companyId) : super(DocumentSequenceState());
Future<void> loadSequences() async { Future<void> loadSequences() async {
emit(DocumentSequenceState(isLoading: true)); emit(state.copyWith(status: DocumentSequenceStatus.loading));
try { try {
final data = await _supabase final list = await _repository.getDocumentSequences();
.from('document_sequences') emit(
.select() state.copyWith(sequences: list, status: DocumentSequenceStatus.success),
.eq('company_id', companyId); );
final list = (data as List)
.map((e) => DocumentSequence.fromMap(e))
.toList();
emit(DocumentSequenceState(sequences: list));
} catch (e) { } catch (e) {
emit(DocumentSequenceState(error: e.toString())); emit(
state.copyWith(
error: e.toString(),
status: DocumentSequenceStatus.failure,
),
);
} }
} }
void updateLocalSequence(String docType, {String? prefix, int? nextValue}) { void updateLocalSequence(String docType, {String? prefix, int? nextValue}) {
bool found = false;
final newList = state.sequences.map((s) { final newList = state.sequences.map((s) {
if (s.docType == docType) { if (s.docType == docType) {
found = true;
return s.copyWith(prefix: prefix, nextValue: nextValue); return s.copyWith(prefix: prefix, nextValue: nextValue);
} }
return s; return s;
}).toList(); }).toList();
emit(DocumentSequenceState(sequences: newList)); if (!found) {
newList.add(
DocumentSequence(
docType: docType,
prefix: prefix ?? '',
nextValue: nextValue ?? 1,
),
);
}
emit(state.copyWith(sequences: newList));
} }
Future<void> saveSequences() async { Future<void> saveSequences() async {
try { try {
for (var seq in state.sequences) { for (var seq in state.sequences) {
await _supabase.from('document_sequences').upsert({ await _repository.createOrUpdateSequence(sequence: seq);
'company_id': companyId,
'doc_type': seq.docType,
'next_value': seq.nextValue,
'prefix': seq.prefix,
});
} }
// Opzionale: mostra un feedback di successo // Opzionale: mostra un feedback di successo
} catch (e) { } catch (e) {
emit( emit(
DocumentSequenceState( state.copyWith(
sequences: state.sequences,
error: "Errore nel salvataggio", error: "Errore nel salvataggio",
status: DocumentSequenceStatus.failure,
), ),
); );
} }

View File

@@ -1,30 +1,61 @@
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart'; import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
class DocumentSequenceRepository { class DocumentSequenceRepository {
final _supabase = GetIt.I.get<SupabaseClient>(); final _supabase = GetIt.I.get<SupabaseClient>();
final _companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
Future<List<DocumentSequence>> getDocumentSequences(String companyId) async { Future<List<DocumentSequence>> getDocumentSequences() async {
try {
final response = await _supabase final response = await _supabase
.from('document_sequences') .from('document_sequences')
.select() .select()
.eq('company_id', companyId); .eq('company_id', _companyId);
return (response as List).map((e) => DocumentSequence.fromMap(e)).toList(); return (response as List)
.map((e) => DocumentSequence.fromMap(e))
.toList();
} catch (e) {
throw ('Errore durante il caricamento delle sequenze: $e');
}
} }
Future<void> updateSequence({ Future<String> getNextDocumentNumber(String docType) async {
required String companyId, try {
required String docType, // Chiamiamo la funzione SQL che abbiamo appena creato!
required int nextValue, final response = await _supabase.rpc(
required String prefix, 'get_next_sequence',
params: {'p_doc_type': docType},
);
return response as String;
} catch (e) {
throw ('Errore durante la generazione del numero: $e');
}
}
Future<void> createOrUpdateSequence({
required DocumentSequence sequence,
}) async { }) async {
try {
await _supabase.from('document_sequences').upsert({ await _supabase.from('document_sequences').upsert({
'company_id': companyId, 'company_id': _companyId,
'doc_type': docType, 'doc_type': sequence.docType,
'next_value': nextValue, 'next_value': sequence.nextValue,
'prefix': prefix, 'prefix': sequence.prefix,
});
} on Exception catch (e) {
throw ('Errore durante la creazione/aggiornamento della sequenza: $e');
}
}
Future<void> updateSequence({required DocumentSequence sequence}) async {
await _supabase.from('document_sequences').upsert({
'company_id': _companyId,
'doc_type': sequence.docType,
'next_value': sequence.nextValue,
'prefix': sequence.prefix,
}); });
} }
} }

View File

@@ -1,4 +1,6 @@
enum DocumentType { ticket, shipment, invoice } // Se un domani ci saranno nuovi tipi di documento, basterà aggiungerli qui
// e alla mappa nella DocumentSequenceSection dei documenti supportati
enum DocumentType { ticket, shipment }
class DocumentSequence { class DocumentSequence {
final String docType; final String docType;

View File

@@ -1,17 +1,34 @@
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/settings/document_sequence/blocs/document_sequence_cubit.dart'; import 'package:flux/features/settings/document_sequence/blocs/document_sequence_cubit.dart';
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
class DocumentSequenceSection extends StatelessWidget { class DocumentSequenceSection extends StatelessWidget {
const DocumentSequenceSection({super.key}); const DocumentSequenceSection({super.key});
// La nostra lista "Ninja" di tutti i documenti gestiti dall'app
// Se domani aggiungo le fatture, basta aggiungere una riga qui!
// e all'enum DocumentType nel model
static final supportedDocumentTypes = [
{
'type': DocumentType.ticket.name,
'label': 'TICKET',
'defaultPrefix': 'TCK',
},
{
'type': DocumentType.shipment.name,
'label': 'DDT (Documento di Trasporto)',
'defaultPrefix': 'DDT',
},
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final year = DateTime.now().year; final year = DateTime.now().year;
return BlocBuilder<DocumentSequenceCubit, DocumentSequenceState>( return BlocBuilder<DocumentSequenceCubit, DocumentSequenceState>(
builder: (context, state) { builder: (context, state) {
if (state.isLoading) { if (state.status == DocumentSequenceStatus.loading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
@@ -27,10 +44,26 @@ class DocumentSequenceSection extends StatelessWidget {
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
), ),
), ),
...state.sequences.map((seq) {
// Anteprima dinamica // Invece di mappare state.sequences, mappiamo i documenti supportati
...supportedDocumentTypes.map((docDef) {
final docType = docDef['type']!;
// Cerchiamo se c'è già una configurazione nello stato per questo documento
final existingList = state.sequences
.where((s) => s.docType == docType)
.toList();
final existingSeq = existingList.isNotEmpty
? existingList.first
: null;
// Se esiste usiamo i suoi valori, altrimenti i default
final prefix = existingSeq?.prefix ?? docDef['defaultPrefix']!;
final nextValue = existingSeq?.nextValue ?? 1;
// Anteprima dinamica (aggiornata a 4 zeri come nel DB!)
final preview = final preview =
"${seq.prefix.isNotEmpty ? '${seq.prefix}-' : ''}$year-${seq.nextValue.toString().padLeft(6, '0')}"; "${prefix.isNotEmpty ? '$prefix-' : ''}$year-${nextValue.toString().padLeft(4, '0')}";
return Card( return Card(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
@@ -40,7 +73,7 @@ class DocumentSequenceSection extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
seq.docType.toUpperCase(), docDef['label']!,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue, color: Colors.blue,
@@ -52,24 +85,21 @@ class DocumentSequenceSection extends StatelessWidget {
Expanded( Expanded(
flex: 2, flex: 2,
child: TextFormField( child: TextFormField(
initialValue: seq.prefix, initialValue: prefix,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Prefisso', labelText: 'Prefisso',
hintText: 'es. TCK', hintText: 'es. TCK',
), ),
onChanged: (val) => context onChanged: (val) => context
.read<DocumentSequenceCubit>() .read<DocumentSequenceCubit>()
.updateLocalSequence( .updateLocalSequence(docType, prefix: val),
seq.docType,
prefix: val,
),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
flex: 3, flex: 3,
child: TextFormField( child: TextFormField(
initialValue: seq.nextValue.toString(), initialValue: nextValue.toString(),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Prossimo Numero', labelText: 'Prossimo Numero',
@@ -77,7 +107,7 @@ class DocumentSequenceSection extends StatelessWidget {
onChanged: (val) => context onChanged: (val) => context
.read<DocumentSequenceCubit>() .read<DocumentSequenceCubit>()
.updateLocalSequence( .updateLocalSequence(
seq.docType, docType,
nextValue: int.tryParse(val) ?? 1, nextValue: int.tryParse(val) ?? 1,
), ),
), ),
@@ -88,7 +118,9 @@ class DocumentSequenceSection extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade100, color: Colors
.grey
.shade100, // Se hai un tema scuro potresti voler usare Theme.of(context).colorScheme.surfaceContainer
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Row( child: Row(
@@ -102,7 +134,9 @@ class DocumentSequenceSection extends StatelessWidget {
Text( Text(
"Anteprima prossimo: ", "Anteprima prossimo: ",
style: TextStyle( style: TextStyle(
color: Colors.grey.shade700, color: Colors
.grey
.shade700, // Idem per la dark mode
fontSize: 12, fontSize: 12,
), ),
), ),

View File

@@ -5,6 +5,7 @@ import 'package:flux/features/documents/data/tickets_shipment_repository.dart';
import 'package:flux/features/documents/models/shipment_document_model.dart'; import 'package:flux/features/documents/models/shipment_document_model.dart';
import 'package:flux/features/master_data/providers/models/provider_location_model.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_model.dart';
import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
part 'ticket_shipping_state.dart'; part 'ticket_shipping_state.dart';
@@ -12,6 +13,8 @@ part 'ticket_shipping_state.dart';
class TicketShippingCubit extends Cubit<TicketShippingState> { class TicketShippingCubit extends Cubit<TicketShippingState> {
final TicketsShipmentRepository _repository = final TicketsShipmentRepository _repository =
GetIt.I<TicketsShipmentRepository>(); GetIt.I<TicketsShipmentRepository>();
final DocumentSequenceRepository _sequenceRepository =
GetIt.I<DocumentSequenceRepository>();
TicketShippingCubit({required List<String> ticketIds}) TicketShippingCubit({required List<String> ticketIds})
: super( : super(
TicketShippingState( TicketShippingState(
@@ -79,16 +82,28 @@ class TicketShippingCubit extends Cubit<TicketShippingState> {
); );
} }
void toggleAutoNumber(bool value) { Future<void> toggleAutoNumber(bool value) async {
// Aggiorniamo subito l'UI per mostrare che lo switch si è acceso
emit(state.copyWith(isAutoNumber: value)); emit(state.copyWith(isAutoNumber: value));
if (value) { if (value) {
final nextNumber = "DDT-${DateTime.now().year}-001"; // Se lo switch è acceso, chiediamo il numero al DB
try {
final nextNumber = await _sequenceRepository.getNextDocumentNumber(
'ddt',
);
emit( emit(
state.copyWith( state.copyWith(
document: state.document.copyWith(docNumber: nextNumber), document: state.document.copyWith(docNumber: nextNumber),
), ),
); );
} catch (e) {
// Se qualcosa va storto, spegniamo lo switch e mostriamo l'errore
emit(state.copyWith(isAutoNumber: false, errorMessage: e.toString()));
}
} else { } else {
// Se lo spegne, svuotiamo semplicemente il campo
emit(state.copyWith(document: state.document.copyWith(docNumber: ''))); emit(state.copyWith(document: state.document.copyWith(docNumber: '')));
} }
} }