Files
lis/mylis/lib/screens/dashboard/dashboard_screen.dart
T
2026-07-22 10:01:55 +07:00

344 lines
9.7 KiB
Dart

part of '../../app/app.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({
super.key,
required this.token,
required this.baseUrl,
});
final String token;
final String baseUrl;
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
late final ApiClient _api;
late Future<Map<String, dynamic>> _future;
@override
void initState() {
super.initState();
_api = ApiClient(baseUrl: widget.baseUrl, token: widget.token);
_future = _api.get('api/mobile/dashboard');
}
void _reload() {
setState(() {
_future = _api.get('api/mobile/dashboard');
});
}
Future<void> _logout() async {
await SessionStore.clear();
if (!mounted) {
return;
}
Navigator.of(
context,
).pushReplacement(MaterialPageRoute(builder: (_) => const LoginScreen()));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(kAppShortName),
actions: [
IconButton(
onPressed: _reload,
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
),
IconButton(
onPressed: _logout,
icon: const Icon(Icons.logout),
tooltip: 'Logout',
),
],
),
body: RefreshIndicator(
onRefresh: () async => _reload(),
child: FutureBuilder<Map<String, dynamic>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const LoadingScreen();
}
if (snapshot.hasError) {
return ErrorView(
message: _message(snapshot.error),
onRetry: _reload,
);
}
final data = snapshot.data!;
final user = asMap(data['user']);
final summary = asMap(data['summary']);
final warnings = asList(data['early_warning_groups']);
final books = asList(data['books']);
return ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
children: [
HeaderPanel(user: user, summary: summary),
const SizedBox(height: 16),
SectionTitle(
title: 'Early Warning Sistem',
actionLabel:
'${warnings.fold<int>(0, (total, item) => total + (asMap(item)['total'] as num? ?? 0).toInt())} kasus',
),
if (warnings.isEmpty)
const EmptyPanel(text: 'Tidak ada data Early Warning.')
else
...warnings.map(
(group) => EarlyWarningGroupCard(
group: asMap(group),
token: widget.token,
baseUrl: widget.baseUrl,
),
),
const SizedBox(height: 18),
const SectionTitle(title: 'List Pemeriksaan'),
...books.map(
(book) => BookTile(
book: asMap(book),
token: widget.token,
baseUrl: widget.baseUrl,
),
),
],
);
},
),
),
);
}
}
class HeaderPanel extends StatelessWidget {
const HeaderPanel({super.key, required this.user, required this.summary});
final Map<String, dynamic> user;
final Map<String, dynamic> summary;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0F766E),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 54,
height: 54,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Image.asset(kHospitalLogoAsset, fit: BoxFit.contain),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kAppShortName,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
const Text(
kHospitalName,
style: TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
],
),
const SizedBox(height: 14),
Text(
user['nama']?.toString() ?? '-',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
Text(
user['previlage']?.toString() ?? '-',
style: const TextStyle(color: Colors.white70),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: SummaryItem(label: 'PPDS', value: summary['total_ppds']),
),
Expanded(
child: SummaryItem(
label: 'Verifikasi',
value: summary['butuh_verifikasi'],
),
),
Expanded(
child: SummaryItem(
label: 'Hari Ini',
value: summary['antrian_hari_ini'],
),
),
],
),
],
),
);
}
}
class SummaryItem extends StatelessWidget {
const SummaryItem({super.key, required this.label, required this.value});
final String label;
final Object? value;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${value ?? 0}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
Text(
label,
style: const TextStyle(color: Colors.white70, fontSize: 12),
),
],
);
}
}
class SectionTitle extends StatelessWidget {
const SectionTitle({super.key, required this.title, this.actionLabel});
final String title;
final String? actionLabel;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Expanded(
child: Text(
title,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800),
),
),
if (actionLabel != null)
BadgeLabel(text: actionLabel!, color: const Color(0xFFDC2626)),
],
),
);
}
}
class EarlyWarningGroupCard extends StatelessWidget {
const EarlyWarningGroupCard({
super.key,
required this.group,
required this.token,
required this.baseUrl,
});
final Map<String, dynamic> group;
final String token;
final String baseUrl;
@override
Widget build(BuildContext context) {
final items = asList(group['items']);
return Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: ExpansionTile(
leading: const Icon(
Icons.warning_amber_rounded,
color: Color(0xFFDC2626),
),
title: Text(
group['subpoli']?.toString() ?? 'Tanpa Subpoli',
style: const TextStyle(fontWeight: FontWeight.w700),
),
subtitle: Text('${group['total'] ?? items.length} kasus warning'),
children: items
.map(
(item) => ExaminationCompactTile(
item: asMap(item),
token: token,
baseUrl: baseUrl,
),
)
.toList(),
),
);
}
}
class BookTile extends StatelessWidget {
const BookTile({
super.key,
required this.book,
required this.token,
required this.baseUrl,
});
final Map<String, dynamic> book;
final String token;
final String baseUrl;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: ListTile(
leading: const CircleAvatar(
backgroundColor: Color(0xFFE0F2F1),
child: Icon(Icons.science_outlined, color: Color(0xFF0F766E)),
),
title: Text(book['label']?.toString() ?? '-'),
subtitle: Text('${book['total'] ?? 0} pemeriksaan aktif'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ExaminationListScreen(
token: token,
baseUrl: baseUrl,
master: book['master'].toString(),
title: book['label']?.toString() ?? 'Pemeriksaan',
),
),
);
},
),
);
}
}