feat: WebSocket temps réel + Finance Workflow + corrections

- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics)
  * Backend: KafkaEventProducer, KafkaEventConsumer
  * Mobile: WebSocketService (reconnection, heartbeat, typed events)
  * DashboardBloc: Auto-refresh depuis WebSocket events

- Finance Workflow: approbations + budgets (backend + mobile)
  * Backend: entities, services, resources, migrations Flyway V6
  * Mobile: features finance_workflow complète avec BLoC

- Corrections DI: interfaces IRepository partout
  * IProfileRepository, IOrganizationRepository, IMembreRepository
  * GetIt configuré avec @injectable

- Spec-Kit: constitution + templates mis à jour
  * .specify/memory/constitution.md enrichie
  * Templates agent, plan, spec, tasks, checklist

- Nettoyage: fichiers temporaires supprimés

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -0,0 +1,63 @@
/// Modèle de configuration des sauvegardes
/// Correspond à BackupConfigResponse du backend
library backup_config_model;
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'backup_config_model.g.dart';
@JsonSerializable(explicitToJson: true)
class BackupConfigModel extends Equatable {
final bool? autoBackupEnabled;
final String? frequency; // HOURLY, DAILY, WEEKLY
final String? retention;
final int? retentionDays;
final String? backupTime;
final bool? includeDatabase;
final bool? includeFiles;
final bool? includeConfiguration;
final DateTime? lastBackup;
final DateTime? nextScheduledBackup;
final int? totalBackups;
final int? totalSizeBytes;
final String? totalSizeFormatted;
const BackupConfigModel({
this.autoBackupEnabled,
this.frequency,
this.retention,
this.retentionDays,
this.backupTime,
this.includeDatabase,
this.includeFiles,
this.includeConfiguration,
this.lastBackup,
this.nextScheduledBackup,
this.totalBackups,
this.totalSizeBytes,
this.totalSizeFormatted,
});
factory BackupConfigModel.fromJson(Map<String, dynamic> json) =>
_$BackupConfigModelFromJson(json);
Map<String, dynamic> toJson() => _$BackupConfigModelToJson(this);
@override
List<Object?> get props => [
autoBackupEnabled,
frequency,
retention,
retentionDays,
backupTime,
includeDatabase,
includeFiles,
includeConfiguration,
lastBackup,
nextScheduledBackup,
totalBackups,
totalSizeBytes,
totalSizeFormatted,
];
}

View File

@@ -0,0 +1,69 @@
/// Modèle de sauvegarde
/// Correspond à BackupResponse du backend
library backup_model;
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'backup_model.g.dart';
@JsonSerializable(explicitToJson: true)
class BackupModel extends Equatable {
final String? id;
final String? name;
final String? description;
final String? type; // AUTO, MANUAL, RESTORE_POINT
final int? sizeBytes;
final String? sizeFormatted;
final String? status; // PENDING, IN_PROGRESS, COMPLETED, FAILED
final DateTime? createdAt;
final DateTime? completedAt;
final String? createdBy;
final bool? includesDatabase;
final bool? includesFiles;
final bool? includesConfiguration;
final String? filePath;
final String? errorMessage;
const BackupModel({
this.id,
this.name,
this.description,
this.type,
this.sizeBytes,
this.sizeFormatted,
this.status,
this.createdAt,
this.completedAt,
this.createdBy,
this.includesDatabase,
this.includesFiles,
this.includesConfiguration,
this.filePath,
this.errorMessage,
});
factory BackupModel.fromJson(Map<String, dynamic> json) =>
_$BackupModelFromJson(json);
Map<String, dynamic> toJson() => _$BackupModelToJson(this);
@override
List<Object?> get props => [
id,
name,
description,
type,
sizeBytes,
sizeFormatted,
status,
createdAt,
completedAt,
createdBy,
includesDatabase,
includesFiles,
includesConfiguration,
filePath,
errorMessage,
];
}

View File

@@ -0,0 +1,131 @@
/// Repository pour la gestion des sauvegardes
/// Interface avec l'API backend BackupResource
library backup_repository;
import 'package:injectable/injectable.dart';
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
import '../models/backup_model.dart';
import '../models/backup_config_model.dart';
abstract class BackupRepository {
Future<List<BackupModel>> getAll();
Future<BackupModel> getById(String id);
Future<BackupModel> create(String name, {String? description});
Future<void> restore(String backupId, {bool createRestorePoint = true});
Future<void> delete(String id);
Future<BackupConfigModel> getConfig();
Future<BackupConfigModel> updateConfig(Map<String, dynamic> config);
Future<BackupModel> createRestorePoint();
}
@LazySingleton(as: BackupRepository)
class BackupRepositoryImpl implements BackupRepository {
final ApiClient _apiClient;
static const String _base = '/api/backups';
BackupRepositoryImpl(this._apiClient);
List<BackupModel> _parseListResponse(dynamic data) {
if (data is List) {
return data
.map((e) => BackupModel.fromJson(e as Map<String, dynamic>))
.toList();
}
if (data is Map && data.containsKey('content')) {
final content = data['content'] as List<dynamic>? ?? [];
return content
.map((e) => BackupModel.fromJson(e as Map<String, dynamic>))
.toList();
}
return [];
}
@override
Future<List<BackupModel>> getAll() async {
final response = await _apiClient.get(_base);
if (response.statusCode == 200) {
return _parseListResponse(response.data);
}
throw Exception('Erreur ${response.statusCode}');
}
@override
Future<BackupModel> getById(String id) async {
final response = await _apiClient.get('$_base/$id');
if (response.statusCode == 200) {
return BackupModel.fromJson(response.data as Map<String, dynamic>);
}
throw Exception('Erreur ${response.statusCode}');
}
@override
Future<BackupModel> create(String name, {String? description}) async {
final response = await _apiClient.post(
_base,
data: {
'name': name,
'description': description,
'type': 'MANUAL',
'includeDatabase': true,
'includeFiles': true,
'includeConfiguration': true,
},
);
if (response.statusCode == 201 || response.statusCode == 200) {
return BackupModel.fromJson(response.data as Map<String, dynamic>);
}
throw Exception('Erreur ${response.statusCode}');
}
@override
Future<void> restore(String backupId, {bool createRestorePoint = true}) async {
final response = await _apiClient.post(
'$_base/restore',
data: {
'backupId': backupId,
'restoreDatabase': true,
'restoreFiles': true,
'restoreConfiguration': true,
'createRestorePoint': createRestorePoint,
},
);
if (response.statusCode != 200) {
throw Exception('Erreur ${response.statusCode}');
}
}
@override
Future<void> delete(String id) async {
final response = await _apiClient.delete('$_base/$id');
if (response.statusCode != 200) {
throw Exception('Erreur ${response.statusCode}');
}
}
@override
Future<BackupConfigModel> getConfig() async {
final response = await _apiClient.get('$_base/config');
if (response.statusCode == 200) {
return BackupConfigModel.fromJson(response.data as Map<String, dynamic>);
}
throw Exception('Erreur ${response.statusCode}');
}
@override
Future<BackupConfigModel> updateConfig(Map<String, dynamic> config) async {
final response = await _apiClient.put('$_base/config', data: config);
if (response.statusCode == 200) {
return BackupConfigModel.fromJson(response.data as Map<String, dynamic>);
}
throw Exception('Erreur ${response.statusCode}');
}
@override
Future<BackupModel> createRestorePoint() async {
final response = await _apiClient.post('$_base/restore-point');
if (response.statusCode == 201 || response.statusCode == 200) {
return BackupModel.fromJson(response.data as Map<String, dynamic>);
}
throw Exception('Erreur ${response.statusCode}');
}
}