feat-add-files-from-qr (#8)
Reviewed-on: http://catelliub.zapto.org:3000/brontomark/flux/pulls/8 Co-authored-by: Mark M2 Macbook <marco@catelli.it> Co-committed-by: Mark M2 Macbook <marco@catelli.it>
This commit is contained in:
@@ -130,4 +130,8 @@ class SessionCubit extends Cubit<SessionState> {
|
||||
await _supabase.auth.signOut();
|
||||
// Non serve emettere stato qui, ci pensa il listener nel costruttore!
|
||||
}
|
||||
|
||||
void setIsMobileDevice(bool isMobile) {
|
||||
emit(state.copyWith(isMobileDevice: isMobile));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ class SessionState extends Equatable {
|
||||
final StoreModel? currentStore;
|
||||
final StaffMemberModel? currentStaff;
|
||||
final OnboardingStep onboardingStep;
|
||||
final bool isMobileDevice;
|
||||
|
||||
const SessionState({
|
||||
this.status = SessionStatus.initial,
|
||||
@@ -32,6 +33,7 @@ class SessionState extends Equatable {
|
||||
this.currentStore,
|
||||
this.currentStaff,
|
||||
this.onboardingStep = OnboardingStep.none,
|
||||
this.isMobileDevice = false,
|
||||
});
|
||||
|
||||
/// Metodo per creare una copia dello stato modificando solo i campi necessari
|
||||
@@ -42,6 +44,7 @@ class SessionState extends Equatable {
|
||||
StoreModel? currentStore,
|
||||
StaffMemberModel? currentStaff,
|
||||
OnboardingStep? onboardingStep,
|
||||
bool? isMobileDevice,
|
||||
}) {
|
||||
return SessionState(
|
||||
status: status ?? this.status,
|
||||
@@ -50,6 +53,7 @@ class SessionState extends Equatable {
|
||||
currentStore: currentStore ?? this.currentStore,
|
||||
currentStaff: currentStaff ?? this.currentStaff,
|
||||
onboardingStep: onboardingStep ?? this.onboardingStep,
|
||||
isMobileDevice: isMobileDevice ?? this.isMobileDevice,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +65,7 @@ class SessionState extends Equatable {
|
||||
currentStore,
|
||||
currentStaff,
|
||||
onboardingStep,
|
||||
isMobileDevice,
|
||||
];
|
||||
|
||||
// Helper rapidi per la UI
|
||||
|
||||
@@ -5,15 +5,19 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
// Importa il tuo SessionCubit e lo State
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/data/core_repository.dart';
|
||||
import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart';
|
||||
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
|
||||
import 'package:flux/features/customers/models/customer_model.dart';
|
||||
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
||||
import 'package:flux/features/home/ui/home_screen.dart';
|
||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
||||
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
||||
import 'package:flux/features/services/blocs/service_files_bloc.dart';
|
||||
import 'package:flux/features/services/models/service_model.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/service_mobile_upload_screen.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -90,7 +94,27 @@ class AppRouter {
|
||||
builder: (context, state) {
|
||||
// Recuperiamo l'oggetto customer passato tramite extra
|
||||
final customer = state.extra as CustomerModel;
|
||||
return CustomerDetailScreen(customer: customer);
|
||||
return BlocProvider(
|
||||
create: (context) => CustomerFilesBloc(customer.id!),
|
||||
child: CustomerDetailScreen(customer: customer),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/customer/:id/upload',
|
||||
builder: (context, state) {
|
||||
final customerId = state.pathParameters['id']!;
|
||||
// Recuperiamo il nome dalle query se vogliamo mostrarlo nel titolo,
|
||||
// oppure lo caricherà il bloc.
|
||||
final customerName = state.uri.queryParameters['name'] ?? 'Cliente';
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => CustomerFilesBloc(customerId),
|
||||
child: CustomerMobileUploadScreen(
|
||||
customerId: customerId,
|
||||
customerName: customerName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -107,9 +131,29 @@ class AppRouter {
|
||||
// Recuperiamo l'ID se presente nell'URL
|
||||
final serviceId = state.uri.queryParameters['serviceId'];
|
||||
|
||||
return ServiceFormScreen(
|
||||
serviceId: serviceId ?? existingService?.id,
|
||||
existingService: existingService,
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
ServiceFilesBloc(serviceId: serviceId ?? existingService?.id),
|
||||
child: ServiceFormScreen(
|
||||
serviceId: serviceId ?? existingService?.id,
|
||||
existingService: existingService,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/service/:id/upload',
|
||||
builder: (context, state) {
|
||||
final serviceId = state.pathParameters['id']!;
|
||||
final serviceName = state.uri.queryParameters['name'] ?? 'Pratica';
|
||||
|
||||
return BlocProvider(
|
||||
// Inizializziamo il bloc col serviceId corretto!
|
||||
create: (context) => ServiceFilesBloc(serviceId: serviceId),
|
||||
child: ServiceMobileUploadScreen(
|
||||
serviceId: serviceId,
|
||||
serviceName: serviceName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
9
lib/core/utils/functions.dart
Normal file
9
lib/core/utils/functions.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
// Funzione che chiede le chiavi a Supabase
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
Future<String> getSignedUrl(String storagePath) async {
|
||||
return await GetIt.I<SupabaseClient>().storage
|
||||
.from('documents')
|
||||
.createSignedUrl(storagePath, 60); // Link che si autodistrugge in 60s
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart'; // <--- AGGIUNGI QUESTO
|
||||
import 'package:flux/core/utils/functions.dart';
|
||||
|
||||
class ImageViewerWidget extends StatelessWidget {
|
||||
final String? storagePath; // ATTENZIONE: Ora contiene lo storagePath!
|
||||
@@ -12,13 +12,6 @@ class ImageViewerWidget extends StatelessWidget {
|
||||
'Errore: Devi fornire un Path valido o i bytes del file!',
|
||||
);
|
||||
|
||||
// Funzione che chiede le chiavi a Supabase
|
||||
Future<String> _getSignedUrl() async {
|
||||
return await Supabase.instance.client.storage
|
||||
.from('documents')
|
||||
.createSignedUrl(storagePath!, 60); // Link che si autodistrugge in 60s
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -37,7 +30,7 @@ class ImageViewerWidget extends StatelessWidget {
|
||||
child: bytes != null
|
||||
? Image.memory(bytes!)
|
||||
: FutureBuilder<String>(
|
||||
future: _getSignedUrl(),
|
||||
future: getSignedUrl(storagePath!),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:flux/core/utils/functions.dart';
|
||||
import 'package:pdfx/pdfx.dart';
|
||||
import 'package:internet_file/internet_file.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class PdfViewerWidget extends StatefulWidget {
|
||||
final String? storagePath;
|
||||
@@ -39,11 +38,7 @@ class _PdfViewerWidgetState extends State<PdfViewerWidget> {
|
||||
pdfData = widget.bytes!;
|
||||
} else if (widget.storagePath != null && widget.storagePath!.isNotEmpty) {
|
||||
// SCENARIO 2: Pratica salvata, scarichiamo da Supabase (Remoto)
|
||||
final signedUrl = await GetIt.I
|
||||
.get<SupabaseClient>()
|
||||
.storage
|
||||
.from('documents')
|
||||
.createSignedUrl(widget.storagePath!, 60);
|
||||
final signedUrl = await getSignedUrl(widget.storagePath!);
|
||||
pdfData = await InternetFile.get(signedUrl);
|
||||
} else {
|
||||
throw Exception("Nessun documento trovato");
|
||||
|
||||
93
lib/core/widgets/qr_upload_dialog.dart
Normal file
93
lib/core/widgets/qr_upload_dialog.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
class QrUploadDialog extends StatelessWidget {
|
||||
final String deepLinkUrl;
|
||||
final String title;
|
||||
|
||||
const QrUploadDialog({
|
||||
super.key,
|
||||
required this.deepLinkUrl,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Usiamo i colori del tema per renderlo coerente col tuo design
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
title: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.qr_code_scanner,
|
||||
size: 48,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
height: 400,
|
||||
width: 350,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // Fondamentale per i dialog
|
||||
children: [
|
||||
const Text(
|
||||
"Inquadra questo codice con la fotocamera del tuo telefono per scattare e caricare i documenti direttamente qui.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// IL CUORE DELLA MAGIA
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors
|
||||
.white, // Lo sfondo bianco salva la vita sui temi scuri
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: QrImageView(
|
||||
data: deepLinkUrl,
|
||||
version: QrVersions.auto,
|
||||
size: 200.0,
|
||||
//Opzionale: puoi metterci il logo di FLUX in mezzo!
|
||||
embeddedImage: const AssetImage('assets/images/logo.png'),
|
||||
embeddedImageStyle: const QrEmbeddedImageStyle(
|
||||
size: Size(40, 40),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"In attesa di file...",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const LinearProgressIndicator(), // Per far capire che è "in ascolto"
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text("CHIUDI"),
|
||||
),
|
||||
],
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user