250 lines
7.4 KiB
Dart
250 lines
7.4 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../config/intra_config.dart';
|
|
import 'intra_api_exception.dart';
|
|
import 'intra_auth_service.dart';
|
|
import 'intra_http.dart';
|
|
|
|
/// Compétence Intra : niveau entier + pourcentage (partie décimale du level API).
|
|
class IntraSkillSummary {
|
|
const IntraSkillSummary({
|
|
required this.name,
|
|
required this.level,
|
|
required this.percentage,
|
|
});
|
|
|
|
final String name;
|
|
final int level;
|
|
final int percentage;
|
|
}
|
|
|
|
/// Projet terminé ou en échec.
|
|
class IntraProjectSummary {
|
|
const IntraProjectSummary({
|
|
required this.name,
|
|
required this.status,
|
|
this.finalMark,
|
|
});
|
|
|
|
final String name;
|
|
final String status;
|
|
final int? finalMark;
|
|
}
|
|
|
|
/// Données agrégées pour la vue profil.
|
|
class IntraUserProfile {
|
|
const IntraUserProfile({
|
|
required this.user,
|
|
required this.cursusLevel,
|
|
required this.skills,
|
|
required this.projects,
|
|
});
|
|
|
|
final Map<String, dynamic> user;
|
|
final double? cursusLevel;
|
|
final List<IntraSkillSummary> skills;
|
|
final List<IntraProjectSummary> projects;
|
|
}
|
|
|
|
class _ProfileCacheEntry {
|
|
_ProfileCacheEntry(this.profile, this.fetchedAt);
|
|
|
|
final IntraUserProfile profile;
|
|
final DateTime fetchedAt;
|
|
}
|
|
|
|
/// Agrège fiche user, skills (cursus, pas `/experiences`) et `projects_users` (scope `projects`).
|
|
class IntraProfileService {
|
|
IntraProfileService._();
|
|
static final IntraProfileService instance = IntraProfileService._();
|
|
|
|
/// Cache court + délai entre GET pour limiter les 429 sur l'API 42.
|
|
static const Duration _cacheTtl = Duration(minutes: 2);
|
|
static const Duration _betweenRequests = Duration(milliseconds: 450);
|
|
|
|
final IntraAuthService _auth = IntraAuthService.instance;
|
|
final Map<String, _ProfileCacheEntry> _cache = {};
|
|
|
|
/// Charge profil complet (user, cursus, skills, projets) avec le token existant.
|
|
Future<IntraUserProfile> fetchProfile(Map<String, dynamic> summaryUser) async {
|
|
if (!_auth.isAuthenticated) {
|
|
throw const IntraNotAuthenticatedException();
|
|
}
|
|
|
|
final userId = summaryUser['id'];
|
|
if (userId == null) {
|
|
throw const IntraApiErrorException('Identifiant utilisateur manquant');
|
|
}
|
|
final id = userId.toString();
|
|
|
|
final cached = _cache[id];
|
|
if (cached != null &&
|
|
DateTime.now().difference(cached.fetchedAt) < _cacheTtl) {
|
|
return cached.profile;
|
|
}
|
|
|
|
try {
|
|
final login = summaryUser['login']?.toString();
|
|
final userPath = login != null && login.isNotEmpty
|
|
? '/users/$login'
|
|
: '/users/$id';
|
|
|
|
final userDecoded = await _getJson(userPath);
|
|
if (userDecoded is! Map<String, dynamic>) {
|
|
throw IntraApiErrorException('Réponse API inattendue ($userPath)');
|
|
}
|
|
|
|
final embeddedCursus = userDecoded['cursus_users'];
|
|
final List<dynamic> cursusList;
|
|
if (embeddedCursus is List && embeddedCursus.isNotEmpty) {
|
|
cursusList = embeddedCursus;
|
|
} else {
|
|
await Future<void>.delayed(_betweenRequests);
|
|
cursusList = await _getJsonList('/users/$id/cursus_users');
|
|
}
|
|
|
|
await Future<void>.delayed(_betweenRequests);
|
|
final projectsUsers = await _getJsonList(
|
|
'/users/$id/projects_users',
|
|
query: {'page[size]': '100'},
|
|
);
|
|
final primaryCursus = _pickPrimaryCursus(cursusList);
|
|
|
|
final profile = IntraUserProfile(
|
|
user: userDecoded,
|
|
cursusLevel: _levelFromCursus(primaryCursus),
|
|
skills: _parseSkillsFromCursus(primaryCursus, cursusList),
|
|
projects: _parseProjects(projectsUsers),
|
|
);
|
|
|
|
_cache[id] = _ProfileCacheEntry(profile, DateTime.now());
|
|
return profile;
|
|
} on IntraApiException {
|
|
rethrow;
|
|
} on SocketException {
|
|
throw const IntraNetworkException();
|
|
} on http.ClientException {
|
|
throw const IntraNetworkException();
|
|
}
|
|
}
|
|
|
|
Future<dynamic> _getJson(String path, {Map<String, String>? query}) async {
|
|
final uri = Uri.parse('${IntraConfig.apiBaseUrl}$path').replace(
|
|
queryParameters: query,
|
|
);
|
|
final response = await intraApiGet(uri, auth: _auth);
|
|
return decodeIntraBody(response);
|
|
}
|
|
|
|
Future<List<dynamic>> _getJsonList(
|
|
String path, {
|
|
Map<String, String>? query,
|
|
}) async {
|
|
final decoded = await _getJson(path, query: query);
|
|
if (decoded is! List) {
|
|
throw IntraApiErrorException('Réponse API inattendue ($path)');
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
Map<String, dynamic>? _pickPrimaryCursus(List<dynamic> cursusUsers) {
|
|
Map<String, dynamic>? fallback;
|
|
for (final entry in cursusUsers) {
|
|
if (entry is! Map<String, dynamic>) continue;
|
|
final cursus = entry['cursus'];
|
|
final slug = cursus is Map ? cursus['slug']?.toString() : null;
|
|
final name = cursus is Map ? cursus['name']?.toString() : null;
|
|
if (slug == '42' || name == '42' || name == '42cursus') {
|
|
return entry;
|
|
}
|
|
fallback ??= entry;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
double? _levelFromCursus(Map<String, dynamic>? cursusUser) {
|
|
if (cursusUser == null) return null;
|
|
final level = cursusUser['level'];
|
|
return level is num ? level.toDouble() : null;
|
|
}
|
|
|
|
List<IntraSkillSummary> _parseSkillsFromCursus(
|
|
Map<String, dynamic>? primaryCursus,
|
|
List<dynamic> allCursus,
|
|
) {
|
|
final sources = <Map<String, dynamic>>[];
|
|
if (primaryCursus != null) {
|
|
sources.add(primaryCursus);
|
|
} else {
|
|
for (final c in allCursus) {
|
|
if (c is Map<String, dynamic>) sources.add(c);
|
|
}
|
|
}
|
|
|
|
final byId = <int, IntraSkillSummary>{};
|
|
for (final cursusUser in sources) {
|
|
final skills = cursusUser['skills'];
|
|
if (skills is! List) continue;
|
|
for (final skill in skills) {
|
|
if (skill is! Map<String, dynamic>) continue;
|
|
final idRaw = skill['id'];
|
|
if (idRaw is! num) continue;
|
|
final skillId = idRaw.toInt();
|
|
final name = skill['name']?.toString() ?? 'Skill $skillId';
|
|
final levelRaw = skill['level'];
|
|
if (levelRaw is! num) continue;
|
|
final summary = _skillFromLevel(name, levelRaw.toDouble());
|
|
final existing = byId[skillId];
|
|
if (existing == null || summary.level > existing.level) {
|
|
byId[skillId] = summary;
|
|
}
|
|
}
|
|
}
|
|
|
|
final list = byId.values.toList();
|
|
list.sort((a, b) => b.level.compareTo(a.level));
|
|
return list;
|
|
}
|
|
|
|
IntraSkillSummary _skillFromLevel(String name, double levelValue) {
|
|
final whole = levelValue.floor();
|
|
final fraction = levelValue - whole;
|
|
final percentage = (fraction * 100).round().clamp(0, 99);
|
|
return IntraSkillSummary(
|
|
name: name,
|
|
level: whole,
|
|
percentage: percentage,
|
|
);
|
|
}
|
|
|
|
List<IntraProjectSummary> _parseProjects(List<dynamic> projectsUsers) {
|
|
final projects = <IntraProjectSummary>[];
|
|
|
|
for (final entry in projectsUsers) {
|
|
if (entry is! Map<String, dynamic>) continue;
|
|
final status = entry['status']?.toString();
|
|
if (status != 'finished' && status != 'failed') continue;
|
|
|
|
final project = entry['project'];
|
|
final name = project is Map
|
|
? (project['name']?.toString() ?? project['slug']?.toString())
|
|
: null;
|
|
if (name == null || name.isEmpty) continue;
|
|
|
|
final mark = entry['final_mark'];
|
|
projects.add(
|
|
IntraProjectSummary(
|
|
name: name,
|
|
status: status!,
|
|
finalMark: mark is int ? mark : (mark is num ? mark.round() : null),
|
|
),
|
|
);
|
|
}
|
|
|
|
projects.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
|
return projects;
|
|
}
|
|
}
|