refactor dashboard store ticket list
This commit is contained in:
@@ -18,6 +18,9 @@ import 'package:flux/features/customers/models/customer_model.dart';
|
|||||||
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
||||||
import 'package:flux/features/customers/ui/customer_form_screen.dart';
|
import 'package:flux/features/customers/ui/customer_form_screen.dart';
|
||||||
import 'package:flux/features/customers/ui/customers_list_screen.dart';
|
import 'package:flux/features/customers/ui/customers_list_screen.dart';
|
||||||
|
import 'package:flux/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart';
|
||||||
|
import 'package:flux/features/home/dashboard_store_ticket_list/blocs/dashboard_store_ticket_list_cubit.dart';
|
||||||
|
import 'package:flux/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart';
|
||||||
import 'package:flux/features/home/ui/home_screen.dart';
|
import 'package:flux/features/home/ui/home_screen.dart';
|
||||||
import 'package:flux/features/master_data/master_data_hub_content.dart';
|
import 'package:flux/features/master_data/master_data_hub_content.dart';
|
||||||
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
||||||
@@ -142,7 +145,29 @@ class AppRouter {
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: Routes.home,
|
name: Routes.home,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return const HomeScreen();
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider<DashboardStoreOperationListCubit>(
|
||||||
|
create: (context) => DashboardStoreOperationListCubit(
|
||||||
|
companyId: sessionCubit.state.company?.id,
|
||||||
|
storeId: sessionCubit.state.currentStore?.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider<DashboardTaskListCubit>(
|
||||||
|
create: (context) => DashboardTaskListCubit(
|
||||||
|
companyId: sessionCubit.state.company?.id,
|
||||||
|
staffId: sessionCubit.state.currentStaffMember?.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider<DashboardStoreTicketListCubit>(
|
||||||
|
create: (context) => DashboardStoreTicketListCubit(
|
||||||
|
companyId: sessionCubit.state.company?.id,
|
||||||
|
storeId: sessionCubit.state.currentStore?.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const HomeScreen(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/features/tickets/data/ticket_repository.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
|
part 'dashboard_store_ticket_list_state.dart';
|
||||||
|
|
||||||
|
class DashboardStoreTicketListCubit
|
||||||
|
extends Cubit<DashboardStoreTicketListState> {
|
||||||
|
final TicketRepository _repository = GetIt.I.get<TicketRepository>();
|
||||||
|
final String? companyId;
|
||||||
|
final String? storeId;
|
||||||
|
StreamSubscription<void>? _subscription;
|
||||||
|
|
||||||
|
DashboardStoreTicketListCubit({
|
||||||
|
required this.companyId,
|
||||||
|
required this.storeId,
|
||||||
|
}) : super(
|
||||||
|
const DashboardStoreTicketListState(
|
||||||
|
status: DashboardStoreTicketListStatus.initial,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void stopListening() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_subscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void startListening() {
|
||||||
|
stopListening();
|
||||||
|
|
||||||
|
emit(state.copyWith(status: DashboardStoreTicketListStatus.loading));
|
||||||
|
|
||||||
|
// Primo caricamento
|
||||||
|
_loadTicketsSilently();
|
||||||
|
|
||||||
|
// Inizio ascolto campanello
|
||||||
|
try {
|
||||||
|
_subscription = _repository
|
||||||
|
.getLatestStoreTicketsStream(storeId: storeId!, limit: 10)
|
||||||
|
.listen((_) {
|
||||||
|
// Quando il campanello suona (qualcosa è cambiato a DB), ricarichiamo!
|
||||||
|
_loadTicketsSilently();
|
||||||
|
});
|
||||||
|
} on Exception catch (e) {
|
||||||
|
debugPrint(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadTicketsSilently() async {
|
||||||
|
try {
|
||||||
|
final tickets = await _repository.fetchTickets(
|
||||||
|
companyId: companyId!,
|
||||||
|
storeId: storeId,
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: DashboardStoreTicketListStatus.success,
|
||||||
|
tickets: tickets,
|
||||||
|
errorMessage: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: DashboardStoreTicketListStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
part of 'dashboard_store_ticket_list_cubit.dart';
|
||||||
|
|
||||||
|
enum DashboardStoreTicketListStatus { initial, loading, success, failure }
|
||||||
|
|
||||||
|
class DashboardStoreTicketListState extends Equatable {
|
||||||
|
final DashboardStoreTicketListStatus status;
|
||||||
|
final List<TicketModel> tickets;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
const DashboardStoreTicketListState({
|
||||||
|
this.status = DashboardStoreTicketListStatus.initial,
|
||||||
|
this.tickets = const [],
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
DashboardStoreTicketListState copyWith({
|
||||||
|
DashboardStoreTicketListStatus? status,
|
||||||
|
List<TicketModel>? tickets,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return DashboardStoreTicketListState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
tickets: tickets ?? this.tickets,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [status, tickets, errorMessage];
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/routes/routes.dart';
|
||||||
|
import 'package:flux/core/theme/theme.dart';
|
||||||
|
import 'package:flux/features/home/dashboard_store_ticket_list/blocs/dashboard_store_ticket_list_cubit.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_status_extension.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class DashboardStoreTicketListCard extends StatelessWidget {
|
||||||
|
const DashboardStoreTicketListCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _DashboardStoreTicketListCardContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DashboardStoreTicketListCardContent extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
const color = Colors.blue;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => context.pushNamed(Routes.tickets),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// --- HEADER DELLA CARD ---
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.design_services_outlined,
|
||||||
|
color: color,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"Ticket recenti",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
color: context.primaryText,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// --- CORPO DELLA CARD (LA LISTA REAL-TIME) ---
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
BlocBuilder<
|
||||||
|
DashboardStoreTicketListCubit,
|
||||||
|
DashboardStoreTicketListState
|
||||||
|
>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.status ==
|
||||||
|
DashboardStoreTicketListStatus.loading ||
|
||||||
|
state.status ==
|
||||||
|
DashboardStoreTicketListStatus.initial) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status ==
|
||||||
|
DashboardStoreTicketListStatus.failure) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
"Errore di caricamento",
|
||||||
|
style: TextStyle(color: theme.colorScheme.error),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.tickets.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
"Nessun ticket recente.",
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.secondaryText,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: state.tickets.length,
|
||||||
|
separatorBuilder: (context, index) => Divider(
|
||||||
|
height: 1,
|
||||||
|
color: theme.dividerColor.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final ticket = state.tickets[index];
|
||||||
|
final statusColor = ticket.ticketStatus.color;
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => context.pushNamed(
|
||||||
|
Routes.ticketForm,
|
||||||
|
extra: (createdBy: null, ticket: ticket),
|
||||||
|
pathParameters: {'id': ticket.id!},
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height:
|
||||||
|
30, // Un'altezza fissa per farlo comparire!
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
4,
|
||||||
|
), // Angoli smussati per stile
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: Text(
|
||||||
|
ticket.customer?.name ??
|
||||||
|
'Cliente sconosciuto',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: context.primaryText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: Text(
|
||||||
|
ticket.targetModelName ??
|
||||||
|
'Modello sconosciuto',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryText,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${ticket.createdAt?.day}/${ticket.createdAt?.month}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.secondaryText,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ class DashboardTaskListCubit extends Cubit<DashboardTaskListState> {
|
|||||||
: super(const DashboardTaskListState());
|
: super(const DashboardTaskListState());
|
||||||
|
|
||||||
void startListening() {
|
void startListening() {
|
||||||
|
stopListening();
|
||||||
|
|
||||||
emit(state.copyWith(status: DashboardTaskListStatus.loading));
|
emit(state.copyWith(status: DashboardTaskListStatus.loading));
|
||||||
|
|
||||||
// Primo caricamento
|
// Primo caricamento
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
|
||||||
import 'package:flux/core/routes/routes.dart';
|
import 'package:flux/core/routes/routes.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart';
|
import 'package:flux/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:flux/features/tasks/models/task_status.dart';
|
import 'package:flux/features/tasks/models/task_status.dart';
|
||||||
|
|
||||||
@@ -13,24 +11,7 @@ class DashboardTasksCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Recuperiamo lo staff (o l'utente) loggato
|
return _DashboardTasksCardContent();
|
||||||
// Adatta il getter in base a come è strutturato il tuo SessionState
|
|
||||||
final currentStaffId = GetIt.I
|
|
||||||
.get<SessionCubit>()
|
|
||||||
.state
|
|
||||||
.currentStaffMember
|
|
||||||
?.id;
|
|
||||||
final companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
|
|
||||||
|
|
||||||
if (currentStaffId == null) {
|
|
||||||
return const SizedBox.shrink(); // Sicurezza se lo stato non è pronto
|
|
||||||
}
|
|
||||||
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
DashboardTaskListCubit(staffId: currentStaffId, companyId: companyId),
|
|
||||||
child: const _DashboardTasksCardContent(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:flux/features/tickets/data/ticket_repository.dart';
|
|
||||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
|
|
||||||
part 'latest_store_tickets_events.dart';
|
|
||||||
part 'latest_store_tickets_state.dart';
|
|
||||||
|
|
||||||
class LatestStoreTicketsBloc
|
|
||||||
extends Bloc<LatestStoreTicketsEvent, LatestStoreTicketsState> {
|
|
||||||
final _repository = GetIt.I.get<TicketRepository>();
|
|
||||||
LatestStoreTicketsBloc()
|
|
||||||
: super(
|
|
||||||
const LatestStoreTicketsState(status: LatestStoreTicketsStatus.initial),
|
|
||||||
) {
|
|
||||||
on<InitLatestStoreTicketsEvent>((event, emit) async {
|
|
||||||
emit(state.copyWith(status: LatestStoreTicketsStatus.loading));
|
|
||||||
try {
|
|
||||||
final hydratedStream = _repository
|
|
||||||
.getLatestStoreTicketsStream(storeId: event.storeId, limit: 10)
|
|
||||||
.asyncMap((List<TicketModel> rawTickets) async {
|
|
||||||
List<TicketModel> fullyHydratedTickets = [];
|
|
||||||
|
|
||||||
for (TicketModel ticket in rawTickets) {
|
|
||||||
TicketModel fullTicket = await _repository.getTicketById(
|
|
||||||
ticket.id!,
|
|
||||||
);
|
|
||||||
fullyHydratedTickets.add(fullTicket);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullyHydratedTickets;
|
|
||||||
});
|
|
||||||
await emit.forEach(
|
|
||||||
hydratedStream,
|
|
||||||
onData: (List<TicketModel> fullyHydratedTickets) {
|
|
||||||
return state.copyWith(
|
|
||||||
tickets: fullyHydratedTickets,
|
|
||||||
status: LatestStoreTicketsStatus.success,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (error, stackTrace) => state.copyWith(
|
|
||||||
status: LatestStoreTicketsStatus.failure,
|
|
||||||
error: error.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: LatestStoreTicketsStatus.failure,
|
|
||||||
error: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
part of 'latest_store_tickets_bloc.dart';
|
|
||||||
|
|
||||||
abstract class LatestStoreTicketsEvent extends Equatable {
|
|
||||||
const LatestStoreTicketsEvent();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [];
|
|
||||||
}
|
|
||||||
|
|
||||||
class InitLatestStoreTicketsEvent extends LatestStoreTicketsEvent {
|
|
||||||
final String storeId;
|
|
||||||
|
|
||||||
const InitLatestStoreTicketsEvent(this.storeId);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [storeId];
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
part of 'latest_store_tickets_bloc.dart';
|
|
||||||
|
|
||||||
enum LatestStoreTicketsStatus { initial, loading, success, failure }
|
|
||||||
|
|
||||||
class LatestStoreTicketsState extends Equatable {
|
|
||||||
final LatestStoreTicketsStatus status;
|
|
||||||
final String? error;
|
|
||||||
final List<TicketModel> tickets;
|
|
||||||
const LatestStoreTicketsState({
|
|
||||||
required this.status,
|
|
||||||
this.error,
|
|
||||||
this.tickets = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [status, error, tickets];
|
|
||||||
|
|
||||||
LatestStoreTicketsState copyWith({
|
|
||||||
LatestStoreTicketsStatus? status,
|
|
||||||
String? error,
|
|
||||||
List<TicketModel>? tickets,
|
|
||||||
}) {
|
|
||||||
return LatestStoreTicketsState(
|
|
||||||
status: status ?? this.status,
|
|
||||||
error: error,
|
|
||||||
tickets: tickets ?? this.tickets,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
|
||||||
import 'package:flux/core/routes/routes.dart';
|
|
||||||
import 'package:flux/core/theme/theme.dart';
|
|
||||||
import 'package:flux/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart';
|
|
||||||
import 'package:flux/features/tickets/models/ticket_status_extension.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
class LatestStoreTicketsCard extends StatelessWidget {
|
|
||||||
const LatestStoreTicketsCard({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final currentStoreId = context.read<SessionCubit>().state.currentStore?.id;
|
|
||||||
|
|
||||||
return BlocProvider(
|
|
||||||
// 1. Creiamo il Bloc e facciamo partire subito la query
|
|
||||||
create: (context) =>
|
|
||||||
LatestStoreTicketsBloc()
|
|
||||||
..add(InitLatestStoreTicketsEvent(currentStoreId ?? '')),
|
|
||||||
child: BlocListener<SessionCubit, SessionState>(
|
|
||||||
// 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream!
|
|
||||||
listenWhen: (previous, current) =>
|
|
||||||
previous.currentStore?.id != current.currentStore?.id,
|
|
||||||
listener: (context, state) {
|
|
||||||
if (state.currentStore?.id != null) {
|
|
||||||
context.read<LatestStoreTicketsBloc>().add(
|
|
||||||
InitLatestStoreTicketsEvent(state.currentStore!.id!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: _LatestStoreTicketsCardContent(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LatestStoreTicketsCardContent extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
const color = Colors.blue;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => context.pushNamed(Routes.tickets),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// --- HEADER DELLA CARD ---
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.design_services_outlined,
|
|
||||||
color: color,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
"Ticket recenti",
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
color: context.primaryText,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// --- CORPO DELLA CARD (LA LISTA REAL-TIME) ---
|
|
||||||
Expanded(
|
|
||||||
child: BlocBuilder<LatestStoreTicketsBloc, LatestStoreTicketsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.status == LatestStoreTicketsStatus.loading ||
|
|
||||||
state.status == LatestStoreTicketsStatus.initial) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.status == LatestStoreTicketsStatus.failure) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
"Errore di caricamento",
|
|
||||||
style: TextStyle(color: theme.colorScheme.error),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.tickets.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Text(
|
|
||||||
"Nessun ticket recente.",
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.secondaryText,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
itemCount: state.tickets.length,
|
|
||||||
separatorBuilder: (context, index) => Divider(
|
|
||||||
height: 1,
|
|
||||||
color: theme.dividerColor.withValues(alpha: 0.3),
|
|
||||||
),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final ticket = state.tickets[index];
|
|
||||||
final statusColor = ticket.ticketStatus.color;
|
|
||||||
return InkWell(
|
|
||||||
onTap: () => context.pushNamed(
|
|
||||||
Routes.ticketForm,
|
|
||||||
extra: (createdBy: null, ticket: ticket),
|
|
||||||
pathParameters: {'id': ticket.id!},
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 8,
|
|
||||||
height:
|
|
||||||
30, // Un'altezza fissa per farlo comparire!
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: statusColor,
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
4,
|
|
||||||
), // Angoli smussati per stile
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Expanded(
|
|
||||||
flex: 5,
|
|
||||||
child: Text(
|
|
||||||
ticket.customer?.name ??
|
|
||||||
'Cliente sconosciuto',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: context.primaryText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
flex: 5,
|
|
||||||
child: Text(
|
|
||||||
ticket.targetModelName ??
|
|
||||||
'Modello sconosciuto',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: context.primaryText,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${ticket.createdAt?.day}/${ticket.createdAt?.month}",
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.secondaryText,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,11 @@ import 'package:flux/core/theme/theme.dart';
|
|||||||
import 'package:flux/core/utils/extensions.dart';
|
import 'package:flux/core/utils/extensions.dart';
|
||||||
import 'package:flux/core/widgets/staff_selector_modal.dart';
|
import 'package:flux/core/widgets/staff_selector_modal.dart';
|
||||||
import 'package:flux/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart';
|
import 'package:flux/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart';
|
||||||
|
import 'package:flux/features/home/dashboard_store_ticket_list/blocs/dashboard_store_ticket_list_cubit.dart';
|
||||||
|
import 'package:flux/features/home/dashboard_store_ticket_list/ui/dashboard_store_ticket_list_card.dart';
|
||||||
import 'package:flux/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart';
|
import 'package:flux/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart';
|
||||||
import 'package:flux/features/home/dashboard_task_list/ui/dashboard_tasks_card.dart';
|
import 'package:flux/features/home/dashboard_task_list/ui/dashboard_tasks_card.dart';
|
||||||
import 'package:flux/features/home/dashboard_store_operation_list/ui/latest_store_operations_card.dart';
|
import 'package:flux/features/home/dashboard_store_operation_list/ui/latest_store_operations_card.dart';
|
||||||
import 'package:flux/features/home/latest_store_tickets/ui/latest_store_tickets_card.dart';
|
|
||||||
import 'package:flux/features/home/ui/quick_actions_widget.dart';
|
import 'package:flux/features/home/ui/quick_actions_widget.dart';
|
||||||
import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart';
|
import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart';
|
||||||
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
|
||||||
@@ -38,22 +39,31 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
onPause: () {
|
onPause: () {
|
||||||
// L'utente ha messo l'app in background (es. per rispondere a un messaggio su WhatsApp)
|
// L'utente ha messo l'app in background (es. per rispondere a un messaggio su WhatsApp)
|
||||||
// Chiudiamo i rubinetti per non sprecare risorse e prevenire crash
|
// Chiudiamo i rubinetti per non sprecare risorse e prevenire crash
|
||||||
context.read<DashboardStoreOperationListCubit>().stopListening();
|
_stopListeners();
|
||||||
context.read<DashboardTaskListCubit>().stopListening();
|
|
||||||
debugPrint('App in background: Stream sospesi.');
|
debugPrint('App in background: Stream sospesi.');
|
||||||
},
|
},
|
||||||
onResume: () {
|
onResume: () {
|
||||||
// L'utente è tornato sull'app!
|
// L'utente è tornato sull'app!
|
||||||
// Riappriamo i rubinetti, Supabase ricreerà una connessione fresca
|
// Riappriamo i rubinetti, Supabase ricreerà una connessione fresca
|
||||||
context.read<DashboardStoreOperationListCubit>().startListening();
|
_startListeners();
|
||||||
context.read<DashboardTaskListCubit>().startListening();
|
|
||||||
debugPrint('App in foreground: Stream riattivati.');
|
debugPrint('App in foreground: Stream riattivati.');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Facciamo partire gli stream la primissima volta che la schermata si carica
|
// Facciamo partire gli stream la primissima volta che la schermata si carica
|
||||||
|
_startListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopListeners() {
|
||||||
|
context.read<DashboardStoreOperationListCubit>().stopListening();
|
||||||
|
context.read<DashboardTaskListCubit>().stopListening();
|
||||||
|
context.read<DashboardStoreTicketListCubit>().stopListening();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startListeners() {
|
||||||
context.read<DashboardStoreOperationListCubit>().startListening();
|
context.read<DashboardStoreOperationListCubit>().startListening();
|
||||||
context.read<DashboardTaskListCubit>().startListening();
|
context.read<DashboardTaskListCubit>().startListening();
|
||||||
|
context.read<DashboardStoreTicketListCubit>().startListening();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -66,87 +76,70 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final sessionCubit = GetIt.I.get<SessionCubit>();
|
|
||||||
|
|
||||||
return MultiBlocProvider(
|
return Scaffold(
|
||||||
providers: [
|
backgroundColor: theme.colorScheme.surface,
|
||||||
BlocProvider<DashboardStoreOperationListCubit>(
|
body: SafeArea(
|
||||||
create: (context) => DashboardStoreOperationListCubit(
|
child: Column(
|
||||||
companyId: sessionCubit.state.company?.id,
|
children: [
|
||||||
storeId: sessionCubit.state.currentStore?.id,
|
// ==========================================
|
||||||
),
|
// 1. HEADER FISSO (Non scrolla mai)
|
||||||
),
|
// ==========================================
|
||||||
BlocProvider<DashboardTaskListCubit>(
|
Container(
|
||||||
create: (context) => DashboardTaskListCubit(
|
padding: const EdgeInsets.all(24.0),
|
||||||
companyId: sessionCubit.state.company?.id,
|
// Un leggero colore di sfondo aiuta a staccare l'header quando il contenuto ci passa sotto
|
||||||
staffId: sessionCubit.state.currentStaffMember?.id,
|
color: theme.colorScheme.surface,
|
||||||
),
|
child: _buildHeader(context, theme),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
child: Scaffold(
|
|
||||||
backgroundColor: theme.colorScheme.surface,
|
|
||||||
body: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// ==========================================
|
|
||||||
// 1. HEADER FISSO (Non scrolla mai)
|
|
||||||
// ==========================================
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(24.0),
|
|
||||||
// Un leggero colore di sfondo aiuta a staccare l'header quando il contenuto ci passa sotto
|
|
||||||
color: theme.colorScheme.surface,
|
|
||||||
child: _buildHeader(context, theme),
|
|
||||||
),
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 2. CORPO DELLA DASHBOARD (Scrollabile)
|
// 2. CORPO DELLA DASHBOARD (Scrollabile)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
// --- QUICK ACTIONS: AZIONI RAPIDE ---
|
// --- QUICK ACTIONS: AZIONI RAPIDE ---
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
||||||
child: _buildQuickActions(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
|
||||||
|
|
||||||
// --- I WIDGET DELLA DASHBOARD (Responsive Grid) ---
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
sliver: SliverGrid(
|
child: _buildQuickActions(context),
|
||||||
gridDelegate:
|
|
||||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
|
||||||
maxCrossAxisExtent: 500,
|
|
||||||
mainAxisSpacing: 16,
|
|
||||||
crossAxisSpacing: 16,
|
|
||||||
childAspectRatio: 1.3,
|
|
||||||
),
|
|
||||||
delegate: SliverChildListDelegate([
|
|
||||||
DashboardStoreOperationListCard(),
|
|
||||||
LatestStoreTicketsCard(),
|
|
||||||
_buildDashboardWidget(
|
|
||||||
title: context.l10n.homeExpiringContracts,
|
|
||||||
icon: Icons.assignment_late_outlined,
|
|
||||||
color: Colors.orange,
|
|
||||||
context: context,
|
|
||||||
),
|
|
||||||
DashboardNotesWidget(),
|
|
||||||
DashboardTasksCard(),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Spazio finale per non far attaccare l'ultima card al fondo
|
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
||||||
const SliverToBoxAdapter(child: SizedBox(height: 40)),
|
|
||||||
],
|
// --- I WIDGET DELLA DASHBOARD (Responsive Grid) ---
|
||||||
),
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
gridDelegate:
|
||||||
|
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 500,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
childAspectRatio: 1.3,
|
||||||
|
),
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
DashboardStoreOperationListCard(),
|
||||||
|
DashboardStoreTicketListCard(),
|
||||||
|
_buildDashboardWidget(
|
||||||
|
title: context.l10n.homeExpiringContracts,
|
||||||
|
icon: Icons.assignment_late_outlined,
|
||||||
|
color: Colors.orange,
|
||||||
|
context: context,
|
||||||
|
),
|
||||||
|
DashboardNotesWidget(),
|
||||||
|
DashboardTasksCard(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Spazio finale per non far attaccare l'ultima card al fondo
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: 40)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ class TicketRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- RECUPERO PAGINATO CON FILTRI E JOIN DEI TICKET DI TUTTA L'AZIENDA ---
|
// --- RECUPERO PAGINATO CON FILTRI E JOIN DEI TICKET DI TUTTA L'AZIENDA ---
|
||||||
Future<List<TicketModel>> fetchCompanyTickets({
|
Future<List<TicketModel>> fetchTickets({
|
||||||
|
required String? companyId,
|
||||||
|
String? storeId,
|
||||||
required int offset,
|
required int offset,
|
||||||
int limit = 50,
|
int limit = 50,
|
||||||
String? searchTerm,
|
String? searchTerm,
|
||||||
@@ -96,7 +98,7 @@ class TicketRepository {
|
|||||||
target_model:${Tables.models}!ticket_model_id_1_fkey (*),
|
target_model:${Tables.models}!ticket_model_id_1_fkey (*),
|
||||||
source_model:${Tables.models}!ticket_model_id_2_fkey (*)
|
source_model:${Tables.models}!ticket_model_id_2_fkey (*)
|
||||||
''')
|
''')
|
||||||
.eq('company_id', GetIt.I.get<SessionCubit>().state.company!.id!);
|
.eq('company_id', companyId!);
|
||||||
|
|
||||||
// Filtro Range Date
|
// Filtro Range Date
|
||||||
if (dateRange != null) {
|
if (dateRange != null) {
|
||||||
@@ -105,6 +107,10 @@ class TicketRepository {
|
|||||||
.lte('created_at', dateRange.end.toIso8601String());
|
.lte('created_at', dateRange.end.toIso8601String());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (storeId != null) {
|
||||||
|
query = query.or('store_id.eq.$storeId,store_id.is.null');
|
||||||
|
}
|
||||||
|
|
||||||
if (ticketStatusFilter != null) {
|
if (ticketStatusFilter != null) {
|
||||||
query = query.eq('status', ticketStatusFilter.value);
|
query = query.eq('status', ticketStatusFilter.value);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user