u
This commit is contained in:
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||||
import 'package:flux/features/auth/ui/auth_screen.dart';
|
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||||
import 'package:flux/features/company/ui/create_company_screen.dart';
|
import 'package:flux/features/company/ui/create_company_screen.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_and_dashboard/ui/home_screen.dart';
|
import 'package:flux/features/home_and_dashboard/ui/home_screen.dart';
|
||||||
import 'package:flux/features/store/ui/create_store_screen.dart';
|
import 'package:flux/features/store/ui/create_store_screen.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -58,6 +60,14 @@ class AppRouter {
|
|||||||
path: '/create-store',
|
path: '/create-store',
|
||||||
builder: (context, state) => const CreateStoreScreen(),
|
builder: (context, state) => const CreateStoreScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/customer/:id',
|
||||||
|
builder: (context, state) {
|
||||||
|
// Recuperiamo l'oggetto customer passato tramite extra
|
||||||
|
final customer = state.extra as CustomerModel;
|
||||||
|
return CustomerDetailScreen(customer: customer);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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:flutter_svg/svg.dart';
|
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/core/widgets/flux_logo.dart';
|
import 'package:flux/core/widgets/flux_logo.dart';
|
||||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
import 'package:flux/core/widgets/flux_text_field.dart';
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import 'package:flux/core/blocs/session/session_bloc.dart';
|
|||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
import 'package:flux/core/widgets/flux_text_field.dart';
|
||||||
import 'package:flux/features/company/models/company_model.dart';
|
import 'package:flux/features/company/models/company_model.dart';
|
||||||
import 'package:flux/features/settings/settings.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
|
|
||||||
class CreateCompanyScreen extends StatefulWidget {
|
class CreateCompanyScreen extends StatefulWidget {
|
||||||
const CreateCompanyScreen({super.key});
|
const CreateCompanyScreen({super.key});
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import '../models/customer_model.dart';
|
import '../models/customer_model.dart';
|
||||||
@@ -38,7 +42,7 @@ class CustomerRepository {
|
|||||||
try {
|
try {
|
||||||
final response = await _client
|
final response = await _client
|
||||||
.from('customer')
|
.from('customer')
|
||||||
.select()
|
.select('*, customer_file(count)')
|
||||||
.eq('company_id', companyId)
|
.eq('company_id', companyId)
|
||||||
.eq('is_active', true)
|
.eq('is_active', true)
|
||||||
.order('nome');
|
.order('nome');
|
||||||
@@ -67,4 +71,89 @@ class CustomerRepository {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recupera i file di un cliente specifico
|
||||||
|
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
|
||||||
|
try {
|
||||||
|
final response = await _client
|
||||||
|
.from('customer_file')
|
||||||
|
.select()
|
||||||
|
.eq('customer_id', customerId);
|
||||||
|
|
||||||
|
return (response as List)
|
||||||
|
.map((f) => CustomerFileModel.fromJson(f))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw 'Errore recupero file: $e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Salva il riferimento del file nel DB
|
||||||
|
Future<void> saveFileReference(CustomerFileModel file) async {
|
||||||
|
await _client.from('customer_file').insert(file.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carica un file e salva il riferimento nel database
|
||||||
|
Future<CustomerFileModel> uploadAndRegisterFile({
|
||||||
|
required String customerId,
|
||||||
|
required PlatformFile pickedFile,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final user = _client.auth.currentUser;
|
||||||
|
if (user == null) throw 'Utente non autenticato';
|
||||||
|
|
||||||
|
final fileName = pickedFile.name;
|
||||||
|
final extension = pickedFile.extension ?? '';
|
||||||
|
final path =
|
||||||
|
'${user.id}/$customerId/${DateTime.now().millisecondsSinceEpoch}_$fileName';
|
||||||
|
|
||||||
|
// Usiamo bytes invece del path per massima compatibilità
|
||||||
|
if (pickedFile.bytes == null && pickedFile.path == null) {
|
||||||
|
throw 'Impossibile leggere il contenuto del file';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se siamo su desktop/mobile abbiamo il path, su web abbiamo i bytes
|
||||||
|
if (pickedFile.bytes != null) {
|
||||||
|
await _client.storage
|
||||||
|
.from('documents')
|
||||||
|
.uploadBinary(path, pickedFile.bytes!);
|
||||||
|
} else {
|
||||||
|
final file = File(pickedFile.path!);
|
||||||
|
await _client.storage.from('documents').upload(path, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String publicUrl = _client.storage
|
||||||
|
.from('documents')
|
||||||
|
.getPublicUrl(path);
|
||||||
|
|
||||||
|
final fileRecord = CustomerFileModel(
|
||||||
|
customerId: customerId,
|
||||||
|
name: fileName,
|
||||||
|
url: publicUrl,
|
||||||
|
extension: extension,
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = await _client
|
||||||
|
.from('customer_file')
|
||||||
|
.insert(fileRecord.toJson())
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return CustomerFileModel.fromJson(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw 'Errore durante l\'upload: $e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aggiorna la lista degli URL nel database
|
||||||
|
Future<void> updateCustomerDocuments(int id, List<String> urls) async {
|
||||||
|
await _client.from('customer').update({'document_urls': urls}).eq('id', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elimina un file dallo storage
|
||||||
|
Future<void> deleteDocument(String fullPath) async {
|
||||||
|
// Il path dovrebbe essere ricavato dall'URL
|
||||||
|
final path = fullPath.split('documents/').last;
|
||||||
|
await _client.storage.from('documents').remove([path]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
lib/features/customers/models/customer_file_model.dart
Normal file
45
lib/features/customers/models/customer_file_model.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class CustomerFileModel extends Equatable {
|
||||||
|
final int? id;
|
||||||
|
final String customerId; // Riferimento UUID
|
||||||
|
final String name;
|
||||||
|
final String url;
|
||||||
|
final String extension;
|
||||||
|
final DateTime? createdAt;
|
||||||
|
|
||||||
|
const CustomerFileModel({
|
||||||
|
this.id,
|
||||||
|
required this.customerId,
|
||||||
|
required this.name,
|
||||||
|
required this.url,
|
||||||
|
required this.extension,
|
||||||
|
this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CustomerFileModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CustomerFileModel(
|
||||||
|
id: json['id'],
|
||||||
|
customerId: json['customer_id'],
|
||||||
|
name: json['name'],
|
||||||
|
url: json['url'],
|
||||||
|
extension: json['extension'] ?? '',
|
||||||
|
createdAt: json['created_at'] != null
|
||||||
|
? DateTime.parse(json['created_at'])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
'customer_id': customerId,
|
||||||
|
'name': name,
|
||||||
|
'url': url,
|
||||||
|
'extension': extension,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, customerId, name, url, extension, createdAt];
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
class CustomerModel extends Equatable {
|
class CustomerModel extends Equatable {
|
||||||
final int? id; // Bigint in SQL
|
final String? id; // Bigint in SQL
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
final String nome;
|
final String nome;
|
||||||
final String telefono;
|
final String telefono;
|
||||||
@@ -11,6 +11,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;
|
||||||
|
|
||||||
const CustomerModel({
|
const CustomerModel({
|
||||||
this.id,
|
this.id,
|
||||||
@@ -23,6 +24,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,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -37,10 +39,11 @@ class CustomerModel extends Equatable {
|
|||||||
nonDisturbare,
|
nonDisturbare,
|
||||||
companyId,
|
companyId,
|
||||||
isActive,
|
isActive,
|
||||||
|
fileCount,
|
||||||
];
|
];
|
||||||
|
|
||||||
CustomerModel copyWith({
|
CustomerModel copyWith({
|
||||||
int? id,
|
String? id,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
String? nome,
|
String? nome,
|
||||||
String? telefono,
|
String? telefono,
|
||||||
@@ -50,6 +53,7 @@ class CustomerModel extends Equatable {
|
|||||||
bool? nonDisturbare,
|
bool? nonDisturbare,
|
||||||
String? companyId,
|
String? companyId,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
|
int? fileCount,
|
||||||
}) {
|
}) {
|
||||||
return CustomerModel(
|
return CustomerModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -62,12 +66,18 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory CustomerModel.fromJson(Map<String, dynamic> json) {
|
factory CustomerModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
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'],
|
id: json['id'] as String,
|
||||||
createdAt: json['created_at'] != null
|
createdAt: json['created_at'] != null
|
||||||
? DateTime.parse(json['created_at'])
|
? DateTime.parse(json['created_at'])
|
||||||
: null,
|
: null,
|
||||||
@@ -79,8 +89,9 @@ class CustomerModel extends Equatable {
|
|||||||
? DateTime.parse(json['data_ultimo_contatto'])
|
? DateTime.parse(json['data_ultimo_contatto'])
|
||||||
: null,
|
: null,
|
||||||
nonDisturbare: json['non_disturbare'] ?? false,
|
nonDisturbare: json['non_disturbare'] ?? false,
|
||||||
companyId: json['company_id'],
|
companyId: json['company_id'] as String,
|
||||||
isActive: json['is_active'] ?? true,
|
isActive: json['is_active'] ?? true,
|
||||||
|
fileCount: count,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
271
lib/features/customers/ui/customer_detail_screen.dart
Normal file
271
lib/features/customers/ui/customer_detail_screen.dart
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flux/core/theme/theme.dart';
|
||||||
|
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||||
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
|
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
|
class CustomerDetailScreen extends StatefulWidget {
|
||||||
|
final CustomerModel customer;
|
||||||
|
const CustomerDetailScreen({super.key, required this.customer});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomerDetailScreen> createState() => _CustomerDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
||||||
|
final _repository = GetIt.I<CustomerRepository>();
|
||||||
|
List<CustomerFileModel> _files = [];
|
||||||
|
bool _isLoadingFiles = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFiles() async {
|
||||||
|
try {
|
||||||
|
final files = await _repository.getCustomerFiles(
|
||||||
|
widget.customer.id.toString(),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_files = files;
|
||||||
|
_isLoadingFiles = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _isLoadingFiles = false);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickAndUpload() async {
|
||||||
|
// Chiamata statica pulita
|
||||||
|
FilePickerResult? result = await FilePicker.pickFiles(
|
||||||
|
allowMultiple: true,
|
||||||
|
type: FileType.any,
|
||||||
|
withData: true, // Fondamentale per avere i bytes pronti se servono
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
for (var pickedFile in result.files) {
|
||||||
|
try {
|
||||||
|
final newFile = await _repository.uploadAndRegisterFile(
|
||||||
|
customerId: widget.customer.id.toString(),
|
||||||
|
pickedFile: pickedFile,
|
||||||
|
);
|
||||||
|
setState(() => _files.add(newFile));
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Errore upload ${pickedFile.name}: $e")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: context.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
widget.customer.nome,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
backgroundColor: context.background,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// COLONNA SINISTRA: ANAGRAFICA
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: _buildInfoSection(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// DIVISORE VERTICALE
|
||||||
|
VerticalDivider(
|
||||||
|
width: 1,
|
||||||
|
color: context.accent.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
|
|
||||||
|
// COLONNA DESTRA: DOCUMENTI
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: _buildDocumentSection(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoSection() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_infoTile(Icons.phone_android, "Telefono", widget.customer.telefono),
|
||||||
|
_infoTile(
|
||||||
|
Icons.email_outlined,
|
||||||
|
"Email",
|
||||||
|
widget.customer.email.isEmpty ? "Non fornita" : widget.customer.email,
|
||||||
|
),
|
||||||
|
_infoTile(
|
||||||
|
Icons.notes_outlined,
|
||||||
|
"Note",
|
||||||
|
widget.customer.note.isEmpty
|
||||||
|
? "Nessun appunto"
|
||||||
|
: widget.customer.note,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (widget.customer.nonDisturbare)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.privacy_tip, color: Colors.red, size: 20),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
"PRIVACY: Non disturbare",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDocumentSection() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"DOCUMENTI",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.accent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _pickAndUpload,
|
||||||
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
|
label: const Text("CARICA FILE"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (_isLoadingFiles)
|
||||||
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (_files.isEmpty)
|
||||||
|
const Center(child: Text("Nessun documento presente"))
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: GridView.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 200,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
childAspectRatio: 1.2,
|
||||||
|
),
|
||||||
|
itemCount: _files.length,
|
||||||
|
itemBuilder: (context, index) => _FileCard(file: _files[index]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _infoTile(IconData icon, String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: context.accent),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(color: context.secondaryText, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileCard extends StatelessWidget {
|
||||||
|
final CustomerFileModel file;
|
||||||
|
const _FileCard({required this.file});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.background,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: context.accent.withValues(alpha: 0.1)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(_getFileIcon(file.extension), size: 48, color: context.accent),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
file.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getFileIcon(String ext) {
|
||||||
|
switch (ext.toLowerCase()) {
|
||||||
|
case 'pdf':
|
||||||
|
return Icons.picture_as_pdf;
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'png':
|
||||||
|
return Icons.image;
|
||||||
|
default:
|
||||||
|
return Icons.insert_drive_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:flux/core/theme/theme.dart';
|
|||||||
import 'package:flux/features/customers/blocs/customer_bloc.dart';
|
import 'package:flux/features/customers/blocs/customer_bloc.dart';
|
||||||
import 'package:flux/features/customers/models/customer_model.dart';
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
import 'package:flux/features/customers/ui/customer_form.dart';
|
import 'package:flux/features/customers/ui/customer_form.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class CustomersContent extends StatefulWidget {
|
class CustomersContent extends StatefulWidget {
|
||||||
const CustomersContent({super.key});
|
const CustomersContent({super.key});
|
||||||
@@ -147,7 +148,10 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
final customer = state.customers[index];
|
final customer = state.customers[index];
|
||||||
return _CustomerTile(
|
return _CustomerTile(
|
||||||
customer: customer,
|
customer: customer,
|
||||||
onTap: () => _openCustomerForm(customer: customer),
|
onTap: () => context.push(
|
||||||
|
'/customer/${customer.id}',
|
||||||
|
extra: customer,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -222,6 +226,27 @@ class _CustomerTile extends StatelessWidget {
|
|||||||
customer.telefono,
|
customer.telefono,
|
||||||
style: TextStyle(color: context.secondaryText),
|
style: TextStyle(color: context.secondaryText),
|
||||||
),
|
),
|
||||||
|
if (customer.email.isNotEmpty) ...[
|
||||||
|
Text(' - ', style: TextStyle(color: context.secondaryText)),
|
||||||
|
Icon(Icons.email, size: 14, color: context.secondaryText),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
customer.email,
|
||||||
|
style: TextStyle(color: context.secondaryText),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (customer.fileCount > 0) ...[
|
||||||
|
Text(' - ', style: TextStyle(color: context.secondaryText)),
|
||||||
|
Icon(Icons.attach_file, size: 14, color: context.accent),
|
||||||
|
Text(
|
||||||
|
'${customer.fileCount} doc',
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.accent,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
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/features/settings/settings.dart';
|
|
||||||
import 'package:flux/features/store/bloc/store_bloc.dart';
|
import 'package:flux/features/store/bloc/store_bloc.dart';
|
||||||
import 'package:flux/features/store/models/store_model.dart';
|
import 'package:flux/features/store/models/store_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
import 'package:flux/core/widgets/flux_text_field.dart';
|
||||||
@@ -23,7 +21,6 @@ class _CreateStoreScreenState extends State<CreateStoreScreen> {
|
|||||||
final _capController = TextEditingController();
|
final _capController = TextEditingController();
|
||||||
final _comuneController = TextEditingController();
|
final _comuneController = TextEditingController();
|
||||||
final _provinciaController = TextEditingController();
|
final _provinciaController = TextEditingController();
|
||||||
final AppSettings _settings = GetIt.I<AppSettings>();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import app_links
|
import app_links
|
||||||
|
import file_picker
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||||
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- app_links (6.4.1):
|
- app_links (6.4.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- file_picker (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -10,6 +12,7 @@ PODS:
|
|||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
||||||
|
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
@@ -17,6 +20,8 @@ DEPENDENCIES:
|
|||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_links:
|
app_links:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
||||||
|
file_picker:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
@@ -26,6 +31,7 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
||||||
|
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||||
|
|||||||
@@ -10,5 +10,13 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -6,5 +6,13 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
40
pubspec.lock
40
pubspec.lock
@@ -105,6 +105,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -121,6 +129,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "3.4.0"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.12"
|
||||||
equatable:
|
equatable:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -153,6 +169,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.2"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -174,6 +198,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.34"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -797,6 +829,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.15.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ environment:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
equatable: ^2.0.8
|
equatable: ^2.0.8
|
||||||
|
file_picker: ^11.0.2
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^9.1.1
|
flutter_bloc: ^9.1.1
|
||||||
|
|||||||
Reference in New Issue
Block a user