380 lines
11 KiB
Dart
380 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../services/intra_api_exception.dart';
|
|
import '../services/intra_profile_service.dart';
|
|
|
|
/// Vue 2 — profil complet (étape 4).
|
|
class ProfileScreen extends StatefulWidget {
|
|
const ProfileScreen({super.key, required this.user});
|
|
|
|
/// Résultat minimal de la recherche (au moins `id` + `login`).
|
|
final Map<String, dynamic> user;
|
|
|
|
@override
|
|
State<ProfileScreen> createState() => _ProfileScreenState();
|
|
}
|
|
|
|
class _ProfileScreenState extends State<ProfileScreen> {
|
|
final _profiles = IntraProfileService.instance;
|
|
|
|
bool _loading = true;
|
|
String? _error;
|
|
IntraUserProfile? _profile;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
setState(() {
|
|
_loading = true;
|
|
_error = null;
|
|
});
|
|
|
|
try {
|
|
final profile = await _profiles.fetchProfile(widget.user);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_profile = profile;
|
|
_loading = false;
|
|
});
|
|
} on IntraApiException catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_error = e.message;
|
|
_loading = false;
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_error = e.toString();
|
|
_loading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
String? _imageUrl(Map<String, dynamic> user) {
|
|
final image = user['image'];
|
|
if (image is Map<String, dynamic>) {
|
|
final link = image['link'];
|
|
if (link is String && link.isNotEmpty) return link;
|
|
}
|
|
final url = user['image_url'];
|
|
if (url is String && url.isNotEmpty) return url;
|
|
return null;
|
|
}
|
|
|
|
String _display(dynamic value, {String fallback = '—'}) {
|
|
if (value == null) return fallback;
|
|
final text = value.toString().trim();
|
|
return text.isEmpty ? fallback : text;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final user = _profile?.user ?? widget.user;
|
|
final login = _display(user['login'], fallback: '?');
|
|
final imageUrl = _imageUrl(user);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(login),
|
|
backgroundColor: const Color(0xFF00BABC),
|
|
foregroundColor: Colors.black,
|
|
),
|
|
body: _loading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _error != null
|
|
? _ErrorBody(message: _error!, onRetry: _load)
|
|
: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final wide = constraints.maxWidth >= 720;
|
|
final profile = _profile!;
|
|
final header = _ProfileHeader(
|
|
login: login,
|
|
imageUrl: imageUrl,
|
|
);
|
|
final details = _DetailsCard(
|
|
user: profile.user,
|
|
cursusLevel: profile.cursusLevel,
|
|
display: _display,
|
|
);
|
|
final skills = _SkillsCard(skills: profile.skills);
|
|
final projects = _ProjectsCard(projects: profile.projects);
|
|
|
|
if (wide) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 2,
|
|
child: Column(
|
|
children: [
|
|
header,
|
|
const SizedBox(height: 16),
|
|
details,
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 24),
|
|
Expanded(
|
|
flex: 3,
|
|
child: Column(
|
|
children: [
|
|
skills,
|
|
const SizedBox(height: 16),
|
|
projects,
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
header,
|
|
const SizedBox(height: 16),
|
|
details,
|
|
const SizedBox(height: 16),
|
|
skills,
|
|
const SizedBox(height: 16),
|
|
projects,
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ErrorBody extends StatelessWidget {
|
|
const _ErrorBody({required this.message, required this.onRetry});
|
|
|
|
final String message;
|
|
final VoidCallback onRetry;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
message,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
|
),
|
|
const SizedBox(height: 16),
|
|
FilledButton(onPressed: onRetry, child: const Text('Réessayer')),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ProfileHeader extends StatelessWidget {
|
|
const _ProfileHeader({required this.login, required this.imageUrl});
|
|
|
|
final String login;
|
|
final String? imageUrl;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 56,
|
|
backgroundImage:
|
|
imageUrl != null ? NetworkImage(imageUrl!) : null,
|
|
child: imageUrl == null
|
|
? Text(
|
|
login.isNotEmpty ? login[0].toUpperCase() : '?',
|
|
style: const TextStyle(fontSize: 32),
|
|
)
|
|
: null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
login,
|
|
textAlign: TextAlign.center,
|
|
style: Theme.of(context).textTheme.headlineSmall,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DetailsCard extends StatelessWidget {
|
|
const _DetailsCard({
|
|
required this.user,
|
|
required this.cursusLevel,
|
|
required this.display,
|
|
});
|
|
|
|
final Map<String, dynamic> user;
|
|
final double? cursusLevel;
|
|
final String Function(dynamic value, {String fallback}) display;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final levelText = cursusLevel != null
|
|
? cursusLevel!.toStringAsFixed(2)
|
|
: display(user['level']);
|
|
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Informations',
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_DetailRow(label: 'Email', value: display(user['email'])),
|
|
_DetailRow(label: 'Téléphone', value: display(user['phone'])),
|
|
_DetailRow(label: 'Niveau cursus', value: levelText),
|
|
_DetailRow(
|
|
label: 'Localisation',
|
|
value: display(user['location']),
|
|
),
|
|
_DetailRow(
|
|
label: 'Wallet',
|
|
value: display(user['wallet']),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DetailRow extends StatelessWidget {
|
|
const _DetailRow({required this.label, required this.value});
|
|
|
|
final String label;
|
|
final String value;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
label,
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
Expanded(child: Text(value)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SkillsCard extends StatelessWidget {
|
|
const _SkillsCard({required this.skills});
|
|
|
|
final List<IntraSkillSummary> skills;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Skills', style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: 12),
|
|
if (skills.isEmpty)
|
|
const Text('Aucune compétence disponible.')
|
|
else
|
|
...skills.map(
|
|
(s) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('${s.name} — niveau ${s.level} (${s.percentage} %)'),
|
|
const SizedBox(height: 4),
|
|
LinearProgressIndicator(
|
|
value: (s.level + s.percentage / 100).clamp(0, 15) / 15,
|
|
minHeight: 6,
|
|
backgroundColor: Colors.grey.shade300,
|
|
color: const Color(0xFF00BABC),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ProjectsCard extends StatelessWidget {
|
|
const _ProjectsCard({required this.projects});
|
|
|
|
final List<IntraProjectSummary> projects;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Projets (validés et failed)',
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (projects.isEmpty)
|
|
const Text('Aucun projet terminé ou en échec.')
|
|
else
|
|
...projects.map((p) {
|
|
final statusLabel =
|
|
p.status == 'failed' ? 'Échec' : 'Validé';
|
|
final mark = p.finalMark != null ? ' · ${p.finalMark}' : '';
|
|
return ListTile(
|
|
dense: true,
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(p.name),
|
|
trailing: Text(
|
|
'$statusLabel$mark',
|
|
style: TextStyle(
|
|
color: p.status == 'failed'
|
|
? Theme.of(context).colorScheme.error
|
|
: Colors.green.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|