j
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
// TODO: implement event handlers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
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: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];
|
||||
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: [
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user