j
This commit is contained in:
@@ -17,12 +17,12 @@ class LatestStoreOperationsBloc
|
|||||||
status: LatestStoreOperationsStatus.initial,
|
status: LatestStoreOperationsStatus.initial,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
on<InitLastStoreOperationsEvent>((event, emit) async {
|
on<InitLatestStoreOperationsEvent>((event, emit) async {
|
||||||
emit(state.copyWith(status: LatestStoreOperationsStatus.loading));
|
emit(state.copyWith(status: LatestStoreOperationsStatus.loading));
|
||||||
try {
|
try {
|
||||||
// 1. Creiamo uno stream "intermedio" che idrata i dati
|
// 1. Creiamo uno stream "intermedio" che idrata i dati
|
||||||
final hydratedStream = _repository
|
final hydratedStream = _repository
|
||||||
.getLastStoreOperationsStream(storeId: event.storeId, limit: 5)
|
.getLatestStoreOperationsStream(storeId: event.storeId, limit: 10)
|
||||||
.asyncMap((List<OperationModel> rawOperations) async {
|
.asyncMap((List<OperationModel> rawOperations) async {
|
||||||
// Questo gira ad ogni "scatto" dello stream di Supabase
|
// Questo gira ad ogni "scatto" dello stream di Supabase
|
||||||
List<OperationModel> fullyHydratedOperations = [];
|
List<OperationModel> fullyHydratedOperations = [];
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ sealed class LatestStoreOperationsEvent extends Equatable {
|
|||||||
List<Object> get props => [];
|
List<Object> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
class InitLastStoreOperationsEvent extends LatestStoreOperationsEvent {
|
class InitLatestStoreOperationsEvent extends LatestStoreOperationsEvent {
|
||||||
final String storeId;
|
final String storeId;
|
||||||
|
|
||||||
const InitLastStoreOperationsEvent(this.storeId);
|
const InitLatestStoreOperationsEvent(this.storeId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [storeId];
|
List<Object> get props => [storeId];
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class LatestStoreOperationsCard extends StatelessWidget {
|
|||||||
// 1. Creiamo il Bloc e facciamo partire subito la query
|
// 1. Creiamo il Bloc e facciamo partire subito la query
|
||||||
create: (context) =>
|
create: (context) =>
|
||||||
LatestStoreOperationsBloc()
|
LatestStoreOperationsBloc()
|
||||||
..add(InitLastStoreOperationsEvent(currentStoreId ?? '')),
|
..add(InitLatestStoreOperationsEvent(currentStoreId ?? '')),
|
||||||
child: BlocListener<SessionCubit, SessionState>(
|
child: BlocListener<SessionCubit, SessionState>(
|
||||||
// 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream!
|
// 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream!
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
@@ -26,7 +26,7 @@ class LatestStoreOperationsCard extends StatelessWidget {
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.currentStore?.id != null) {
|
if (state.currentStore?.id != null) {
|
||||||
context.read<LatestStoreOperationsBloc>().add(
|
context.read<LatestStoreOperationsBloc>().add(
|
||||||
InitLastStoreOperationsEvent(state.currentStore!.id!),
|
InitLatestStoreOperationsEvent(state.currentStore!.id!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ 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/latest_store_operations/ui/latest_store_operations_card.dart';
|
import 'package:flux/features/home/latest_store_operations/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';
|
||||||
@@ -80,15 +81,7 @@ class HomeScreen extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
),
|
),
|
||||||
LatestStoreOperationsCard(),
|
LatestStoreOperationsCard(),
|
||||||
_buildDashboardWidget(
|
LatestStoreTicketsCard(),
|
||||||
title: context.l10n.homeLatestOperationTickets,
|
|
||||||
icon: Icons.support_agent_outlined,
|
|
||||||
color: Colors.purple,
|
|
||||||
context: context,
|
|
||||||
onTap: () => context.pushNamed(
|
|
||||||
Routes.tickets,
|
|
||||||
), // <-- Aggiunto!
|
|
||||||
),
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class OperationsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<OperationModel>> getLastStoreOperationsStream({
|
Stream<List<OperationModel>> getLatestStoreOperationsStream({
|
||||||
required String storeId,
|
required String storeId,
|
||||||
required int limit,
|
required int limit,
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -174,6 +174,22 @@ class TicketRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<TicketModel>> getLatestStoreTicketsStream({
|
||||||
|
required String storeId,
|
||||||
|
required int limit,
|
||||||
|
}) {
|
||||||
|
return _supabase
|
||||||
|
.from(_tableName)
|
||||||
|
.stream(primaryKey: ['id'])
|
||||||
|
.eq('store_id', storeId)
|
||||||
|
.order('created_at', ascending: false)
|
||||||
|
.limit(limit)
|
||||||
|
.map(
|
||||||
|
(listOfMaps) =>
|
||||||
|
listOfMaps.map((map) => TicketModel.fromMap(map)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Recupera un ticket specifico CON TUTTE LE RELAZIONI espanse (Cliente e Modelli)
|
/// Recupera un ticket specifico CON TUTTE LE RELAZIONI espanse (Cliente e Modelli)
|
||||||
/// Questa è la vera magia di Supabase!
|
/// Questa è la vera magia di Supabase!
|
||||||
Future<TicketModel> getTicketById(String ticketId) async {
|
Future<TicketModel> getTicketById(String ticketId) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user