Files
swift_companion/swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart
2026-09-29 15:07:05 +02:00

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,
),
),
);
}),
],
),
),
);
}
}