Files
lis/mylis/lib/screens/dashboard/sample_groups_screen.dart
T
2026-08-02 10:46:34 +07:00

422 lines
13 KiB
Dart

part of '../../app/app.dart';
class _DashboardSubHeader extends StatelessWidget {
const _DashboardSubHeader({
required this.title,
required this.subtitle,
required this.badgeText,
required this.user,
required this.notificationCount,
required this.onNotifications,
required this.onProfile,
});
final String title;
final String subtitle;
final String badgeText;
final Map<String, dynamic> user;
final int notificationCount;
final VoidCallback onNotifications;
final VoidCallback onProfile;
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_new_rounded),
color: const Color(0xFF080D3D),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(
color: const Color(0xFF080D3D),
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(width: 8),
BadgeLabel(text: badgeText, color: const Color(0xFF0B7BFF)),
],
),
const SizedBox(height: 4),
Text(
subtitle,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: const Color(0xFF47517A),
fontWeight: FontWeight.w700,
),
),
],
),
),
Stack(
clipBehavior: Clip.none,
children: [
IconButton(
onPressed: onNotifications,
icon: const Icon(Icons.notifications_none_rounded, size: 30),
color: const Color(0xFF063B60),
tooltip: 'Notifikasi nilai kritis',
),
if (notificationCount > 0)
Positioned(
top: 6,
right: 6,
child: Container(
padding: const EdgeInsets.all(5),
constraints: const BoxConstraints(minWidth: 20),
decoration: const BoxDecoration(
color: Color(0xFFFF1D25),
shape: BoxShape.circle,
),
child: Text(
notificationCount > 99 ? '99+' : '$notificationCount',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w800,
),
),
),
),
],
),
const SizedBox(width: 10),
InkWell(
borderRadius: BorderRadius.circular(28),
onTap: onProfile,
child: CircleAvatar(
radius: 27,
backgroundColor: const Color(0xFF12BFA5),
child: Text(
_initials(user['nama']?.toString() ?? 'SP'),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
),
),
),
),
],
);
}
String _initials(String name) {
final parts = name
.trim()
.split(RegExp(r'\s+'))
.where((part) => part.isNotEmpty)
.toList();
if (parts.isEmpty) {
return 'SP';
}
return parts.take(2).map((part) => part[0].toUpperCase()).join();
}
}
class SampleGroupsScreen extends StatelessWidget {
const SampleGroupsScreen({
super.key,
required this.books,
required this.token,
required this.baseUrl,
required this.user,
required this.notificationCount,
});
final List<dynamic> books;
final String token;
final String baseUrl;
final Map<String, dynamic> user;
final int notificationCount;
@override
Widget build(BuildContext context) {
final total = books.fold<int>(
0,
(sum, item) => sum + (asMap(item)['total'] as num? ?? 0).toInt(),
);
return Scaffold(
body: ListView(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 24),
children: [
_DashboardSubHeader(
title: 'Detail Sampel',
subtitle: 'Catatan & Register',
badgeText: '$total',
user: user,
notificationCount: notificationCount,
onNotifications: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
CriticalValuesScreen(token: token, baseUrl: baseUrl),
),
);
},
onProfile: () => _showProfile(context, user),
),
const SizedBox(height: 28),
_SampleGroupsSummary(total: total, count: books.length),
const SizedBox(height: 18),
Text(
'Jenis Spesimen',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w900,
color: const Color(0xFF080D3D),
),
),
const SizedBox(height: 12),
if (books.isEmpty)
const EmptyPanel(text: 'Tidak ada kelompok pemeriksaan.')
else
...books.map(
(book) => _SampleGroupCard(
book: asMap(book),
onTap: () {
final mapped = asMap(book);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => SpecimenRegisterScreen(
token: token,
baseUrl: baseUrl,
master: mapped['master']?.toString() ?? 'buku0',
title: mapped['label']?.toString() ?? 'Spesimen',
total: (mapped['total'] as num? ?? 0).toInt(),
user: user,
notificationCount: notificationCount,
),
),
);
},
),
),
],
),
);
}
void _showProfile(BuildContext context, Map<String, dynamic> user) {
showModalBottomSheet<void>(
context: context,
showDragHandle: true,
builder: (context) => SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
CircleAvatar(
radius: 28,
backgroundColor: const Color(0xFF0F766E),
child: Text(
_initials(user['nama']?.toString() ?? 'SP'),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user['nama']?.toString() ?? '-',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w900),
),
const SizedBox(height: 2),
Text(
'${user['username'] ?? '-'} - ${user['previlage'] ?? '-'}',
style: const TextStyle(color: Colors.black54),
),
],
),
),
],
),
const SizedBox(height: 18),
FilledButton.icon(
onPressed: () async {
await SessionStore.clear();
if (!context.mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginScreen()),
(_) => false,
);
},
icon: const Icon(Icons.logout),
label: const Text('Logout'),
),
],
),
),
),
);
}
String _initials(String name) {
final parts = name
.trim()
.split(RegExp(r'\s+'))
.where((part) => part.isNotEmpty)
.toList();
if (parts.isEmpty) {
return 'SP';
}
return parts.take(2).map((part) => part[0].toUpperCase()).join();
}
}
class _SampleGroupsSummary extends StatelessWidget {
const _SampleGroupsSummary({required this.total, required this.count});
final int total;
final int count;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFFE1E8F5)),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF2563EB).withValues(alpha: 0.05),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Row(
children: [
Container(
width: 58,
height: 58,
decoration: BoxDecoration(
color: const Color(0xFFFF6B00).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.menu_book_rounded,
color: Color(0xFFFF6B00),
size: 32,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$total sampel aktif',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: const Color(0xFF080D3D),
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 4),
Text(
'$count jenis spesimen tersedia',
style: const TextStyle(
color: Color(0xFF47517A),
fontWeight: FontWeight.w700,
),
),
],
),
),
],
),
);
}
}
class _SampleGroupCard extends StatelessWidget {
const _SampleGroupCard({required this.book, required this.onTap});
final Map<String, dynamic> book;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final total = (book['total'] as num? ?? 0).toInt();
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: const BorderSide(color: Color(0xFFE1E8F5)),
),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 54,
height: 54,
decoration: BoxDecoration(
color: const Color(0xFFFF6B00).withValues(alpha: 0.10),
shape: BoxShape.circle,
),
child: const Icon(
Icons.science_outlined,
color: Color(0xFFFF6B00),
size: 28,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
book['label']?.toString() ?? 'Spesimen',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: const Color(0xFF080D3D),
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 6),
BadgeLabel(
text: '${book['master'] ?? '-'}',
color: const Color(0xFFFF6B00),
),
],
),
),
BadgeLabel(text: '$total sampel', color: const Color(0xFF0B7BFF)),
const SizedBox(width: 8),
const Icon(Icons.chevron_right_rounded, color: Color(0xFF667095)),
],
),
),
),
);
}
}