feat-tickets #14

Merged
brontomark merged 13 commits from feat-tickets into main 2026-05-07 16:28:01 +02:00
7 changed files with 164 additions and 33 deletions
Showing only changes of commit 0af51aae10 - Show all commits

View File

@@ -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.

View File

@@ -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

View File

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

View File

@@ -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,12 +29,63 @@ class SharedFilesSection extends StatelessWidget {
'Allegati e Foto', 'Allegati e Foto',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
BlocBuilder<AttachmentsBloc, AttachmentsState>(
builder: (context, state) {
return Row(
children: [
// --- IL TASTO QR CODE (Ora sempre attivo!) ---
Tooltip(
message: 'Carica foto con lo smartphone',
child: IconButton(
icon: const Icon(Icons.qr_code_scanner),
color: theme.colorScheme.primary, // Sempre colorato!
onPressed: () async {
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( TextButton.icon(
icon: const Icon(Icons.add_a_photo), icon: const Icon(Icons.add_a_photo),
label: const Text('Aggiungi'), label: const Text('Aggiungi'),
onPressed: () { onPressed: () {
// Navighiamo verso la nostra fiammante pagina di upload agnostica!
// Assicurati che l'AttachmentsBloc sopravviva al cambio pagina usando BlocProvider.value
final bloc = context.read<AttachmentsBloc>(); final bloc = context.read<AttachmentsBloc>();
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
@@ -44,14 +100,17 @@ 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),
); );

View 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!

View File

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

View File

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