fix supabase storage
This commit is contained in:
@@ -1,22 +1,60 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart'; // <--- AGGIUNGI QUESTO
|
||||||
|
|
||||||
class ImageViewerWidget extends StatelessWidget {
|
class ImageViewerWidget extends StatelessWidget {
|
||||||
final String url;
|
final String? storagePath; // ATTENZIONE: Ora contiene lo storagePath!
|
||||||
|
final Uint8List? bytes;
|
||||||
|
|
||||||
const ImageViewerWidget({super.key, required this.url});
|
const ImageViewerWidget({super.key, this.storagePath, this.bytes})
|
||||||
|
: assert(
|
||||||
|
(storagePath != null && storagePath != '') || bytes != null,
|
||||||
|
'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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close, color: Colors.black),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: InteractiveViewer(
|
body: InteractiveViewer(
|
||||||
// InteractiveViewer dà lo zoom gratis alle immagini!
|
maxScale: 5.0,
|
||||||
child: Center(child: Image.network(url)),
|
child: Center(
|
||||||
|
// Se abbiamo i byte, mostriamo subito. Altrimenti usiamo il FutureBuilder!
|
||||||
|
child: bytes != null
|
||||||
|
? Image.memory(bytes!)
|
||||||
|
: FutureBuilder<String>(
|
||||||
|
future: _getSignedUrl(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return const Text(
|
||||||
|
"Errore caricamento immagine (Permessi negati?)",
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return Image.network(snapshot.data!);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:pdfx/pdfx.dart';
|
import 'package:pdfx/pdfx.dart';
|
||||||
import 'package:internet_file/internet_file.dart'; // flutter pub add internet_file
|
import 'package:internet_file/internet_file.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
class PdfViewerWidget extends StatefulWidget {
|
class PdfViewerWidget extends StatefulWidget {
|
||||||
final String url;
|
final String? storagePath;
|
||||||
|
final Uint8List? bytes;
|
||||||
|
|
||||||
const PdfViewerWidget({super.key, required this.url});
|
const PdfViewerWidget({super.key, this.storagePath, this.bytes})
|
||||||
|
: assert(
|
||||||
|
(storagePath != null && storagePath != '') || bytes != null,
|
||||||
|
'Errore: Devi fornire un URL valido o i bytes del file!',
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PdfViewerWidget> createState() => _PdfViewerWidgetState();
|
State<PdfViewerWidget> createState() => _PdfViewerWidgetState();
|
||||||
@@ -14,6 +22,7 @@ class PdfViewerWidget extends StatefulWidget {
|
|||||||
class _PdfViewerWidgetState extends State<PdfViewerWidget> {
|
class _PdfViewerWidgetState extends State<PdfViewerWidget> {
|
||||||
late PdfControllerPinch _pdfController;
|
late PdfControllerPinch _pdfController;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -22,12 +31,37 @@ class _PdfViewerWidgetState extends State<PdfViewerWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initPdf() async {
|
Future<void> _initPdf() async {
|
||||||
// Scarica il file in memoria in modo fluido
|
try {
|
||||||
final pdfData = await InternetFile.get(widget.url);
|
Uint8List pdfData;
|
||||||
|
|
||||||
|
if (widget.bytes != null) {
|
||||||
|
// SCENARIO 1: Pratica in bozza, file appena scelto (Locale)
|
||||||
|
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);
|
||||||
|
pdfData = await InternetFile.get(signedUrl);
|
||||||
|
} else {
|
||||||
|
throw Exception("Nessun documento trovato");
|
||||||
|
}
|
||||||
|
|
||||||
_pdfController = PdfControllerPinch(
|
_pdfController = PdfControllerPinch(
|
||||||
document: PdfDocument.openData(pdfData),
|
document: PdfDocument.openData(pdfData),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = e.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -39,21 +73,25 @@ class _PdfViewerWidgetState extends State<PdfViewerWidget> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_errorMessage != null) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(leading: const CloseButton()),
|
||||||
|
body: Center(child: Text("Errore: $_errorMessage")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// Usiamo Scaffold dentro il Dialog per avere l'AppBar e poter chiudere
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Visualizzatore PDF"),
|
title: const Text("Anteprima PDF"),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: PdfViewPinch(
|
body: PdfViewPinch(controller: _pdfController),
|
||||||
controller: _pdfController,
|
|
||||||
// pdfx gestisce nativamente il pinch to zoom!
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class CustomerRepository {
|
|||||||
.upsert(customer.toJson())
|
.upsert(customer.toJson())
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
return CustomerModel.fromJson(response);
|
return CustomerModel.fromMap(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw 'Errore durante il salvataggio del cliente: $e';
|
throw 'Errore durante il salvataggio del cliente: $e';
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ class CustomerRepository {
|
|||||||
.eq('id', customer.id!)
|
.eq('id', customer.id!)
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
return CustomerModel.fromJson(response);
|
return CustomerModel.fromMap(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw 'Errore durante la modifica del cliente: $e';
|
throw 'Errore durante la modifica del cliente: $e';
|
||||||
}
|
}
|
||||||
@@ -43,12 +43,15 @@ class CustomerRepository {
|
|||||||
try {
|
try {
|
||||||
final response = await _supabase
|
final response = await _supabase
|
||||||
.from('customer')
|
.from('customer')
|
||||||
.select('*, customer_file(count)')
|
.select('''
|
||||||
|
*,
|
||||||
|
customer_file(*)
|
||||||
|
''')
|
||||||
.eq('company_id', companyId)
|
.eq('company_id', companyId)
|
||||||
.eq('is_active', true)
|
.eq('is_active', true)
|
||||||
.order('nome');
|
.order('nome');
|
||||||
|
|
||||||
return (response as List).map((c) => CustomerModel.fromJson(c)).toList();
|
return (response as List).map((c) => CustomerModel.fromMap(c)).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw 'Errore nel recupero clienti';
|
throw 'Errore nel recupero clienti';
|
||||||
}
|
}
|
||||||
@@ -67,7 +70,7 @@ class CustomerRepository {
|
|||||||
.or('nome.ilike.%$query%,telefono.ilike.%$query%')
|
.or('nome.ilike.%$query%,telefono.ilike.%$query%')
|
||||||
.limit(10);
|
.limit(10);
|
||||||
|
|
||||||
return (response as List).map((c) => CustomerModel.fromJson(c)).toList();
|
return (response as List).map((c) => CustomerModel.fromMap(c)).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -110,7 +113,7 @@ class CustomerRepository {
|
|||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
name: cleanFileName.fileNameWithoutExtension(),
|
name: cleanFileName.fileNameWithoutExtension(),
|
||||||
extension: cleanFileName.fileExtension(),
|
extension: cleanFileName.fileExtension(),
|
||||||
url: '',
|
url: storagePath,
|
||||||
fileSize: fileSize,
|
fileSize: fileSize,
|
||||||
);
|
);
|
||||||
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
|
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
|
||||||
@@ -133,13 +136,9 @@ class CustomerRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String publicUrl = _supabase.storage
|
|
||||||
.from('documents')
|
|
||||||
.getPublicUrl(storagePath);
|
|
||||||
|
|
||||||
final response = await _supabase
|
final response = await _supabase
|
||||||
.from('customer_file')
|
.from('customer_file')
|
||||||
.insert(fileToSave.copyWith(url: publicUrl).toMap())
|
.insert(fileToSave.toMap())
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flux/core/utils/string_extensions.dart';
|
import 'package:flux/core/utils/string_extensions.dart';
|
||||||
|
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||||
|
|
||||||
class CustomerModel extends Equatable {
|
class CustomerModel extends Equatable {
|
||||||
final String? id; // Bigint in SQL
|
final String? id; // Bigint in SQL
|
||||||
@@ -12,7 +13,7 @@ class CustomerModel extends Equatable {
|
|||||||
final bool nonDisturbare;
|
final bool nonDisturbare;
|
||||||
final String companyId; // UUID
|
final String companyId; // UUID
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final int fileCount;
|
final List<CustomerFileModel> files;
|
||||||
|
|
||||||
const CustomerModel({
|
const CustomerModel({
|
||||||
this.id,
|
this.id,
|
||||||
@@ -25,7 +26,7 @@ class CustomerModel extends Equatable {
|
|||||||
this.nonDisturbare = false,
|
this.nonDisturbare = false,
|
||||||
required this.companyId,
|
required this.companyId,
|
||||||
this.isActive = true,
|
this.isActive = true,
|
||||||
this.fileCount = 0,
|
this.files = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -40,7 +41,7 @@ class CustomerModel extends Equatable {
|
|||||||
nonDisturbare,
|
nonDisturbare,
|
||||||
companyId,
|
companyId,
|
||||||
isActive,
|
isActive,
|
||||||
fileCount,
|
files,
|
||||||
];
|
];
|
||||||
|
|
||||||
CustomerModel copyWith({
|
CustomerModel copyWith({
|
||||||
@@ -54,7 +55,7 @@ class CustomerModel extends Equatable {
|
|||||||
bool? nonDisturbare,
|
bool? nonDisturbare,
|
||||||
String? companyId,
|
String? companyId,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
int? fileCount,
|
List<CustomerFileModel>? files,
|
||||||
}) {
|
}) {
|
||||||
return CustomerModel(
|
return CustomerModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -67,32 +68,31 @@ class CustomerModel extends Equatable {
|
|||||||
nonDisturbare: nonDisturbare ?? this.nonDisturbare,
|
nonDisturbare: nonDisturbare ?? this.nonDisturbare,
|
||||||
companyId: companyId ?? this.companyId,
|
companyId: companyId ?? this.companyId,
|
||||||
isActive: isActive ?? this.isActive,
|
isActive: isActive ?? this.isActive,
|
||||||
fileCount: fileCount ?? this.fileCount,
|
files: files ?? this.files,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory CustomerModel.fromJson(Map<String, dynamic> json) {
|
factory CustomerModel.fromMap(Map<String, dynamic> map) {
|
||||||
int count = 0;
|
|
||||||
if (json['customer_file'] != null &&
|
|
||||||
(json['customer_file'] as List).isNotEmpty) {
|
|
||||||
count = json['customer_file'][0]['count'] ?? 0;
|
|
||||||
}
|
|
||||||
return CustomerModel(
|
return CustomerModel(
|
||||||
id: json['id'] as String,
|
id: map['id'] as String,
|
||||||
createdAt: json['created_at'] != null
|
createdAt: map['created_at'] != null
|
||||||
? DateTime.parse(json['created_at'])
|
? DateTime.parse(map['created_at'])
|
||||||
: null,
|
: null,
|
||||||
nome: (json['nome'] as String).myFormat(),
|
nome: (map['nome'] as String).myFormat(),
|
||||||
telefono: json['telefono'],
|
telefono: map['telefono'],
|
||||||
email: json['email'],
|
email: map['email'],
|
||||||
note: json['note'] ?? '',
|
note: map['note'] ?? '',
|
||||||
dataUltimoContatto: json['data_ultimo_contatto'] != null
|
dataUltimoContatto: map['data_ultimo_contatto'] != null
|
||||||
? DateTime.parse(json['data_ultimo_contatto'])
|
? DateTime.parse(map['data_ultimo_contatto'])
|
||||||
: null,
|
: null,
|
||||||
nonDisturbare: json['non_disturbare'] ?? false,
|
nonDisturbare: map['non_disturbare'] ?? false,
|
||||||
companyId: json['company_id'] as String,
|
companyId: map['company_id'] as String,
|
||||||
isActive: json['is_active'] ?? true,
|
isActive: map['is_active'] ?? true,
|
||||||
fileCount: count,
|
files:
|
||||||
|
(map['customer_file'] as List?)
|
||||||
|
?.map((x) => CustomerFileModel.fromMap(x))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,11 +229,11 @@ class _CustomerTile extends StatelessWidget {
|
|||||||
style: TextStyle(color: context.secondaryText),
|
style: TextStyle(color: context.secondaryText),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (customer.fileCount > 0) ...[
|
if (customer.files.isNotEmpty) ...[
|
||||||
Text(' - ', style: TextStyle(color: context.secondaryText)),
|
Text(' - ', style: TextStyle(color: context.secondaryText)),
|
||||||
Icon(Icons.attach_file, size: 14, color: context.accent),
|
Icon(Icons.attach_file, size: 14, color: context.accent),
|
||||||
Text(
|
Text(
|
||||||
'${customer.fileCount} doc',
|
'${customer.files.length} doc',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: context.accent,
|
color: context.accent,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
companyId: _sessionBloc.state.company!.id,
|
companyId: _sessionBloc.state.company!.id,
|
||||||
),
|
),
|
||||||
status: ServicesStatus.ready,
|
status: ServicesStatus.ready,
|
||||||
files: [],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -212,7 +211,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
final serviceToSave = state.currentService!.copyWith(isBozza: isBozza);
|
final serviceToSave = state.currentService!.copyWith(isBozza: isBozza);
|
||||||
|
|
||||||
// 2. Salvataggio corazzato
|
// 2. Salvataggio corazzato
|
||||||
await _repository.saveFullService(serviceToSave, state.files);
|
await _repository.saveFullService(serviceToSave);
|
||||||
|
|
||||||
// 3. Reset e ricaricamento
|
// 3. Reset e ricaricamento
|
||||||
emit(state.copyWith(status: ServicesStatus.saved, currentService: null));
|
emit(state.copyWith(status: ServicesStatus.saved, currentService: null));
|
||||||
@@ -230,32 +229,34 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
// --- GESTIONE ALLEGATI LOCALI ---
|
// --- GESTIONE ALLEGATI LOCALI ---
|
||||||
|
|
||||||
void addAttachments(List<PlatformFile> files) {
|
void addAttachments(List<PlatformFile> files) {
|
||||||
// Trasformiamo i PlatformFile in ServiceFileModel "temporanei"
|
|
||||||
final newAttachments = files.map((file) {
|
final newAttachments = files.map((file) {
|
||||||
return ServiceFileModel(
|
return ServiceFileModel(
|
||||||
id: '', // ID vuoto perché non ancora su DB
|
id: null, // Meglio null se non è su DB
|
||||||
serviceId: state.currentService?.id ?? '',
|
serviceId: state.currentService?.id ?? '',
|
||||||
name: file.name.fileNameWithoutExtension(),
|
name: file.name.fileNameWithoutExtension(),
|
||||||
extension: file.name.fileExtension(),
|
extension: file.name.fileExtension(),
|
||||||
url: '', // URL vuoto perché non ancora caricato
|
url: '',
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
localBytes: file.bytes, // Fondamentale per l'upload!
|
localBytes: file.bytes,
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// Uniamo i file esistenti (remoti + locali già aggiunti) con i nuovi
|
// Creiamo una nuova lista pulita
|
||||||
final updatedList = [
|
final List<ServiceFileModel> updatedList = [
|
||||||
...(state.currentService?.files ?? []),
|
...(state.currentService?.files ?? []),
|
||||||
...newAttachments,
|
...newAttachments,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Emettiamo lo stato assicurandoci che il ServiceModel venga clonato
|
||||||
|
if (state.currentService != null) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
currentService: state.currentService?.copyWith(files: updatedList),
|
currentService: state.currentService!.copyWith(files: updatedList),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void removeAttachment(int index) {
|
void removeAttachment(int index) {
|
||||||
if (state.currentService == null) return;
|
if (state.currentService == null) return;
|
||||||
@@ -295,7 +296,9 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (savedFile.url.isEmpty) {
|
if (savedFile.url.isEmpty) {
|
||||||
throw Exception("Errore: URL del file non trovato dopo il salvataggio.");
|
throw Exception(
|
||||||
|
"Errore: URL del file non trovato dopo il salvataggio.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Chiamiamo il repository per la copia fisica nel database del cliente
|
// 3. Chiamiamo il repository per la copia fisica nel database del cliente
|
||||||
@@ -308,12 +311,13 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
// 4. Feedback all'utente
|
// 4. Feedback all'utente
|
||||||
// Potresti emettere un successo o mostrare un toast
|
// Potresti emettere un successo o mostrare un toast
|
||||||
emit(state.copyWith(status: ServicesStatus.success));
|
emit(state.copyWith(status: ServicesStatus.success));
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(
|
emit(
|
||||||
|
state.copyWith(
|
||||||
status: ServicesStatus.failure,
|
status: ServicesStatus.failure,
|
||||||
errorMessage: "Errore durante la copia del file: $e",
|
errorMessage: "Errore durante la copia del file: $e",
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ class ServicesState extends Equatable {
|
|||||||
final String query;
|
final String query;
|
||||||
final DateTimeRange? dateRange;
|
final DateTimeRange? dateRange;
|
||||||
final bool hasReachedMax;
|
final bool hasReachedMax;
|
||||||
final List<ServiceFileModel> files;
|
|
||||||
|
|
||||||
const ServicesState({
|
const ServicesState({
|
||||||
required this.status,
|
required this.status,
|
||||||
@@ -20,7 +19,6 @@ class ServicesState extends Equatable {
|
|||||||
this.query = '',
|
this.query = '',
|
||||||
this.dateRange,
|
this.dateRange,
|
||||||
this.hasReachedMax = false,
|
this.hasReachedMax = false,
|
||||||
this.files = const [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ServicesState copyWith({
|
ServicesState copyWith({
|
||||||
@@ -31,7 +29,6 @@ class ServicesState extends Equatable {
|
|||||||
String? query,
|
String? query,
|
||||||
DateTimeRange? dateRange,
|
DateTimeRange? dateRange,
|
||||||
bool? hasReachedMax,
|
bool? hasReachedMax,
|
||||||
List<ServiceFileModel>? files,
|
|
||||||
}) {
|
}) {
|
||||||
return ServicesState(
|
return ServicesState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
@@ -41,7 +38,6 @@ class ServicesState extends Equatable {
|
|||||||
query: query ?? this.query,
|
query: query ?? this.query,
|
||||||
dateRange: dateRange ?? this.dateRange,
|
dateRange: dateRange ?? this.dateRange,
|
||||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||||
files: files ?? this.files,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +50,5 @@ class ServicesState extends Equatable {
|
|||||||
query,
|
query,
|
||||||
dateRange,
|
dateRange,
|
||||||
hasReachedMax,
|
hasReachedMax,
|
||||||
files,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,10 +83,7 @@ class ServicesRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
||||||
Future<void> saveFullService(
|
Future<void> saveFullService(ServiceModel service) async {
|
||||||
ServiceModel service,
|
|
||||||
List<ServiceFileModel> files,
|
|
||||||
) async {
|
|
||||||
try {
|
try {
|
||||||
// 1. Upsert del record principale
|
// 1. Upsert del record principale
|
||||||
final serviceData = await _supabase
|
final serviceData = await _supabase
|
||||||
@@ -153,16 +150,16 @@ class ServicesRepository {
|
|||||||
if (insertTasks.isNotEmpty) {
|
if (insertTasks.isNotEmpty) {
|
||||||
await Future.wait(insertTasks);
|
await Future.wait(insertTasks);
|
||||||
}
|
}
|
||||||
if (files.isNotEmpty) {
|
if (service.files.isNotEmpty) {
|
||||||
final List<Future> uploadTasks = [];
|
final List<Future> uploadTasks = [];
|
||||||
|
|
||||||
for (var file in files) {
|
for (var file in service.files) {
|
||||||
final storagePath =
|
final storagePath =
|
||||||
'$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}';
|
'$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}';
|
||||||
final String mimeType = file.extension.toLowerCase() == 'pdf'
|
final String mimeType = file.extension.toLowerCase() == 'pdf'
|
||||||
? 'application/pdf'
|
? 'application/pdf'
|
||||||
: 'image/${file.extension}';
|
: 'image/${file.extension}';
|
||||||
final fileToSave = file.copyWith(serviceId: newId);
|
final fileToSave = file.copyWith(serviceId: newId, url: storagePath);
|
||||||
|
|
||||||
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
|
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
|
||||||
Future<void> uploadAndLink() async {
|
Future<void> uploadAndLink() async {
|
||||||
@@ -182,13 +179,7 @@ class ServicesRepository {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// B. Otteniamo l'URL pubblico e scriviamo il record del file nel DB
|
await _supabase.from('service_file').insert(fileToSave.toMap());
|
||||||
final String publicUrl = _supabase.storage
|
|
||||||
.from('documents')
|
|
||||||
.getPublicUrl(storagePath);
|
|
||||||
await _supabase
|
|
||||||
.from('service_file')
|
|
||||||
.insert(fileToSave.copyWith(url: publicUrl).toMap());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadTasks.add(uploadAndLink());
|
uploadTasks.add(uploadAndLink());
|
||||||
|
|||||||
@@ -93,5 +93,6 @@ class ServiceFileModel extends Equatable {
|
|||||||
url,
|
url,
|
||||||
serviceId,
|
serviceId,
|
||||||
fileSize,
|
fileSize,
|
||||||
|
localBytes,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ServicesCubit, ServicesState>(
|
return BlocBuilder<ServicesCubit, ServicesState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final files = state.files;
|
final files = state.currentService?.files ?? [];
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -168,8 +168,14 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: MediaQuery.of(context).size.height * 0.8,
|
height: MediaQuery.of(context).size.height * 0.8,
|
||||||
child: file.isPdf
|
child: file.isPdf
|
||||||
? PdfViewerWidget(url: file.url)
|
? PdfViewerWidget(
|
||||||
: ImageViewerWidget(url: file.url),
|
storagePath: file.url.isNotEmpty ? file.url : null,
|
||||||
|
bytes: file.localBytes,
|
||||||
|
)
|
||||||
|
: ImageViewerWidget(
|
||||||
|
storagePath: file.url.isNotEmpty ? file.url : null,
|
||||||
|
bytes: file.localBytes,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user