Files
bini-google-tv/flutter_app/lib/screens/admin/admin_screen.dart

1792 lines
66 KiB
Dart

import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../models/bible_verse.dart';
import '../../models/family_member.dart';
import '../../models/photo.dart';
import '../../models/announcement.dart';
import '../../models/schedule_item.dart';
import '../../services/bible_verse_service.dart';
import '../../services/family_service.dart';
import '../../services/photo_service.dart';
import '../../services/announcement_service.dart';
import '../../services/schedule_service.dart';
import '../../models/todo_item.dart';
import '../../services/todo_service.dart';
class AdminScreen extends StatefulWidget {
const AdminScreen({super.key});
@override
State<AdminScreen> createState() => _AdminScreenState();
}
class _AdminScreenState extends State<AdminScreen> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 6,
child: Scaffold(
appBar: AppBar(
title: const Text('Admin Settings'),
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Family Members'),
Tab(text: 'Photos'),
Tab(text: 'Bible Verses'),
Tab(text: 'Announcements'),
Tab(text: 'Schedules'),
Tab(text: 'Todos'),
],
),
),
body: const TabBarView(
children: [
FamilyManagerTab(),
PhotoManagerTab(),
BibleVerseManagerTab(),
AnnouncementManagerTab(),
ScheduleManagerTab(),
TodoManagerTab(),
],
),
),
);
}
}
class FamilyManagerTab extends StatefulWidget {
const FamilyManagerTab({super.key});
@override
State<FamilyManagerTab> createState() => _FamilyManagerTabState();
}
class _FamilyManagerTabState extends State<FamilyManagerTab> {
// Rainbow palette colors
final List<Color> _palette = const [
Color(0xFFFF0000), // Red
Color(0xFFFF7F00), // Orange
Color(0xFFFFFF00), // Yellow
Color(0xFF00FF00), // Green
Color(0xFF0000FF), // Blue
Color(0xFF4B0082), // Indigo
Color(0xFF9400D3), // Violet
];
String _colorToHex(Color color) {
return '#${color.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}';
}
Color _hexToColor(String hex) {
try {
String cleanHex = hex.replaceAll('#', '').replaceAll('0x', '');
if (cleanHex.length == 6) {
cleanHex = 'FF$cleanHex';
}
if (cleanHex.length == 8) {
return Color(int.parse(cleanHex, radix: 16));
}
return Colors.grey;
} catch (_) {
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddMemberDialog(context),
child: const Icon(Icons.add),
),
body: FutureBuilder<List<FamilyMember>>(
future: Provider.of<FamilyService>(context).fetchFamilyMembers(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
final members = snapshot.data!;
return ListView.builder(
itemCount: members.length,
itemBuilder: (context, index) {
final member = members[index];
Color memberColor = _hexToColor(member.color);
return ListTile(
leading: CircleAvatar(
backgroundColor: memberColor,
child: member.iconUrl.isNotEmpty
? ClipOval(
child: Image.network(
member.iconUrl,
width: 40,
height: 40,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Text(
member.name.isNotEmpty
? member.name[0].toUpperCase()
: '?',
style: const TextStyle(fontSize: 20),
),
),
)
: Text(
member.name.isNotEmpty
? member.name[0].toUpperCase()
: '?',
style: const TextStyle(fontSize: 20),
),
),
title: Text(member.name),
subtitle: Text('Order: ${member.order}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => _showEditMemberDialog(context, member),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
await Provider.of<FamilyService>(
context,
listen: false,
).deleteFamilyMember(member.id);
setState(() {});
},
),
],
),
);
},
);
},
),
);
}
void _showAddMemberDialog(BuildContext context) {
final nameController = TextEditingController();
final orderController = TextEditingController(text: '1');
Color selectedColor = _palette[0];
Uint8List? selectedIconBytes;
String? selectedIconName;
bool isUploading = false;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: const Text('Add Family Member'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Name'),
),
const SizedBox(height: 16),
const Text('Icon', style: TextStyle(color: Colors.grey)),
const SizedBox(height: 8),
Row(
children: [
CircleAvatar(
backgroundColor: selectedColor,
child: selectedIconBytes != null
? ClipOval(
child: Image.memory(
selectedIconBytes!,
width: 40,
height: 40,
fit: BoxFit.cover,
),
)
: const Icon(Icons.person, color: Colors.white),
),
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
withData: true,
type: FileType.image,
);
if (result != null && result.files.isNotEmpty) {
setDialogState(() {
selectedIconBytes = result.files.first.bytes;
selectedIconName = result.files.first.name;
});
}
},
icon: const Icon(Icons.image),
label: const Text('Pick Image'),
),
],
),
if (selectedIconName != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
selectedIconName!,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
),
const SizedBox(height: 16),
const Text('Color', style: TextStyle(color: Colors.grey)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _palette.map((color) {
final isSelected = color.value == selectedColor.value;
return GestureDetector(
onTap: () {
setDialogState(() {
selectedColor = color;
});
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isSelected
? Border.all(color: Colors.white, width: 3)
: null,
boxShadow: [
if (isSelected)
const BoxShadow(
color: Colors.black26,
blurRadius: 4,
spreadRadius: 2,
)
],
),
child: isSelected
? const Icon(Icons.check, color: Colors.white)
: null,
),
);
}).toList(),
),
const SizedBox(height: 16),
TextField(
controller: orderController,
decoration: const InputDecoration(labelText: 'Order'),
keyboardType: TextInputType.number,
),
if (isUploading)
const Padding(
padding: EdgeInsets.only(top: 16),
child: Center(child: CircularProgressIndicator()),
),
],
),
),
actions: [
TextButton(
onPressed: isUploading ? null : () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: isUploading
? null
: () async {
if (nameController.text.isNotEmpty) {
setDialogState(() => isUploading = true);
String iconUrl = '';
if (selectedIconBytes != null &&
selectedIconName != null) {
try {
iconUrl = await Provider.of<FamilyService>(
context,
listen: false,
).uploadFamilyIcon(
bytes: selectedIconBytes!,
filename: selectedIconName!,
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to upload icon: $e')),
);
setDialogState(() => isUploading = false);
return;
}
}
await Provider.of<FamilyService>(
context,
listen: false,
).createFamilyMember(
FamilyMember(
id: '',
name: nameController.text,
emoji: '', // Legacy field
iconUrl: iconUrl,
color: _colorToHex(selectedColor),
order: int.tryParse(orderController.text) ?? 1,
),
);
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: const Text('Add'),
),
],
),
),
);
}
void _showEditMemberDialog(BuildContext context, FamilyMember member) {
final nameController = TextEditingController(text: member.name);
final orderController =
TextEditingController(text: member.order.toString());
Color selectedColor = _hexToColor(member.color);
if (!_palette.any((c) => c.value == selectedColor.value)) {
// If current color is not in palette, try to match closest or add it,
// but for now let's just default to first palette color if invalid,
// or keep it if we want to support custom legacy colors.
// The requirement says "replace hex color input with rainbow palette selection".
// Let's assume we map to the palette or keep custom if it exists.
// But for better UX let's just keep it as is visually, but if they pick a new one it changes.
}
Uint8List? selectedIconBytes;
String? selectedIconName;
bool isUploading = false;
ImageProvider? previewImage;
if (member.iconUrl.isNotEmpty) {
previewImage = NetworkImage(member.iconUrl);
}
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: const Text('Edit Family Member'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Name'),
),
const SizedBox(height: 16),
const Text('Icon', style: TextStyle(color: Colors.grey)),
const SizedBox(height: 8),
Row(
children: [
CircleAvatar(
backgroundColor: selectedColor,
child: selectedIconBytes != null
? ClipOval(
child: Image.memory(
selectedIconBytes!,
width: 40,
height: 40,
fit: BoxFit.cover,
),
)
: (previewImage != null
? ClipOval(
child: Image(
image: previewImage!,
width: 40,
height: 40,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Text(
member.name.isNotEmpty
? member.name[0]
: '?',
),
),
)
: Text(member.name.isNotEmpty
? member.name[0]
: '?')),
),
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
withData: true,
type: FileType.image,
);
if (result != null && result.files.isNotEmpty) {
setDialogState(() {
selectedIconBytes = result.files.first.bytes;
selectedIconName = result.files.first.name;
previewImage = selectedIconBytes == null
? previewImage
: MemoryImage(selectedIconBytes!);
});
}
},
icon: const Icon(Icons.image),
label: const Text('Change Icon'),
),
],
),
const SizedBox(height: 16),
const Text('Color', style: TextStyle(color: Colors.grey)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _palette.map((color) {
final isSelected = color.value == selectedColor.value;
return GestureDetector(
onTap: () {
setDialogState(() {
selectedColor = color;
});
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isSelected
? Border.all(color: Colors.white, width: 3)
: null,
boxShadow: [
if (isSelected)
const BoxShadow(
color: Colors.black26,
blurRadius: 4,
spreadRadius: 2,
)
],
),
child: isSelected
? const Icon(Icons.check, color: Colors.white)
: null,
),
);
}).toList(),
),
const SizedBox(height: 16),
TextField(
controller: orderController,
decoration: const InputDecoration(labelText: 'Order'),
keyboardType: TextInputType.number,
),
if (isUploading)
const Padding(
padding: EdgeInsets.only(top: 16),
child: Center(child: CircularProgressIndicator()),
),
],
),
),
actions: [
TextButton(
onPressed: isUploading ? null : () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: isUploading
? null
: () async {
if (nameController.text.isNotEmpty) {
setDialogState(() => isUploading = true);
String iconUrl = member.iconUrl;
if (selectedIconBytes != null &&
selectedIconName != null) {
try {
iconUrl = await Provider.of<FamilyService>(
context,
listen: false,
).uploadFamilyIcon(
bytes: selectedIconBytes!,
filename: selectedIconName!,
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to upload icon: $e')),
);
setDialogState(() => isUploading = false);
return;
}
}
await Provider.of<FamilyService>(
context,
listen: false,
).updateFamilyMember(
FamilyMember(
id: member.id,
name: nameController.text,
emoji: member
.emoji, // Keep existing emoji or clear it? Let's keep it to be safe
iconUrl: iconUrl,
color: _colorToHex(selectedColor),
order: int.tryParse(orderController.text) ?? 1,
),
);
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: const Text('Save'),
),
],
),
),
);
}
}
class PhotoManagerTab extends StatefulWidget {
const PhotoManagerTab({super.key});
@override
State<PhotoManagerTab> createState() => _PhotoManagerTabState();
}
class _PhotoManagerTabState extends State<PhotoManagerTab> {
Widget _buildGooglePhotosHeader() {
return FutureBuilder<bool>(
future: Provider.of<PhotoService>(context, listen: false).getGoogleStatus(),
builder: (context, snapshot) {
final isConnected = snapshot.data ?? false;
return Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isConnected ? Colors.green.withOpacity(0.1) : Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isConnected ? Colors.green : Colors.blue,
width: 1,
),
),
child: Row(
children: [
Icon(
isConnected ? Icons.cloud_done : Icons.cloud_off,
color: isConnected ? Colors.green : Colors.blue,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isConnected ? 'Google Photos Connected' : 'Google Photos Not Connected',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
isConnected
? 'Photos will be synced automatically.'
: 'Connect to sync your Google Photos albums.',
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
),
ElevatedButton(
onPressed: () async {
if (isConnected) {
await Provider.of<PhotoService>(context, listen: false).disconnectGoogle();
setState(() {});
} else {
try {
final url = await Provider.of<PhotoService>(context, listen: false).getGoogleAuthUrl();
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to connect: $e')),
);
}
}
},
style: ElevatedButton.styleFrom(
backgroundColor: isConnected ? Colors.red.withOpacity(0.1) : null,
foregroundColor: isConnected ? Colors.red : null,
),
child: Text(isConnected ? 'Disconnect' : 'Connect'),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddPhotoDialog(context),
child: const Icon(Icons.add_a_photo),
),
body: Column(
children: [
_buildGooglePhotosHeader(),
Expanded(
child: FutureBuilder<List<Photo>>(
future: Provider.of<PhotoService>(context).fetchPhotos(),
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
final photos = snapshot.data!;
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: photos.length,
itemBuilder: (context, index) {
final photo = photos[index];
return GridTile(
footer: GridTileBar(
backgroundColor: Colors.black54,
title: Text(photo.caption),
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.white),
onPressed: () async {
await Provider.of<PhotoService>(
context,
listen: false,
).deletePhoto(photo.id);
setState(() {});
},
),
),
child: photo.url.startsWith('http')
? Image.network(
photo.url,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
const Center(child: Icon(Icons.broken_image)),
)
: const Center(child: Icon(Icons.photo)),
);
},
);
},
),
),
],
),
);
}
void _showAddPhotoDialog(BuildContext context) {
final urlController = TextEditingController();
final captionController = TextEditingController();
Uint8List? selectedFileBytes;
String? selectedFileName;
bool isUploading = false;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: const Text('Add Photo'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
withData: true,
type: FileType.image,
);
if (result != null && result.files.isNotEmpty) {
setDialogState(() {
selectedFileBytes = result.files.first.bytes;
selectedFileName = result.files.first.name;
// Clear URL if file is selected to avoid confusion
urlController.clear();
});
}
},
icon: const Icon(Icons.upload_file),
label: const Text('Pick Local Image'),
),
if (selectedFileName != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
'Selected: $selectedFileName',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text('- OR -', style: TextStyle(color: Colors.grey)),
),
TextField(
controller: urlController,
decoration: const InputDecoration(labelText: 'Image URL'),
enabled: selectedFileBytes == null,
),
TextField(
controller: captionController,
decoration: const InputDecoration(labelText: 'Caption'),
),
if (isUploading)
const Padding(
padding: EdgeInsets.only(top: 16),
child: CircularProgressIndicator(),
),
],
),
actions: [
TextButton(
onPressed: isUploading ? null : () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: isUploading
? null
: () async {
if (selectedFileBytes != null ||
urlController.text.isNotEmpty) {
setDialogState(() {
isUploading = true;
});
try {
final photoService =
Provider.of<PhotoService>(context, listen: false);
if (selectedFileBytes != null) {
await photoService.uploadPhotoBytes(
bytes: selectedFileBytes!,
filename: selectedFileName!,
caption: captionController.text,
);
} else {
await photoService.createPhoto(
Photo(
id: '',
url: urlController.text,
caption: captionController.text,
active: true,
),
);
}
if (mounted) {
Navigator.pop(context);
setState(() {});
}
} catch (e) {
setDialogState(() {
isUploading = false;
});
// Optionally show error
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
}
},
child: const Text('Add'),
),
],
),
),
);
}
}
class BibleVerseManagerTab extends StatefulWidget {
const BibleVerseManagerTab({super.key});
@override
State<BibleVerseManagerTab> createState() => _BibleVerseManagerTabState();
}
class _BibleVerseManagerTabState extends State<BibleVerseManagerTab> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddVerseDialog(context),
child: const Icon(Icons.menu_book),
),
body: FutureBuilder<List<BibleVerse>>(
future: Provider.of<BibleVerseService>(context).fetchVerses(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final verses = snapshot.data!;
if (verses.isEmpty) {
return const Center(
child: Text(
'No verses added yet',
style: TextStyle(color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: verses.length,
itemBuilder: (context, index) {
final verse = verses[index];
return ListTile(
onTap: () => _showEditVerseDialog(context, verse),
title: Text(verse.reference),
subtitle: Text(
verse.text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (verse.date != null && verse.date!.isNotEmpty)
Text(
verse.date!,
style:
const TextStyle(fontSize: 12, color: Colors.grey),
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => _showEditVerseDialog(context, verse),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
await Provider.of<BibleVerseService>(
context,
listen: false,
).deleteVerse(verse.id);
setState(() {});
},
),
],
),
);
},
);
},
),
);
}
void _showAddVerseDialog(BuildContext context) {
_showVerseDialog(context, null);
}
void _showEditVerseDialog(BuildContext context, BibleVerse verse) {
_showVerseDialog(context, verse);
}
void _showVerseDialog(BuildContext context, BibleVerse? verse) {
final isEditing = verse != null;
final textController = TextEditingController(text: verse?.text ?? '');
final referenceController = TextEditingController(text: verse?.reference ?? '');
final dateController = TextEditingController(text: verse?.date ?? '');
bool isActive = verse?.active ?? true;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(isEditing ? 'Edit Bible Verse' : 'Add Bible Verse'),
content: SizedBox(
width: 600, // Increased width
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: referenceController,
decoration: const InputDecoration(
labelText: 'Reference (e.g., Psalms 23:1)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: textController,
decoration: const InputDecoration(
labelText: 'Verse Text (Korean)',
hintText: 'Enter verse text here. Use \\n for line breaks.',
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
maxLines: 8, // Increased maxLines
),
const SizedBox(height: 16),
TextField(
controller: dateController,
readOnly: true,
decoration: const InputDecoration(
labelText: 'Date (YYYY-MM-DD) - Optional',
hintText: 'Click to select date',
border: OutlineInputBorder(),
suffixIcon: Icon(Icons.calendar_today),
),
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (picked != null) {
setDialogState(() {
dateController.text =
picked.toIso8601String().split('T')[0];
});
}
},
),
const SizedBox(height: 8),
SwitchListTile(
title: const Text('Active'),
value: isActive,
onChanged: (value) {
setDialogState(() {
isActive = value;
});
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
onPressed: () async {
if (textController.text.isNotEmpty &&
referenceController.text.isNotEmpty) {
final newVerse = BibleVerse(
id: verse?.id ?? '',
text: textController.text,
reference: referenceController.text,
date: dateController.text.isEmpty
? null
: dateController.text,
active: isActive,
);
if (isEditing) {
await Provider.of<BibleVerseService>(
context,
listen: false,
).updateVerse(newVerse);
} else {
await Provider.of<BibleVerseService>(
context,
listen: false,
).createVerse(newVerse);
}
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: Text(isEditing ? 'Save Verse' : 'Add Verse'),
),
],
),
),
);
}
}
class AnnouncementManagerTab extends StatefulWidget {
const AnnouncementManagerTab({super.key});
@override
State<AnnouncementManagerTab> createState() => _AnnouncementManagerTabState();
}
class _AnnouncementManagerTabState extends State<AnnouncementManagerTab> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddAnnouncementDialog(context),
child: const Icon(Icons.campaign),
),
body: FutureBuilder<List<Announcement>>(
future: Provider.of<AnnouncementService>(context).fetchAnnouncements(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final announcements = snapshot.data!;
if (announcements.isEmpty) {
return const Center(
child: Text(
'No announcements added yet',
style: TextStyle(color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: announcements.length,
itemBuilder: (context, index) {
final announcement = announcements[index];
return ListTile(
onTap: () => _showEditAnnouncementDialog(context, announcement),
title: Text(announcement.title),
subtitle: Text(
announcement.content,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (announcement.priority > 0)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'P${announcement.priority}',
style: const TextStyle(
fontSize: 10,
color: Colors.white,
),
),
),
Icon(
announcement.active ? Icons.check_circle : Icons.cancel,
color: announcement.active ? Colors.green : Colors.grey,
size: 16,
),
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () =>
_showEditAnnouncementDialog(context, announcement),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
await Provider.of<AnnouncementService>(
context,
listen: false,
).deleteAnnouncement(announcement.id);
setState(() {});
},
),
],
),
);
},
);
},
),
);
}
void _showAddAnnouncementDialog(BuildContext context) {
_showAnnouncementDialog(context, null);
}
void _showEditAnnouncementDialog(
BuildContext context, Announcement announcement) {
_showAnnouncementDialog(context, announcement);
}
void _showAnnouncementDialog(BuildContext context, Announcement? announcement) {
final isEditing = announcement != null;
final titleController =
TextEditingController(text: announcement?.title ?? '');
final contentController =
TextEditingController(text: announcement?.content ?? '');
final priorityController =
TextEditingController(text: announcement?.priority.toString() ?? '0');
bool isActive = announcement?.active ?? true;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(isEditing ? 'Edit Announcement' : 'Add Announcement'),
content: SizedBox(
width: 600,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: contentController,
decoration: const InputDecoration(
labelText: 'Content',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
TextField(
controller: priorityController,
decoration: const InputDecoration(
labelText: 'Priority (0-10)',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 8),
SwitchListTile(
title: const Text('Active'),
value: isActive,
onChanged: (value) {
setDialogState(() {
isActive = value;
});
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
onPressed: () async {
if (titleController.text.isNotEmpty) {
final newAnnouncement = Announcement(
id: announcement?.id ?? '',
title: titleController.text,
content: contentController.text,
priority: int.tryParse(priorityController.text) ?? 0,
active: isActive,
);
if (isEditing) {
await Provider.of<AnnouncementService>(
context,
listen: false,
).updateAnnouncement(newAnnouncement);
} else {
await Provider.of<AnnouncementService>(
context,
listen: false,
).createAnnouncement(newAnnouncement);
}
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child:
Text(isEditing ? 'Save Announcement' : 'Add Announcement'),
),
],
),
),
);
}
}
class ScheduleManagerTab extends StatefulWidget {
const ScheduleManagerTab({super.key});
@override
State<ScheduleManagerTab> createState() => _ScheduleManagerTabState();
}
class _ScheduleManagerTabState extends State<ScheduleManagerTab> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddScheduleDialog(context),
child: const Icon(Icons.calendar_today),
),
body: FutureBuilder<List<ScheduleItem>>(
future: Provider.of<ScheduleService>(context).fetchSchedules(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final schedules = snapshot.data!;
if (schedules.isEmpty) {
return const Center(
child: Text(
'No schedules added yet',
style: TextStyle(color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: schedules.length,
itemBuilder: (context, index) {
final schedule = schedules[index];
return ListTile(
onTap: () => _showEditScheduleDialog(context, schedule),
title: Text(schedule.title),
subtitle: Text(
'${schedule.description}\n${schedule.startDate.toIso8601String().split('T')[0]} ~ ${schedule.endDate.toIso8601String().split('T')[0]}',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
isThreeLine: true,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => _showEditScheduleDialog(context, schedule),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
await Provider.of<ScheduleService>(
context,
listen: false,
).deleteSchedule(schedule.id);
setState(() {});
},
),
],
),
);
},
);
},
),
);
}
void _showAddScheduleDialog(BuildContext context) {
_showScheduleDialog(context, null);
}
void _showEditScheduleDialog(BuildContext context, ScheduleItem schedule) {
_showScheduleDialog(context, schedule);
}
void _showScheduleDialog(BuildContext context, ScheduleItem? schedule) {
final isEditing = schedule != null;
final titleController = TextEditingController(text: schedule?.title ?? '');
final descriptionController =
TextEditingController(text: schedule?.description ?? '');
final startController = TextEditingController(
text: schedule?.startDate.toIso8601String().split('T')[0] ??
DateTime.now().toIso8601String().split('T')[0],
);
final endController = TextEditingController(
text: schedule?.endDate.toIso8601String().split('T')[0] ??
DateTime.now().toIso8601String().split('T')[0],
);
const bool isAllDay = true; // Forcing all day as requested
String? selectedFamilyMemberId = schedule?.familyMemberId;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(isEditing ? 'Edit Schedule' : 'Add Schedule'),
content: SizedBox(
width: 600,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: descriptionController,
decoration: const InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 16),
TextField(
controller: startController,
readOnly: true,
decoration: const InputDecoration(
labelText: 'Start Date (YYYY-MM-DD)',
suffixIcon: Icon(Icons.calendar_today),
border: OutlineInputBorder(),
),
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.tryParse(startController.text) ??
DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (picked != null) {
setDialogState(() {
startController.text =
picked.toIso8601String().split('T')[0];
});
}
},
),
const SizedBox(height: 16),
TextField(
controller: endController,
readOnly: true,
decoration: const InputDecoration(
labelText: 'End Date (YYYY-MM-DD)',
suffixIcon: Icon(Icons.calendar_today),
border: OutlineInputBorder(),
),
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.tryParse(endController.text) ??
DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (picked != null) {
setDialogState(() {
endController.text =
picked.toIso8601String().split('T')[0];
});
}
},
),
const SizedBox(height: 8),
// All Day toggle removed as requested
const SizedBox(height: 8),
FutureBuilder<List<FamilyMember>>(
future: Provider.of<FamilyService>(context, listen: false)
.fetchFamilyMembers(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox();
final members = snapshot.data!;
return DropdownButtonFormField<String>(
value: selectedFamilyMemberId,
decoration: const InputDecoration(
labelText: 'Family Member',
border: OutlineInputBorder(),
),
items: members.map((member) {
return DropdownMenuItem(
value: member.id,
child: Text(member.name),
);
}).toList(),
onChanged: (value) {
setDialogState(() {
selectedFamilyMemberId = value;
});
},
);
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
if (titleController.text.isNotEmpty) {
final startDate =
DateTime.tryParse(startController.text) ?? DateTime.now();
final endDate =
DateTime.tryParse(endController.text) ?? DateTime.now();
final newSchedule = ScheduleItem(
id: schedule?.id ?? '',
title: titleController.text,
description: descriptionController.text,
startDate: startDate,
endDate: endDate,
familyMemberId: selectedFamilyMemberId ?? '',
isAllDay: isAllDay,
);
if (isEditing) {
await Provider.of<ScheduleService>(
context,
listen: false,
).updateSchedule(newSchedule);
} else {
await Provider.of<ScheduleService>(
context,
listen: false,
).createSchedule(newSchedule);
}
if (mounted) {
Navigator.pop(context);
setState(() {});
}
}
},
child: Text(isEditing ? 'Save Schedule' : 'Add Schedule'),
),
],
),
),
);
}
}
class TodoManagerTab extends StatefulWidget {
const TodoManagerTab({super.key});
@override
State<TodoManagerTab> createState() => _TodoManagerTabState();
}
class _TodoManagerTabState extends State<TodoManagerTab> {
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddTodoDialog(context),
child: const Icon(Icons.check_box),
),
body: FutureBuilder<List<TodoItem>>(
future: Provider.of<TodoService>(context).fetchTodos(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final todos = snapshot.data!;
if (todos.isEmpty) {
return const Center(
child: Text(
'No todos added yet',
style: TextStyle(color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
onTap: () => _showEditTodoDialog(context, todo),
leading: Checkbox(
value: todo.completed,
onChanged: (val) async {
await Provider.of<TodoService>(
context,
listen: false,
).updateTodo(
TodoItem(
id: todo.id,
familyMemberId: todo.familyMemberId,
title: todo.title,
completed: val ?? false,
dueDate: todo.dueDate,
),
);
setState(() {});
},
),
title: Text(
todo.title,
style: TextStyle(
decoration:
todo.completed ? TextDecoration.lineThrough : null,
),
),
subtitle: todo.dueDate != null
? Text(
todo.dueDate!.toIso8601String().split('T')[0],
style:
const TextStyle(fontSize: 12, color: Colors.grey),
)
: null,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => _showEditTodoDialog(context, todo),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
await Provider.of<TodoService>(
context,
listen: false,
).deleteTodo(todo.id);
setState(() {});
},
),
],
),
);
},
);
},
),
);
}
void _showAddTodoDialog(BuildContext context) {
_showTodoDialog(context, null);
}
void _showEditTodoDialog(BuildContext context, TodoItem todo) {
_showTodoDialog(context, todo);
}
void _showTodoDialog(BuildContext context, TodoItem? todo) {
final isEditing = todo != null;
final titleController = TextEditingController(text: todo?.title ?? '');
final todoDueDate = todo?.dueDate;
final dateController = TextEditingController(
text: todoDueDate != null ? todoDueDate.toIso8601String().split('T')[0] : '',
);
final timeController = TextEditingController(
text: todoDueDate != null ? TimeOfDay.fromDateTime(todoDueDate).format(context) : '',
);
String? selectedFamilyMemberId = todo?.familyMemberId;
bool isCompleted = todo?.completed ?? false;
TimeOfDay? selectedTime = todoDueDate != null ? TimeOfDay.fromDateTime(todoDueDate) : null;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(isEditing ? 'Edit Todo' : 'Add Todo'),
content: SizedBox(
width: 600,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: dateController,
readOnly: true,
decoration: const InputDecoration(
labelText: 'Due Date',
hintText: 'Select date',
suffixIcon: Icon(Icons.calendar_today),
border: OutlineInputBorder(),
),
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.tryParse(dateController.text) ??
DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (picked != null) {
setDialogState(() {
dateController.text =
picked.toIso8601String().split('T')[0];
});
}
},
),
const SizedBox(height: 16),
TextField(
controller: timeController,
readOnly: true,
decoration: const InputDecoration(
labelText: 'Time (Optional)',
hintText: 'Select time',
suffixIcon: Icon(Icons.access_time),
border: OutlineInputBorder(),
),
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: selectedTime ?? TimeOfDay.now(),
);
if (picked != null) {
setDialogState(() {
selectedTime = picked;
timeController.text = picked.format(context);
});
}
},
),
const SizedBox(height: 16),
FutureBuilder<List<FamilyMember>>(
future: Provider.of<FamilyService>(context, listen: false)
.fetchFamilyMembers(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox();
final members = snapshot.data!;
return DropdownButtonFormField<String>(
value: selectedFamilyMemberId,
decoration: const InputDecoration(
labelText: 'Family Member',
border: OutlineInputBorder(),
),
items: members.map((member) {
return DropdownMenuItem(
value: member.id,
child: Text(member.name),
);
}).toList(),
onChanged: (value) {
setDialogState(() {
selectedFamilyMemberId = value;
});
},
);
},
),
if (isEditing)
SwitchListTile(
title: const Text('Completed'),
value: isCompleted,
onChanged: (value) {
setDialogState(() {
isCompleted = value;
});
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
if (titleController.text.isNotEmpty &&
selectedFamilyMemberId != null) {
DateTime? fullDate;
if (dateController.text.isNotEmpty) {
final baseDate = DateTime.tryParse(dateController.text);
if (baseDate != null) {
fullDate = baseDate;
if (selectedTime != null) {
fullDate = DateTime(
fullDate.year,
fullDate.month,
fullDate.day,
selectedTime!.hour,
selectedTime!.minute,
);
}
}
}
final newItem = TodoItem(
id: todo?.id ?? '',
familyMemberId: selectedFamilyMemberId!,
title: titleController.text,
completed: isCompleted,
dueDate: fullDate,
);
if (isEditing) {
await Provider.of<TodoService>(
context,
listen: false,
).updateTodo(newItem);
} else {
await Provider.of<TodoService>(
context,
listen: false,
).createTodo(newItem);
}
if (mounted) {
Navigator.pop(context);
setState(() {});
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Title and Family Member are required')),
);
}
},
child: Text(isEditing ? 'Save Todo' : 'Add Todo'),
),
],
),
),
);
}
}