refined responsive ui for dashboard and start customer work
This commit is contained in:
@@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||
import 'package:flux/features/company/ui/create_company_screen.dart';
|
||||
import 'package:flux/features/home/ui/dashboard_content.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/ui/home_screen.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:async';
|
||||
|
||||
|
||||
82
lib/features/customers/blocs/customer_bloc.dart
Normal file
82
lib/features/customers/blocs/customer_bloc.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||
import 'package:flux/features/customers/models/customer_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
part 'customer_events.dart';
|
||||
part 'customer_state.dart';
|
||||
|
||||
class CustomerBloc extends Bloc<CustomerEvent, CustomerState> {
|
||||
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
|
||||
|
||||
CustomerBloc() : super(const CustomerState()) {
|
||||
on<LoadCustomersRequested>(_onLoadCustomers);
|
||||
on<CreateCustomerRequested>(_onCreateCustomer);
|
||||
on<SearchCustomersRequested>(_onSearchCustomers);
|
||||
}
|
||||
|
||||
Future<void> _onLoadCustomers(
|
||||
LoadCustomersRequested event,
|
||||
Emitter<CustomerState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: CustomerStatus.loading));
|
||||
try {
|
||||
final customers = await _repository.getCustomers(event.companyId);
|
||||
emit(
|
||||
state.copyWith(status: CustomerStatus.success, customers: customers),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CustomerStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCreateCustomer(
|
||||
CreateCustomerRequested event,
|
||||
Emitter<CustomerState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: CustomerStatus.loading));
|
||||
try {
|
||||
final newCustomer = await _repository.createCustomer(event.customer);
|
||||
|
||||
// Aggiorniamo la lista locale aggiungendo il nuovo cliente in cima
|
||||
final updatedList = List<CustomerModel>.from(state.customers)
|
||||
..insert(0, newCustomer);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CustomerStatus.success,
|
||||
customers: updatedList,
|
||||
lastCreatedCustomer:
|
||||
newCustomer, // Lo passiamo per le Dialog "al volo"
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CustomerStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSearchCustomers(
|
||||
SearchCustomersRequested event,
|
||||
Emitter<CustomerState> emit,
|
||||
) async {
|
||||
// Non mettiamo loading per evitare flickering durante la digitazione
|
||||
try {
|
||||
final results = await _repository.searchCustomers(
|
||||
event.companyId,
|
||||
event.query,
|
||||
);
|
||||
emit(state.copyWith(status: CustomerStatus.success, customers: results));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
26
lib/features/customers/blocs/customer_events.dart
Normal file
26
lib/features/customers/blocs/customer_events.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
part of 'customer_bloc.dart';
|
||||
|
||||
abstract class CustomerEvent extends Equatable {
|
||||
const CustomerEvent();
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
// Carica tutti i clienti dell'azienda
|
||||
class LoadCustomersRequested extends CustomerEvent {
|
||||
final String companyId;
|
||||
const LoadCustomersRequested(this.companyId);
|
||||
}
|
||||
|
||||
// Crea un cliente (usato sia dalla lista che dalla Dialog operazioni)
|
||||
class CreateCustomerRequested extends CustomerEvent {
|
||||
final CustomerModel customer;
|
||||
const CreateCustomerRequested(this.customer);
|
||||
}
|
||||
|
||||
// Ricerca in tempo reale
|
||||
class SearchCustomersRequested extends CustomerEvent {
|
||||
final String companyId;
|
||||
final String query;
|
||||
const SearchCustomersRequested(this.companyId, this.query);
|
||||
}
|
||||
40
lib/features/customers/blocs/customer_state.dart
Normal file
40
lib/features/customers/blocs/customer_state.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
part of 'customer_bloc.dart';
|
||||
|
||||
enum CustomerStatus { initial, loading, success, failure }
|
||||
|
||||
class CustomerState extends Equatable {
|
||||
final CustomerStatus status;
|
||||
final List<CustomerModel> customers; // Per la lista generale
|
||||
final CustomerModel?
|
||||
lastCreatedCustomer; // <--- Fondamentale per la Dialog "al volo"
|
||||
final String? errorMessage;
|
||||
|
||||
const CustomerState({
|
||||
this.status = CustomerStatus.initial,
|
||||
this.customers = const [],
|
||||
this.lastCreatedCustomer,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
CustomerState copyWith({
|
||||
CustomerStatus? status,
|
||||
List<CustomerModel>? customers,
|
||||
CustomerModel? lastCreatedCustomer,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return CustomerState(
|
||||
status: status ?? this.status,
|
||||
customers: customers ?? this.customers,
|
||||
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
status,
|
||||
customers,
|
||||
lastCreatedCustomer,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
56
lib/features/customers/data/customer_repository.dart
Normal file
56
lib/features/customers/data/customer_repository.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/customer_model.dart';
|
||||
|
||||
class CustomerRepository {
|
||||
final SupabaseClient _client = GetIt.I<SupabaseClient>();
|
||||
|
||||
// Crea un nuovo cliente
|
||||
Future<CustomerModel> createCustomer(CustomerModel customer) async {
|
||||
try {
|
||||
final response = await _client
|
||||
.from('customer')
|
||||
.insert(customer.toJson())
|
||||
.select()
|
||||
.single();
|
||||
return CustomerModel.fromJson(response);
|
||||
} catch (e) {
|
||||
throw 'Errore durante la creazione del cliente: $e';
|
||||
}
|
||||
}
|
||||
|
||||
// Recupera tutti i clienti dell'azienda
|
||||
Future<List<CustomerModel>> getCustomers(String companyId) async {
|
||||
try {
|
||||
final response = await _client
|
||||
.from('customer')
|
||||
.select()
|
||||
.eq('company_id', companyId)
|
||||
.eq('is_active', true)
|
||||
.order('nome');
|
||||
|
||||
return (response as List).map((c) => CustomerModel.fromJson(c)).toList();
|
||||
} catch (e) {
|
||||
throw 'Errore nel recupero clienti';
|
||||
}
|
||||
}
|
||||
|
||||
// Ricerca clienti per nome o telefono (fondamentale per la UX)
|
||||
Future<List<CustomerModel>> searchCustomers(
|
||||
String companyId,
|
||||
String query,
|
||||
) async {
|
||||
try {
|
||||
final response = await _client
|
||||
.from('customer')
|
||||
.select()
|
||||
.eq('company_id', companyId)
|
||||
.or('nome.ilike.%$query%,telefono.ilike.%$query%')
|
||||
.limit(10);
|
||||
|
||||
return (response as List).map((c) => CustomerModel.fromJson(c)).toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
101
lib/features/customers/models/customer_model.dart
Normal file
101
lib/features/customers/models/customer_model.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class CustomerModel extends Equatable {
|
||||
final BigInt? id; // Bigint in SQL
|
||||
final DateTime? createdAt;
|
||||
final String nome;
|
||||
final String telefono;
|
||||
final String email;
|
||||
final String note;
|
||||
final DateTime? dataUltimoContatto;
|
||||
final bool nonDisturbare;
|
||||
final String companyId; // UUID
|
||||
final bool isActive;
|
||||
|
||||
const CustomerModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.nome,
|
||||
required this.telefono,
|
||||
required this.email,
|
||||
required this.note,
|
||||
this.dataUltimoContatto,
|
||||
this.nonDisturbare = false,
|
||||
required this.companyId,
|
||||
this.isActive = true,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
createdAt,
|
||||
nome,
|
||||
telefono,
|
||||
email,
|
||||
note,
|
||||
dataUltimoContatto,
|
||||
nonDisturbare,
|
||||
companyId,
|
||||
isActive,
|
||||
];
|
||||
|
||||
CustomerModel copyWith({
|
||||
BigInt? id,
|
||||
DateTime? createdAt,
|
||||
String? nome,
|
||||
String? telefono,
|
||||
String? email,
|
||||
String? note,
|
||||
DateTime? dataUltimoContatto,
|
||||
bool? nonDisturbare,
|
||||
String? companyId,
|
||||
bool? isActive,
|
||||
}) {
|
||||
return CustomerModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
nome: nome ?? this.nome,
|
||||
telefono: telefono ?? this.telefono,
|
||||
email: email ?? this.email,
|
||||
note: note ?? this.note,
|
||||
dataUltimoContatto: dataUltimoContatto ?? this.dataUltimoContatto,
|
||||
nonDisturbare: nonDisturbare ?? this.nonDisturbare,
|
||||
companyId: companyId ?? this.companyId,
|
||||
isActive: isActive ?? this.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
factory CustomerModel.fromJson(Map<String, dynamic> json) {
|
||||
return CustomerModel(
|
||||
id: json['id'],
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
nome: json['nome'],
|
||||
telefono: json['telefono'],
|
||||
email: json['email'],
|
||||
note: json['note'] ?? '',
|
||||
dataUltimoContatto: json['data_ultimo_contatto'] != null
|
||||
? DateTime.parse(json['data_ultimo_contatto'])
|
||||
: null,
|
||||
nonDisturbare: json['non_disturbare'] ?? false,
|
||||
companyId: json['company_id'],
|
||||
isActive: json['is_active'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'nome': nome,
|
||||
'telefono': telefono,
|
||||
'email': email,
|
||||
'note': note,
|
||||
if (dataUltimoContatto != null)
|
||||
'data_ultimo_contatto': dataUltimoContatto!.toIso8601String(),
|
||||
'non_disturbare': nonDisturbare,
|
||||
'company_id': companyId,
|
||||
'is_active': isActive,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
|
||||
class DashboardContent extends StatelessWidget {
|
||||
final bool? isLargeScreen;
|
||||
|
||||
const DashboardContent({super.key, this.isLargeScreen});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Ascoltiamo il SessionBloc per avere i dati in tempo reale
|
||||
return BlocBuilder<SessionBloc, SessionState>(
|
||||
builder: (context, state) {
|
||||
final store = state.selectedStore;
|
||||
final company = state.company;
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
// Un'AppBar elegante che si rimpicciolisce
|
||||
SliverAppBar(
|
||||
expandedHeight: 120.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
title: Text(
|
||||
store?.nome ?? 'Flux Dashboard',
|
||||
style: TextStyle(color: context.primaryText),
|
||||
),
|
||||
background: Container(color: context.background),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => Navigator.pushNamed(context, '/settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1200,
|
||||
), // Larghezza massima "confortevole"
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWelcomeHeader(context, company?.ragioneSociale),
|
||||
const SizedBox(height: 30),
|
||||
_SectionTitle(title: 'AZIONI RAPIDE'),
|
||||
const SizedBox(height: 16),
|
||||
_buildGridActions(context),
|
||||
const SizedBox(height: 32),
|
||||
_SectionTitle(title: 'STATO NEGOZIO'),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoCard(context, store),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcomeHeader(BuildContext context, String? companyName) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Bentornato,',
|
||||
style: TextStyle(color: context.secondaryText, fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
companyName ?? 'La tua Azienda',
|
||||
style: TextStyle(
|
||||
color: context.primaryText,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridActions(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Calcoliamo il numero di colonne in base alla larghezza
|
||||
// Sotto i 600px (mobile): 2 colonne
|
||||
// Tra 600 e 1000px (tablet): 3 o 4 colonne
|
||||
// Sopra i 1000px (desktop): 6 colonne
|
||||
int crossAxisCount = 2;
|
||||
if (constraints.maxWidth > 1000) {
|
||||
crossAxisCount = 6;
|
||||
} else if (constraints.maxWidth > 600) {
|
||||
crossAxisCount = 4;
|
||||
}
|
||||
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
// Su desktop rendiamo i tasti un po' più squadrati (1.0)
|
||||
// Su mobile manteniamo il rettangolo (1.5)
|
||||
childAspectRatio: constraints.maxWidth > 600 ? 1.2 : 1.5,
|
||||
children: [
|
||||
_ActionCard(
|
||||
label: 'Nuova Op',
|
||||
icon: Icons.add_task_rounded,
|
||||
color: context.accent,
|
||||
onTap: () {},
|
||||
),
|
||||
_ActionCard(
|
||||
label: 'Clienti',
|
||||
icon: Icons.people_alt_rounded,
|
||||
color: Colors.orange,
|
||||
onTap: () {},
|
||||
),
|
||||
_ActionCard(
|
||||
label: 'Campagne',
|
||||
icon: Icons.campaign_rounded,
|
||||
color: Colors.purple,
|
||||
onTap: () {},
|
||||
),
|
||||
_ActionCard(
|
||||
label: 'Report',
|
||||
icon: Icons.analytics_rounded,
|
||||
color: Colors.teal,
|
||||
onTap: () {},
|
||||
),
|
||||
// Se siamo su desktop, possiamo aggiungere altri slot senza affollare
|
||||
if (constraints.maxWidth > 600) ...[
|
||||
_ActionCard(
|
||||
label: 'Impostazioni',
|
||||
icon: Icons.settings,
|
||||
color: Colors.grey,
|
||||
onTap: () {},
|
||||
),
|
||||
_ActionCard(
|
||||
label: 'Supporto',
|
||||
icon: Icons.help_outline,
|
||||
color: Colors.blueGrey,
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(BuildContext context, dynamic store) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: context.accent.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.location_on_rounded, color: context.accent),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${store?.indirizzo}, ${store?.comune}',
|
||||
style: TextStyle(color: context.primaryText),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionCard extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionCard({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.background,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: context.accent.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionTitle extends StatelessWidget {
|
||||
final String title;
|
||||
const _SectionTitle({required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: context.secondaryText,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
206
lib/features/home_and_dashboard/ui/dashboard_content.dart
Normal file
206
lib/features/home_and_dashboard/ui/dashboard_content.dart
Normal file
@@ -0,0 +1,206 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
|
||||
class DashboardContent extends StatelessWidget {
|
||||
final bool isLargeScreen;
|
||||
const DashboardContent({super.key, this.isLargeScreen = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SessionBloc, SessionState>(
|
||||
builder: (context, state) {
|
||||
final store = state.selectedStore;
|
||||
final company = state.company;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: context.background,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
expandedHeight: 100.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: context.background,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
titlePadding: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
title: Text(
|
||||
store?.nome ?? 'Dashboard',
|
||||
style: TextStyle(
|
||||
color: context.primaryText,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 1200),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWelcome(context, company?.ragioneSociale),
|
||||
const SizedBox(height: 32),
|
||||
const _SectionTitle(title: 'AZIONI RAPIDE'),
|
||||
const SizedBox(height: 16),
|
||||
_buildAdaptiveGrid(context),
|
||||
const SizedBox(height: 40),
|
||||
const _SectionTitle(title: 'INFO PUNTO VENDITA'),
|
||||
const SizedBox(height: 16),
|
||||
_buildStoreCard(context, store),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWelcome(BuildContext context, String? name) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Benvenuto in',
|
||||
style: TextStyle(color: context.secondaryText, fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
name ?? 'Azienda',
|
||||
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAdaptiveGrid(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Logica Colonne: Mobile 2, Tablet 3, Desktop 4+
|
||||
int crossAxisCount = 2;
|
||||
if (constraints.maxWidth > 1000) {
|
||||
crossAxisCount = 5;
|
||||
} else if (constraints.maxWidth > 700) {
|
||||
crossAxisCount = 3;
|
||||
}
|
||||
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
childAspectRatio: isLargeScreen ? 1.3 : 1.5,
|
||||
children: [
|
||||
_ActionCard(
|
||||
label: 'Nuova Op',
|
||||
icon: Icons.add_task,
|
||||
color: context.accent,
|
||||
onTap: () {},
|
||||
),
|
||||
_ActionCard(
|
||||
label: 'Clienti',
|
||||
icon: Icons.people,
|
||||
color: Colors.orange,
|
||||
onTap: () {},
|
||||
),
|
||||
_ActionCard(
|
||||
label: 'Campagne',
|
||||
icon: Icons.campaign,
|
||||
color: Colors.purple,
|
||||
onTap: () {},
|
||||
),
|
||||
_ActionCard(
|
||||
label: 'Report',
|
||||
icon: Icons.analytics,
|
||||
color: Colors.teal,
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStoreCard(BuildContext context, dynamic store) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: context.accent.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: context.accent.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.location_on, color: context.accent),
|
||||
const SizedBox(width: 16),
|
||||
Text('${store?.indirizzo}, ${store?.comune} (${store?.provincia})'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Widget di supporto rimasti invariati (ActionCard e SectionTitle)
|
||||
class _ActionCard extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ActionCard({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
// CAMBIA QUI: da Border.all a BorderSide
|
||||
side: BorderSide(
|
||||
color: context.accent.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 32),
|
||||
const SizedBox(height: 8),
|
||||
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionTitle extends StatelessWidget {
|
||||
final String title;
|
||||
const _SectionTitle({required this.title});
|
||||
@override
|
||||
Widget build(BuildContext context) => Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: context.accent,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
import 'package:flux/features/home/ui/dashboard_content.dart';
|
||||
import 'dashboard_content.dart'; // Importiamo il contenuto della dashboard
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
@@ -20,25 +20,28 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
builder: (context, state) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Definiamo se siamo su uno schermo "Large" (es. sopra i 900px)
|
||||
// Se lo schermo è più largo di 900px usiamo il layout Desktop
|
||||
final bool isLargeScreen = constraints.maxWidth > 900;
|
||||
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
// --- SIDEBAR (Solo per schermi grandi) ---
|
||||
// --- SIDEBAR (Solo Desktop/Tablet) ---
|
||||
if (isLargeScreen)
|
||||
NavigationRail(
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (int index) {
|
||||
setState(() => _selectedIndex = index);
|
||||
},
|
||||
extended:
|
||||
constraints.maxWidth >
|
||||
1200, // Si allarga se c'è molto spazio
|
||||
labelType: constraints.maxWidth > 1200
|
||||
? NavigationRailLabelType.none
|
||||
: NavigationRailLabelType.all,
|
||||
onDestinationSelected: (index) =>
|
||||
setState(() => _selectedIndex = index),
|
||||
extended: constraints.maxWidth > 1200,
|
||||
backgroundColor: context.background,
|
||||
selectedIconTheme: IconThemeData(color: context.accent),
|
||||
selectedLabelTextStyle: TextStyle(
|
||||
color: context.accent,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
unselectedLabelTextStyle: TextStyle(
|
||||
color: context.secondaryText,
|
||||
),
|
||||
leading: _buildRailHeader(constraints.maxWidth > 1200),
|
||||
destinations: const [
|
||||
NavigationRailDestination(
|
||||
@@ -59,18 +62,20 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
],
|
||||
),
|
||||
|
||||
// --- CONTENUTO PRINCIPALE ---
|
||||
// --- CONTENUTO DINAMICO ---
|
||||
Expanded(
|
||||
child: _buildMainContent(context, state, isLargeScreen),
|
||||
child: _buildPageContent(_selectedIndex, isLargeScreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
// --- BOTTOM NAVIGATION (Solo per Mobile) ---
|
||||
// --- BOTTOM BAR (Solo Mobile) ---
|
||||
bottomNavigationBar: isLargeScreen
|
||||
? null
|
||||
: BottomNavigationBar(
|
||||
currentIndex: _selectedIndex,
|
||||
onTap: (index) => setState(() => _selectedIndex = index),
|
||||
selectedItemColor: context.accent,
|
||||
unselectedItemColor: context.secondaryText,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.dashboard),
|
||||
@@ -93,7 +98,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// Header per la Sidebar (Logo o Icona)
|
||||
Widget _buildRailHeader(bool isExtended) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
@@ -110,19 +114,17 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMainContent(
|
||||
BuildContext context,
|
||||
SessionState state,
|
||||
bool isLargeScreen,
|
||||
) {
|
||||
// Qui gestiamo lo switch tra le pagine
|
||||
switch (_selectedIndex) {
|
||||
// Switch tra le sottopagine
|
||||
Widget _buildPageContent(int index, bool isLargeScreen) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return DashboardContent(isLargeScreen: isLargeScreen);
|
||||
case 1:
|
||||
return const Center(child: Text('Pagina Clienti')); // La faremo!
|
||||
return const Center(child: Text('Pagina Clienti (Coming Soon)'));
|
||||
case 2:
|
||||
return const Center(child: Text('Pagina Operazioni (Coming Soon)'));
|
||||
default:
|
||||
return const DashboardContent();
|
||||
return DashboardContent(isLargeScreen: isLargeScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,12 @@ import 'package:flux/core/routes/app_router.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
import 'package:flux/core/theme/bloc/theme_bloc.dart';
|
||||
import 'package:flux/features/auth/bloc/auth_bloc.dart';
|
||||
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||
import 'package:flux/features/company/bloc/company_bloc.dart';
|
||||
import 'package:flux/features/company/data/company_repository.dart';
|
||||
import 'package:flux/features/company/ui/create_company_screen.dart';
|
||||
import 'package:flux/features/customers/blocs/customer_bloc.dart';
|
||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||
import 'package:flux/features/store/bloc/store_bloc.dart';
|
||||
import 'package:flux/features/store/data/store_repository.dart';
|
||||
import 'package:flux/features/store/ui/create_store_screen.dart';
|
||||
import 'package:flux/ui/home_screen.dart';
|
||||
import 'package:flux/features/settings/settings.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -34,6 +32,7 @@ void main() async {
|
||||
BlocProvider<AuthBloc>(create: (context) => AuthBloc()),
|
||||
BlocProvider<CompanyBloc>(create: (context) => CompanyBloc()),
|
||||
BlocProvider<StoreBloc>(create: (context) => StoreBloc()),
|
||||
BlocProvider<CustomerBloc>(create: (context) => CustomerBloc()),
|
||||
],
|
||||
child: const FluxApp(),
|
||||
),
|
||||
@@ -55,6 +54,7 @@ Future<void> setupLocator() async {
|
||||
getIt.registerLazySingleton<AppSettings>(() => AppSettings());
|
||||
getIt.registerLazySingleton<CompanyRepository>(() => CompanyRepository());
|
||||
getIt.registerLazySingleton<StoreRepository>(() => StoreRepository());
|
||||
getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository());
|
||||
}
|
||||
|
||||
class FluxApp extends StatelessWidget {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
|
||||
class AnagraficheMainView extends StatelessWidget {
|
||||
const AnagraficheMainView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Controller locale per gestire i Tab
|
||||
return DefaultTabController(
|
||||
length: 4,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Anagrafiche'),
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
indicatorColor: FluxColors.accentTurquoise,
|
||||
labelColor: FluxColors.accentTurquoise,
|
||||
unselectedLabelColor: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
tabs: [
|
||||
Tab(icon: Icon(Icons.storefront), text: 'Negozi'),
|
||||
Tab(icon: Icon(Icons.support_agent), text: 'Gestori'),
|
||||
Tab(icon: Icon(Icons.assignment_ind), text: 'Clienti'),
|
||||
Tab(icon: Icon(Icons.phone_android), text: 'Prodotti'),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: [
|
||||
// Esempi di view iniettate con Bloc dedicati (da implementare)
|
||||
/* ElencoEntitaView(tipoEntita: 'Negozi'), // Provider<AnagraficaBloc>...
|
||||
ElencoEntitaView(tipoEntita: 'Gestori'),
|
||||
ElencoEntitaView(tipoEntita: 'Clienti'),
|
||||
ElencoEntitaView(tipoEntita: 'Prodotti'), */
|
||||
Placeholder(),
|
||||
Placeholder(),
|
||||
Placeholder(),
|
||||
Placeholder(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
|
||||
class DashboardView extends StatelessWidget {
|
||||
const DashboardView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Iniezione del Bloc per la creazione di operazioni (da implementare)
|
||||
return /* BlocProvider(
|
||||
create: (context) => OperazioneBloc(), // Implementa la logica nel Bloc
|
||||
child: */ Scaffold(
|
||||
appBar: AppBar(title: Text('FLUX')),
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_WelcomeHeader(),
|
||||
SizedBox(height: 24),
|
||||
_QuickActions(), // Contiene "Nuova Operazione"
|
||||
SizedBox(height: 24),
|
||||
_RecentActivityPreview(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
class _WelcomeHeader extends StatelessWidget {
|
||||
const _WelcomeHeader();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Benvenuto,', style: Theme.of(context).textTheme.bodyMedium),
|
||||
Text(
|
||||
'Negozio Piacenza Centro',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickActions extends StatelessWidget {
|
||||
const _QuickActions();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'AZIONI RAPIDE',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(color: FluxColors.accentTurquoise),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Emetti evento al Bloc: BlocProvider.of<OperazioneBloc>(context).add(IniziaNuovaOperazione());
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Avvio Nuova Operazione...')),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('NUOVA OPERAZIONE TELCO'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecentActivityPreview extends StatelessWidget {
|
||||
const _RecentActivityPreview();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Attività Recenti',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Divider(color: Theme.of(context).textTheme.bodyMedium?.color),
|
||||
// Sostituire con BlocBuilder
|
||||
_activityTile('Nuova Linea', 'Mario Rossi', '10 min fa', context),
|
||||
_activityTile('Assistenza Tech', 'iPhone 13', '45 min fa', context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _activityTile(
|
||||
String title,
|
||||
String subtitle,
|
||||
String time,
|
||||
BuildContext context,
|
||||
) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.history, color: FluxColors.accentTurquoise),
|
||||
title: Text(title, style: Theme.of(context).textTheme.titleLarge),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: Text(time, style: Theme.of(context).textTheme.bodyMedium),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
import 'package:flux/ui/anagrafiche/anagrafiche_main_view.dart';
|
||||
import 'package:flux/ui/dashboard/dashboard_view.dart';
|
||||
import 'package:flux/features/settings/settings_view.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
static const List<Widget> _widgetOptions = <Widget>[
|
||||
DashboardView(), // Contiene Nuova Operazione
|
||||
Placeholder(),
|
||||
AnagraficheMainView(), // Gestisce [negozi, gestori, clienti, prodotti]
|
||||
SettingsView(),
|
||||
];
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final surfaceColor = Theme.of(context).colorScheme.surface;
|
||||
return Scaffold(
|
||||
body: Center(child: _widgetOptions.elementAt(_selectedIndex)),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
items: const <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
activeIcon: Icon(Icons.dashboard),
|
||||
label: 'Dashboard',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.history_edu_outlined),
|
||||
activeIcon: Icon(Icons.history_edu),
|
||||
label: 'Operazioni',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.people_alt_outlined),
|
||||
activeIcon: Icon(Icons.people_alt),
|
||||
label: 'Anagrafiche',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
activeIcon: Icon(Icons.settings),
|
||||
label: 'Impostazioni',
|
||||
),
|
||||
],
|
||||
currentIndex: _selectedIndex,
|
||||
selectedItemColor: FluxColors.accentTurquoise,
|
||||
unselectedItemColor: Theme.of(context).textTheme.bodyMedium?.color,
|
||||
backgroundColor: surfaceColor,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
onTap: _onItemTapped,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user