Phase 2 scaffold: mitra online status & pairing logic
Add mitra online/offline status with heartbeat-based auto-offline, customer-mitra pairing via Valkey pub/sub blast, session management, and control center dashboard with real-time stats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
129
mitra_app/lib/core/status/status_bloc.dart
Normal file
129
mitra_app/lib/core/status/status_bloc.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'dart:async';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../api/api_client.dart';
|
||||
|
||||
// Events
|
||||
abstract class StatusEvent extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class StatusLoadRequested extends StatusEvent {}
|
||||
class ToggleOnline extends StatusEvent {}
|
||||
class ToggleOffline extends StatusEvent {}
|
||||
class HeartbeatTick extends StatusEvent {}
|
||||
class AppPaused extends StatusEvent {}
|
||||
class AppResumed extends StatusEvent {}
|
||||
|
||||
// States
|
||||
abstract class StatusState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class StatusInitial extends StatusState {}
|
||||
|
||||
class StatusLoaded extends StatusState {
|
||||
final bool isOnline;
|
||||
StatusLoaded({required this.isOnline});
|
||||
@override
|
||||
List<Object?> get props => [isOnline];
|
||||
}
|
||||
|
||||
class StatusLoading extends StatusState {}
|
||||
|
||||
class StatusError extends StatusState {
|
||||
final String message;
|
||||
StatusError(this.message);
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
// Bloc
|
||||
class StatusBloc extends Bloc<StatusEvent, StatusState> {
|
||||
final ApiClient apiClient;
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
StatusBloc({required this.apiClient}) : super(StatusInitial()) {
|
||||
on<StatusLoadRequested>(_onLoad);
|
||||
on<ToggleOnline>(_onToggleOnline);
|
||||
on<ToggleOffline>(_onToggleOffline);
|
||||
on<HeartbeatTick>(_onHeartbeat);
|
||||
on<AppPaused>(_onAppPaused);
|
||||
on<AppResumed>(_onAppResumed);
|
||||
}
|
||||
|
||||
Future<void> _onLoad(StatusLoadRequested event, Emitter<StatusState> emit) async {
|
||||
try {
|
||||
final response = await apiClient.get('/api/mitra/status');
|
||||
final data = response['data'] as Map<String, dynamic>;
|
||||
emit(StatusLoaded(isOnline: data['is_online'] as bool));
|
||||
} catch (e) {
|
||||
emit(StatusLoaded(isOnline: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onToggleOnline(ToggleOnline event, Emitter<StatusState> emit) async {
|
||||
emit(StatusLoading());
|
||||
try {
|
||||
await apiClient.post('/api/mitra/status/online');
|
||||
_startHeartbeat();
|
||||
emit(StatusLoaded(isOnline: true));
|
||||
} catch (e) {
|
||||
emit(StatusError('Gagal mengubah status. Coba lagi.'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onToggleOffline(ToggleOffline event, Emitter<StatusState> emit) async {
|
||||
emit(StatusLoading());
|
||||
try {
|
||||
await apiClient.post('/api/mitra/status/offline');
|
||||
_stopHeartbeat();
|
||||
emit(StatusLoaded(isOnline: false));
|
||||
} catch (e) {
|
||||
emit(StatusError('Gagal mengubah status. Coba lagi.'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onHeartbeat(HeartbeatTick event, Emitter<StatusState> emit) async {
|
||||
try {
|
||||
await apiClient.post('/api/mitra/status/heartbeat');
|
||||
} catch (_) {
|
||||
// Heartbeat failure is non-critical; server will auto-offline after 45s
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onAppPaused(AppPaused event, Emitter<StatusState> emit) async {
|
||||
if (state is StatusLoaded && (state as StatusLoaded).isOnline) {
|
||||
try {
|
||||
await apiClient.post('/api/mitra/status/offline');
|
||||
} catch (_) {}
|
||||
_stopHeartbeat();
|
||||
emit(StatusLoaded(isOnline: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onAppResumed(AppResumed event, Emitter<StatusState> emit) async {
|
||||
// Do NOT auto-set online on resume; mitra must explicitly toggle
|
||||
add(StatusLoadRequested());
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_stopHeartbeat();
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (_) {
|
||||
add(HeartbeatTick());
|
||||
});
|
||||
}
|
||||
|
||||
void _stopHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_stopHeartbeat();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user