497 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/theme/app_theme.dart';
import '../../models/announcement.dart';
import '../../providers/announcement_provider.dart';
class AnnouncementsScreen extends ConsumerWidget {
const AnnouncementsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final activeFilter = ref.watch(announcementTypeFilterProvider);
final announcementsAsync = ref.watch(announcementsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Notice Board'),
),
body: Column(
children: [
// Filter Chips Bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildFilterChip(
context: context,
ref: ref,
label: 'All',
filterValue: null,
isSelected: activeFilter == null,
),
const SizedBox(width: 8),
_buildFilterChip(
context: context,
ref: ref,
label: 'Notices',
filterValue: 'notice',
isSelected: activeFilter == 'notice',
),
const SizedBox(width: 8),
_buildFilterChip(
context: context,
ref: ref,
label: 'Circulars',
filterValue: 'circular',
isSelected: activeFilter == 'circular',
),
const SizedBox(width: 8),
_buildFilterChip(
context: context,
ref: ref,
label: 'Events',
filterValue: 'event',
isSelected: activeFilter == 'event',
),
],
),
),
),
const Divider(height: 1),
// Main Content
Expanded(
child: RefreshIndicator(
onRefresh: () async {
ref.invalidate(announcementsProvider);
await ref.read(announcementsProvider.future);
},
child: announcementsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline_rounded, size: 48, color: AppTheme.errorColor),
const SizedBox(height: 12),
const Text(
'Failed to load announcements',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
err.toString(),
textAlign: TextAlign.center,
style: const TextStyle(color: AppTheme.textSecondary, fontSize: 13),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.refresh(announcementsProvider),
child: const Text('Retry'),
),
],
),
),
),
data: (pageResult) {
final announcements = pageResult.announcements;
if (announcements.isEmpty) {
return ListView(
children: const [
SizedBox(height: 100),
Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.campaign_outlined, size: 64, color: AppTheme.textMuted),
SizedBox(height: 16),
Text(
'No announcements found',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
SizedBox(height: 8),
Text(
'There are no active notices or circulars matching your criteria at this time.',
textAlign: TextAlign.center,
style: TextStyle(color: AppTheme.textSecondary, fontSize: 13),
),
],
),
),
),
],
);
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: announcements.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final ann = announcements[index];
return _buildAnnouncementCard(context, ann);
},
);
},
),
),
),
],
),
);
}
Widget _buildFilterChip({
required BuildContext context,
required WidgetRef ref,
required String label,
required String? filterValue,
required bool isSelected,
}) {
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: (_) {
ref.read(announcementTypeFilterProvider.notifier).state = filterValue;
},
selectedColor: AppTheme.primaryColor.withOpacity(0.15),
checkmarkColor: AppTheme.primaryColor,
labelStyle: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected ? AppTheme.primaryColor : AppTheme.textSecondary,
),
);
}
Widget _buildAnnouncementCard(BuildContext context, AnnouncementItem ann) {
return Card(
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _showAnnouncementDetail(context, ann),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top Badges & Date
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildTypeBadge(ann),
if (ann.isUrgent || ann.isImportant) ...[
const SizedBox(width: 6),
_buildPriorityBadge(ann),
],
const Spacer(),
if (ann.date.isNotEmpty)
Text(
ann.date,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(height: 10),
// Title
Text(
ann.title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 6),
// Summary
if (ann.summary.isNotEmpty)
Text(
ann.summary,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
height: 1.3,
),
),
// Footer: Author & Attachment
if (ann.author != null || ann.hasAttachment) ...[
const SizedBox(height: 10),
Row(
children: [
if (ann.author != null)
Row(
children: [
const Icon(Icons.person_outline, size: 14, color: AppTheme.textMuted),
const SizedBox(width: 4),
Text(
ann.author!,
style: const TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
],
),
const Spacer(),
if (ann.hasAttachment)
Row(
children: [
const Icon(Icons.attach_file, size: 14, color: AppTheme.primaryColor),
const SizedBox(width: 2),
Text(
ann.circularFilename ?? 'Attachment',
style: const TextStyle(
fontSize: 11,
color: AppTheme.primaryColor,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
],
],
),
),
),
);
}
Widget _buildTypeBadge(AnnouncementItem ann) {
Color bg;
Color fg;
switch (ann.type) {
case 'circular':
bg = Colors.deepPurple.shade50;
fg = Colors.deepPurple.shade700;
break;
case 'event':
bg = Colors.teal.shade50;
fg = Colors.teal.shade700;
break;
case 'notice':
default:
bg = Colors.blue.shade50;
fg = Colors.blue.shade700;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(6),
),
child: Text(
ann.typeLabel,
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: fg),
),
);
}
Widget _buildPriorityBadge(AnnouncementItem ann) {
Color bg;
Color fg;
if (ann.isUrgent) {
bg = Colors.red.shade50;
fg = Colors.red.shade700;
} else {
bg = Colors.amber.shade50;
fg = Colors.amber.shade900;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(6),
),
child: Text(
ann.priorityLabel,
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: fg),
),
);
}
void _showAnnouncementDetail(BuildContext context, AnnouncementItem ann) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.7,
minChildSize: 0.4,
maxChildSize: 0.95,
builder: (context, scrollController) {
return SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
),
// Header Badges
Row(
children: [
_buildTypeBadge(ann),
if (ann.isUrgent || ann.isImportant) ...[
const SizedBox(width: 8),
_buildPriorityBadge(ann),
],
const Spacer(),
if (ann.date.isNotEmpty)
Text(
'Published: ${ann.date}',
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
],
),
const SizedBox(height: 16),
// Title
Text(
ann.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 12),
// Expiry Date & Author
if (ann.expiryDate != null || ann.author != null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (ann.author != null)
Row(
children: [
const Icon(Icons.person_outline, size: 16, color: AppTheme.textSecondary),
const SizedBox(width: 6),
Text(
'Posted by: ${ann.author}',
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
],
),
if (ann.expiryDate != null) ...[
if (ann.author != null) const SizedBox(height: 6),
Row(
children: [
const Icon(Icons.event_busy, size: 16, color: AppTheme.textSecondary),
const SizedBox(width: 6),
Text(
'Expires on: ${ann.expiryDate}',
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
],
),
],
],
),
),
const SizedBox(height: 16),
// Content
const Text(
'Details',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
ann.summary.isNotEmpty ? ann.summary : ann.title,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textPrimary,
height: 1.5,
),
),
// Attachment info
if (ann.hasAttachment) ...[
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: AppTheme.primaryColor.withOpacity(0.3)),
borderRadius: BorderRadius.circular(8),
color: AppTheme.primaryColor.withOpacity(0.05),
),
child: Row(
children: [
const Icon(Icons.description_outlined, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Expanded(
child: Text(
ann.circularFilename ?? 'Attached Document',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppTheme.primaryColor,
),
),
),
],
),
),
],
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
),
],
),
);
},
);
},
);
}
}