fg
This commit is contained in:
@@ -24,11 +24,11 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter android:label="flux_deep_link">
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="fluxapp" />
|
<data android:scheme="flux" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://operations.gradle.org/distributions/gradle-8.14-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ class AppRouter {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
/* GoRoute(
|
||||||
path: '/customer/:id/upload',
|
path: '/customer/:id/upload',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final customerId = state.pathParameters['id']!;
|
final customerId = state.pathParameters['id']!;
|
||||||
@@ -223,7 +223,7 @@ class AppRouter {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
), */
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/operation-form',
|
path: '/operation-form',
|
||||||
name: 'operation-form',
|
name: 'operation-form',
|
||||||
@@ -255,7 +255,7 @@ class AppRouter {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
/* GoRoute(
|
||||||
path: '/operation/:id/upload',
|
path: '/operation/:id/upload',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final operationId = state.pathParameters['id']!;
|
final operationId = state.pathParameters['id']!;
|
||||||
@@ -283,6 +283,29 @@ class AppRouter {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
), */
|
||||||
|
GoRoute(
|
||||||
|
path: '/upload/:type/:id',
|
||||||
|
builder: (context, state) {
|
||||||
|
final typeString = state.pathParameters['type']!;
|
||||||
|
final id = state.pathParameters['id']!;
|
||||||
|
|
||||||
|
// Trasformiamo la stringa dell'URL nel nostro amato Enum!
|
||||||
|
final parentType = AttachmentParentType.values.firstWhere(
|
||||||
|
(e) => e.name == typeString,
|
||||||
|
orElse: () =>
|
||||||
|
AttachmentParentType.ticket, // Fallback di sicurezza
|
||||||
|
);
|
||||||
|
|
||||||
|
// Creiamo il BLoC "al volo" solo per questa schermata
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
AttachmentsBloc(parentId: id, parentType: parentType),
|
||||||
|
child: const SharedMobileUploadScreen(
|
||||||
|
title: 'Caricamento Rapido',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
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/core/widgets/qr_upload_dialog.dart';
|
||||||
import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart';
|
import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart';
|
||||||
// Adatta gli import alle tue cartelle reali!
|
|
||||||
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
|
|
||||||
class SharedFilesSection extends StatelessWidget {
|
class SharedFilesSection extends StatelessWidget {
|
||||||
final String
|
final String titleNameForUpload;
|
||||||
titleNameForUpload; // Es. il nome del cliente o il modello da passare alla pagina di upload
|
// LA NOSTRA CALLBACK MAGICA
|
||||||
|
final Future<String?> Function()? onGenerateIdForQr;
|
||||||
|
|
||||||
const SharedFilesSection({super.key, required this.titleNameForUpload});
|
const SharedFilesSection({
|
||||||
|
super.key,
|
||||||
|
required this.titleNameForUpload,
|
||||||
|
this.onGenerateIdForQr,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -24,22 +29,77 @@ class SharedFilesSection extends StatelessWidget {
|
|||||||
'Allegati e Foto',
|
'Allegati e Foto',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
|
||||||
icon: const Icon(Icons.add_a_photo),
|
BlocBuilder<AttachmentsBloc, AttachmentsState>(
|
||||||
label: const Text('Aggiungi'),
|
builder: (context, state) {
|
||||||
onPressed: () {
|
return Row(
|
||||||
// Navighiamo verso la nostra fiammante pagina di upload agnostica!
|
children: [
|
||||||
// Assicurati che l'AttachmentsBloc sopravviva al cambio pagina usando BlocProvider.value
|
// --- IL TASTO QR CODE (Ora sempre attivo!) ---
|
||||||
final bloc = context.read<AttachmentsBloc>();
|
Tooltip(
|
||||||
Navigator.of(context).push(
|
message: 'Carica foto con lo smartphone',
|
||||||
MaterialPageRoute(
|
child: IconButton(
|
||||||
builder: (_) => BlocProvider.value(
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
value: bloc,
|
color: theme.colorScheme.primary, // Sempre colorato!
|
||||||
child: SharedMobileUploadScreen(
|
onPressed: () async {
|
||||||
title: titleNameForUpload,
|
String? targetId = state.parentId;
|
||||||
|
|
||||||
|
// SE L'ID NON C'È, CHIAMIAMO IL SALVATAGGIO IN BACKGROUND!
|
||||||
|
if (targetId == null) {
|
||||||
|
if (onGenerateIdForQr != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Salvataggio rapido scheda in corso... ⏳',
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aspettiamo che il TicketFormCubit faccia il suo lavoro
|
||||||
|
targetId = await onGenerateIdForQr!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se fallisce (es. validazione form non passata), ci fermiamo
|
||||||
|
if (targetId == null) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GENERAZIONE DEL DEEP LINK AGNOSTICO
|
||||||
|
final deepLink =
|
||||||
|
'flux://app/upload/${state.parentType.name}/$targetId';
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => QrUploadDialog(
|
||||||
|
deepLinkUrl: deepLink,
|
||||||
|
title: 'Carica File: $titleNameForUpload',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
|
// --- IL TASTO AGGIUNGI CLASSICO (da PC) ---
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.add_a_photo),
|
||||||
|
label: const Text('Aggiungi'),
|
||||||
|
onPressed: () {
|
||||||
|
final bloc = context.read<AttachmentsBloc>();
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BlocProvider.value(
|
||||||
|
value: bloc,
|
||||||
|
child: SharedMobileUploadScreen(
|
||||||
|
title: titleNameForUpload,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -47,11 +107,10 @@ class SharedFilesSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// LA VETRINA DEI FILE
|
// --- LA VETRINA DEI FILE (Identica a prima) ---
|
||||||
BlocBuilder<AttachmentsBloc, AttachmentsState>(
|
BlocBuilder<AttachmentsBloc, AttachmentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final files =
|
final files = state.allFiles;
|
||||||
state.allFiles; // Unisce sia i remoti che i locali (bozze)
|
|
||||||
|
|
||||||
if (state.status == AttachmentsStatus.loading) {
|
if (state.status == AttachmentsStatus.loading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@@ -107,21 +166,19 @@ class SharedFilesSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Sfondo File / Anteprima
|
|
||||||
Center(
|
Center(
|
||||||
child: isImage
|
child: isImage
|
||||||
? const Icon(
|
? const Icon(
|
||||||
Icons.image,
|
Icons.image,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
size: 40,
|
size: 40,
|
||||||
) // Qui in futuro metteremo Image.network da Supabase
|
)
|
||||||
: const Icon(
|
: const Icon(
|
||||||
Icons.picture_as_pdf,
|
Icons.picture_as_pdf,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
size: 40,
|
size: 40,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Indicatore "Bozza" per i file non ancora caricati
|
|
||||||
if (file.id == null)
|
if (file.id == null)
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 4,
|
bottom: 4,
|
||||||
@@ -144,7 +201,6 @@ class SharedFilesSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Pulsante Elimina
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: -8,
|
top: -8,
|
||||||
right: -8,
|
right: -8,
|
||||||
@@ -155,7 +211,6 @@ class SharedFilesSection extends StatelessWidget {
|
|||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Manda l'evento di eliminazione
|
|
||||||
context.read<AttachmentsBloc>().add(
|
context.read<AttachmentsBloc>().add(
|
||||||
DeleteSpecificAttachmentEvent(file),
|
DeleteSpecificAttachmentEvent(file),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:flux/core/blocs/session/session_cubit.dart';
|
|||||||
|
|
||||||
class AttachmentsRepository {
|
class AttachmentsRepository {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
static const String _bucketName = 'attachments';
|
static const String _bucketName = 'documents';
|
||||||
static const String _tableName =
|
static const String _tableName =
|
||||||
'attachment'; // Cambia col vero nome della tua tabella se diverso!
|
'attachment'; // Cambia col vero nome della tua tabella se diverso!
|
||||||
|
|
||||||
|
|||||||
@@ -186,4 +186,32 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 5.1 SALVATAGGIO SILENZIOSO (Per generare il QR Code al volo)
|
||||||
|
Future<String?> saveTicketDraft() async {
|
||||||
|
// Non mettiamo lo stato 'saving' per non far sfarfallare tutta la UI,
|
||||||
|
// usiamo un caricamento invisibile.
|
||||||
|
try {
|
||||||
|
final ticketToSave = state.ticket;
|
||||||
|
|
||||||
|
if (ticketToSave.customerId == null || ticketToSave.customerId!.isEmpty) {
|
||||||
|
throw Exception("Seleziona un cliente prima di poter usare il QR.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final savedTicket = await _repository.saveTicket(ticketToSave);
|
||||||
|
|
||||||
|
// Aggiorniamo silenziosamente lo stato con il ticket che ora ha un ID!
|
||||||
|
emit(state.copyWith(ticket: savedTicket, status: TicketFormStatus.ready));
|
||||||
|
|
||||||
|
return savedTicket.id;
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: TicketFormStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flux/core/widgets/shared_forms/customer_section.dart';
|
import 'package:flux/core/widgets/shared_forms/customer_section.dart';
|
||||||
import 'package:flux/core/widgets/shared_forms/model_section.dart';
|
import 'package:flux/core/widgets/shared_forms/model_section.dart';
|
||||||
import 'package:flux/core/widgets/shared_forms/shared_files_section.dart';
|
import 'package:flux/core/widgets/shared_forms/shared_files_section.dart';
|
||||||
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart';
|
import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart';
|
||||||
import 'package:flux/features/tickets/blocs/ticket_form_state.dart';
|
import 'package:flux/features/tickets/blocs/ticket_form_state.dart';
|
||||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
@@ -99,6 +100,25 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> _generateIdForQr() async {
|
||||||
|
// 1. Validiamo i campi obbligatori (es. il cliente)
|
||||||
|
if (!_formKey.currentState!.validate()) return null;
|
||||||
|
|
||||||
|
// 2. Sincronizziamo i testi scritti a mano nel Cubit
|
||||||
|
_flushControllersToCubit();
|
||||||
|
|
||||||
|
// 3. Facciamo il salvataggio silenzioso
|
||||||
|
final newId = await context.read<TicketFormCubit>().saveTicketDraft();
|
||||||
|
|
||||||
|
if (newId != null && context.mounted) {
|
||||||
|
// 4. IL TOCCO DI CLASSE: Diciamo all'AttachmentsBloc che ora la pratica ha un ID!
|
||||||
|
// Questo farà partire l'upload automatico di eventuali file "in bozza"
|
||||||
|
context.read<AttachmentsBloc>().add(ParentEntitySavedEvent(newId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -514,7 +534,12 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
|||||||
// ECCO LA MAGIA:
|
// ECCO LA MAGIA:
|
||||||
SharedFilesSection(
|
SharedFilesSection(
|
||||||
titleNameForUpload: ticket.customerName ?? 'Nuovo Ticket',
|
titleNameForUpload: ticket.customerName ?? 'Nuovo Ticket',
|
||||||
|
onGenerateIdForQr: _generateIdForQr,
|
||||||
),
|
),
|
||||||
|
/* SharedAttachmentsSection(
|
||||||
|
parentType: AttachmentParentType.ticket,
|
||||||
|
parentId: ticket.id,
|
||||||
|
), */
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user