3 Commits

Author SHA1 Message Date
f8504d466a fix attachments
All checks were successful
Build and Release FLUX (Multi-Platform) / build-android (push) Successful in 1m44s
Build and Release FLUX (Multi-Platform) / build-web (push) Successful in 1m5s
Build and Release FLUX (Multi-Platform) / build-windows (push) Successful in 8m7s
2026-06-08 14:36:31 +02:00
22bb86f052 Merge branch 'main' of ssh.catelli.it:brontomark/flux 2026-06-08 13:20:52 +02:00
fc850795c9 fix attachments with no parentId 2026-06-08 13:17:33 +02:00
4 changed files with 54 additions and 76 deletions

View File

@@ -34,14 +34,14 @@ class SharedAttachmentsSection extends StatefulWidget {
final String? parentId;
final String titleForUpload;
final AttachmentParentType parentType;
final Future<String?> Function()? onGenerateIdForQr;
final Future<String?> Function()? onEnsureEntitySaved;
const SharedAttachmentsSection({
super.key,
this.parentId,
this.titleForUpload = 'Cliente_sconosciuto',
required this.parentType,
this.onGenerateIdForQr,
this.onEnsureEntitySaved,
});
@override
@@ -90,6 +90,32 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
// --- SELEZIONE FILE DAL PC/TELEFONO ---
Future<void> _pickFiles() async {
final attachmentsBloc = context.read<AttachmentsBloc>();
String? targetId = attachmentsBloc.state.parentId;
// 🥷 SE L'ID NON C'È (Nuova Operazione), FORZIAMO IL SALVATAGGIO PREVENTIVO!
if (targetId == null || targetId.isEmpty) {
if (widget.onEnsureEntitySaved != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Salvataggio rapido scheda per allegati... ⏳'),
duration: Duration(seconds: 1),
),
);
// Chiamiamo la funzione passata dal TicketForm/OperationForm
targetId = await widget.onEnsureEntitySaved!();
}
// Se il salvataggio fallisce (es. form non valido), ci fermiamo per evitare file orfani
if (targetId == null || targetId.isEmpty) return;
// Comunichiamo immediatamente al BLoC che l'entità padre è stata salvata e ha un nuovo ID.
// Questo eviterà che i file finiscano nei `localFiles` temporanei.
attachmentsBloc.add(ParentEntitySavedEvent(targetId));
}
// Ora che abbiamo la certezza matematica di avere un targetId, apriamo il picker
final result = await FilePicker.pickFiles(
allowMultiple: true,
type: FileType.custom,
@@ -98,8 +124,8 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
);
if (result != null && mounted) {
// MAGIA: Passiamo direttamente la lista di PlatformFile al tuo BLoC!
context.read<AttachmentsBloc>().add(AddAttachmentsEvent(result.files));
// Ora il BLoC eseguirà l'ambiente di "Upload immediato" (Bivio 2) perché ha l'ID aggiornato!
attachmentsBloc.add(AddAttachmentsEvent(result.files));
}
}
@@ -481,7 +507,7 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
// SE L'ID NON C'È, CHIAMIAMO IL SALVATAGGIO IN BACKGROUND!
if (targetId == null) {
if (widget.onGenerateIdForQr != null) {
if (widget.onEnsureEntitySaved != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
@@ -492,7 +518,7 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
);
// Aspettiamo che il TicketFormCubit faccia il suo lavoro
targetId = await widget.onGenerateIdForQr!();
targetId = await widget.onEnsureEntitySaved!();
}
// Se fallisce (es. validazione form non passata), ci fermiamo

View File

@@ -111,69 +111,6 @@ class OperationsRepository {
}
}
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
/* Future<List<OperationModel>> fetchOperations({
required String companyId,
String? storeId,
String? staffId,
String? providerId,
required int offset,
int limit = 50,
String? searchTerm,
DateTimeRange? dateRange,
}) async {
try {
var query = _supabase
.from(Tables.operations)
.select('''
*,
${Tables.customers}(*),
${Tables.stores}(name),
${Tables.providers}(name),
${Tables.models}(name_with_brand),
${Tables.staffMembers}(name),
${Tables.attachments}(*)
''')
.eq('company_id', companyId);
// Filtro Range Date
if (dateRange != null) {
query = query
.gte('created_at', dateRange.start.toIso8601String())
.lte('created_at', dateRange.end.toIso8601String());
}
if (storeId != null) {
query = query.or('store_id.eq.$storeId,store_id.is.null');
}
if (staffId != null) {
query = query.or('staff_id.eq.$staffId,staff_id.is.null');
}
if (providerId != null) {
query = query.or('provider_id.eq.$providerId,provider_id.is.null');
}
if (searchTerm != null && searchTerm.isNotEmpty) {
// Filtra sui campi della tabella principale O su quelli della tabella joinata
query = query.or(
'reference.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%',
);
}
final response = await query
.order('created_at', ascending: false)
.range(offset, offset + limit - 1);
return (response as List)
.map((map) => OperationModel.fromMap(map))
.toList();
} catch (e) {
throw Exception('$e');
}
} */
Stream<List<Map<String, dynamic>>> watchStoreOperations({
required String storeId,
required int limit,

View File

@@ -26,6 +26,9 @@ class OperationFormScreen extends StatefulWidget {
class _OperationFormScreenState extends State<OperationFormScreen> {
final _formKey = GlobalKey<FormState>();
// 🥷 IL NUOVO FLAG: Ci ricorderà se vogliamo davvero uscire!
bool _isClosingIntent = false;
final _referenceController = TextEditingController();
final _noteController = TextEditingController();
final _freeTextSubtypeController = TextEditingController();
@@ -101,10 +104,11 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
required bool keepAdding,
}) {
if (_formKey.currentState!.validate()) {
// Se non stiamo facendo "Salva e Aggiungi Altro", il nostro intento è chiudere
_isClosingIntent = !keepAdding;
_flushControllersToCubit();
// Aggiorniamo prima lo stato bersaglio nel cubit
context.read<OperationFormCubit>().updateFields(status: targetStatus);
// Poi chiamiamo il salvataggio
context.read<OperationFormCubit>().saveOperation(
targetStatus: targetStatus,
keepAdding: keepAdding,
@@ -112,11 +116,15 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
}
}
Future<String?> _generateIdForQr() async {
// RINOMINATA E RESA SICURA
Future<String?> _ensureEntitySaved() async {
if (!_formKey.currentState!.validate()) return null;
// 🥷 Diciamo esplicitamente al sistema che NON vogliamo uscire dalla pagina
_isClosingIntent = false;
_flushControllersToCubit();
// Lo leggiamo pulito pulito dal context, perché c'è!
final attachmentsBloc = context.read<AttachmentsBloc>();
final newId = await context.read<OperationFormCubit>().saveOperationDraft();
@@ -151,8 +159,15 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
if (state.status == OperationFormStatus.ready && !_isInitialized) {
_syncTextControllers(state.operation);
}
// 🥷 ORA POPPA SOLO SE L'INTENTO ERA QUELLO DI USCIRE!
if (state.status == OperationFormStatus.success) {
if (_isClosingIntent) {
Navigator.of(context).pop();
} else {
// È stato un salvataggio background (Pick Files o QR).
// Non facciamo nulla, l'utente resta sulla pagina e i file vengono caricati!
}
} else if (state.status == OperationFormStatus.successAndAddAnother) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@@ -626,7 +641,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
parentType: AttachmentParentType.operation,
parentId: state.operation.id,
titleForUpload: state.operation.customer?.name ?? 'Nuova Pratica',
onGenerateIdForQr: _generateIdForQr,
onEnsureEntitySaved: _ensureEntitySaved, // 🥷 Il nuovo nome!
),
],
);

View File

@@ -1,7 +1,7 @@
name: flux
description: "Gestione attività negozio di telefonia"
publish_to: 'none'
version: 1.1.21+39
version: 1.1.22+40
environment:
sdk: ^3.11.3