From 80bad7a1b1c31fb7a28d74796adcf44ee319f586 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Fri, 14 Apr 2023 17:58:15 -0600 Subject: Fully implemented events --- lib/models/event.dart | 2 +- lib/providers/events_provider.dart | 10 ++++ lib/screens/events_screen.dart | 87 ++++++++++++++++++++++++++-------- lib/screens/new_event_screen.dart | 24 ++++++++-- lib/utils.dart | 5 ++ lib/widgets/event_item.dart | 96 ++++++++++++++++++++++++++++++++------ 6 files changed, 186 insertions(+), 38 deletions(-) create mode 100644 lib/utils.dart diff --git a/lib/models/event.dart b/lib/models/event.dart index d9ef6f6..1c050ad 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -16,7 +16,7 @@ class Event { id: map["id"], description: map["description"], date: DateTime.parse(map["date"]), - completed: map["date"] == 1, + completed: map["completed"] == 1, ); } } diff --git a/lib/providers/events_provider.dart b/lib/providers/events_provider.dart index eee9853..01b6982 100644 --- a/lib/providers/events_provider.dart +++ b/lib/providers/events_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:pmsna1/database/helper.dart'; import 'package:pmsna1/models/event.dart'; +import 'package:table_calendar/table_calendar.dart'; class EventsProvider with ChangeNotifier { DatabaseHelper helper = DatabaseHelper(); @@ -25,4 +26,13 @@ class EventsProvider with ChangeNotifier { events = e; }); } + + Future deleteEvent(int id) async { + await helper.delete('events', 'id', id); + fetchDB(); + } + + List getEventsForDay(DateTime day) { + return events.where((e) => isSameDay(e.date, day)).toList(); + } } diff --git a/lib/screens/events_screen.dart b/lib/screens/events_screen.dart index 0b78fbe..9183e5e 100644 --- a/lib/screens/events_screen.dart +++ b/lib/screens/events_screen.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:pmsna1/utils.dart'; +import 'package:provider/provider.dart'; import 'package:table_calendar/table_calendar.dart'; +import '../models/event.dart'; +import '../providers/events_provider.dart'; import '../widgets/event_list.dart'; enum Vista { calendario, lista } @@ -18,8 +22,32 @@ class _EventsScreenState extends State { DateTime _focusedDay = DateTime.now(); CalendarFormat _calendarFormat = CalendarFormat.month; + Color? getEventColor(Event e) { + DateTime today = DateTime.now(); + if (daysBetween(today, e.date) > 0 && daysBetween(today, e.date) <= 2) { + // 7. Cuando el evento esté a 2 días de realizarse, la + // celda del calendario deberá pintarse de color amarillo + return Colors.yellow; + } else if (isSameDay(e.date, today)) { + // 8. Cuando el evento sea el día en curso cambiará a un + // color verde + return Colors.green; + } else if (today.isAfter(e.date) && !e.completed) { + // 9. Cuando el evento ya haya pasado y no se haya + // completado, entonces se marcará en rojo + return Colors.red; + } else if (today.isAfter(e.date) && e.completed) { + // 10. Si el evento ya pasó y está como completado se + // dejará en verde + return Colors.green; + } + return null; + } + @override Widget build(BuildContext context) { + EventsProvider provider = Provider.of(context); + return Scaffold( appBar: AppBar( title: const Text('Eventos'), @@ -47,25 +75,46 @@ class _EventsScreenState extends State { ), body: Container( child: vista == Vista.calendario - ? TableCalendar( - firstDay: DateTime.fromMicrosecondsSinceEpoch(0), - lastDay: DateTime.utc(9000, 12, 31), - focusedDay: _focusedDay, - selectedDayPredicate: (day) { - return isSameDay(_selectedDay, day); - }, - onDaySelected: (selectedDay, focusedDay) { - setState(() { - _selectedDay = selectedDay; - _focusedDay = focusedDay; - }); - }, - calendarFormat: _calendarFormat, - onFormatChanged: (format) { - setState(() { - _calendarFormat = format; - }); - }, + ? SingleChildScrollView( + child: TableCalendar( + firstDay: DateTime.fromMicrosecondsSinceEpoch(0), + lastDay: DateTime.utc(9000, 12, 31), + focusedDay: _focusedDay, + selectedDayPredicate: (day) { + return isSameDay(_selectedDay, day); + }, + onDaySelected: (selectedDay, focusedDay) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + }); + }, + calendarFormat: _calendarFormat, + onFormatChanged: (format) { + setState(() { + _calendarFormat = format; + }); + }, + eventLoader: (day) => provider.getEventsForDay(day), + // Source: https://stackoverflow.com/a/69036998 + calendarBuilders: CalendarBuilders( + singleMarkerBuilder: (context, day, event) { + if (event == null) return null; + Event e = event as Event; + Color color = getEventColor(e) ?? + Theme.of(context).colorScheme.onSurface; + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + width: 5.0, + height: 5.0, + margin: const EdgeInsets.symmetric(horizontal: 1.5), + ); + }, + ), + ), ) : const EventList()), floatingActionButton: FloatingActionButton.extended( diff --git a/lib/screens/new_event_screen.dart b/lib/screens/new_event_screen.dart index 3c49ee6..2e8b52f 100644 --- a/lib/screens/new_event_screen.dart +++ b/lib/screens/new_event_screen.dart @@ -14,11 +14,13 @@ class NewEventScreen extends StatefulWidget { class _NewEventScreenState extends State { Event? event; + bool isEventSet = false; DateTime? selectedDate; DatabaseHelper helper = DatabaseHelper(); final _descController = TextEditingController(); final _formKey = GlobalKey(); + bool completed = false; final padding = 12.0; Widget get spacer => SizedBox(height: padding, width: padding); @@ -26,11 +28,13 @@ class _NewEventScreenState extends State { @override Widget build(BuildContext context) { event = ModalRoute.of(context)?.settings.arguments as Event?; - if (event != null) { + if (event != null && !isEventSet) { _descController.text = event!.description; setState(() { selectedDate = event!.date; + completed = event!.completed; }); + isEventSet = true; } EventsProvider provider = Provider.of(context); @@ -54,7 +58,7 @@ class _NewEventScreenState extends State { helper.insert('events', { 'description': _descController.text, 'date': selectedDate!.toIso8601String(), - 'completed': false, // TODO: implement checkbox + 'completed': completed == true ? 1 : 0, }).then((value) { SnackBar bar = SnackBar( content: Text( @@ -73,7 +77,7 @@ class _NewEventScreenState extends State { { 'description': _descController.text, 'date': selectedDate!.toIso8601String(), - 'completed': false, + 'completed': completed == true ? 1 : 0, }, 'id', event!.id, @@ -115,14 +119,24 @@ class _NewEventScreenState extends State { }, ), spacer, + CheckboxListTile( + value: completed, + title: const Text('Completado'), + onChanged: (value) { + setState(() { + completed = value == true; + }); + }, + ), + spacer, OutlinedButton( child: Text(selectedDate != null ? selectedDate!.toIso8601String() : 'Seleccionar fecha'), onPressed: () { showDatePicker( - initialDate: DateTime.now(), - firstDate: DateTime.now(), + initialDate: selectedDate ?? DateTime.now(), + firstDate: DateTime.fromMicrosecondsSinceEpoch(0), lastDate: DateTime.utc(9000, 12, 31), currentDate: DateTime.now(), initialEntryMode: DatePickerEntryMode.calendar, diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..79bee4e --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,5 @@ +int daysBetween(DateTime from, DateTime to) { + from = DateTime(from.year, from.month, from.day); + to = DateTime(to.year, to.month, to.day); + return (to.difference(from).inHours / 24).round(); +} diff --git a/lib/widgets/event_item.dart b/lib/widgets/event_item.dart index e00d328..eb65566 100644 --- a/lib/widgets/event_item.dart +++ b/lib/widgets/event_item.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../models/event.dart'; +import '../providers/events_provider.dart'; class EventItem extends StatelessWidget { final Event event; @@ -16,25 +18,93 @@ class EventItem extends StatelessWidget { @override Widget build(BuildContext context) { final txtDate = Text( - event.date.toIso8601String(), - style: Theme.of(context).typography.englishLike.labelMedium, + event.date.toString().split(' ').first, + style: Theme.of(context).typography.englishLike.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ); final txtDesc = Text( event.description, - style: Theme.of(context).typography.englishLike.bodyLarge, + style: Theme.of(context).typography.englishLike.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + decoration: event.completed == true + ? TextDecoration.lineThrough + : TextDecoration.none, + ), ); + EventsProvider provider = context.watch(); + return Card( - child: Padding( - padding: EdgeInsets.all(padding), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - txtDesc, - spacer, - txtDate, - ], + semanticContainer: true, + clipBehavior: Clip.antiAliasWithSaveLayer, + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(event.description), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Text('ID'), + subtitle: Text(event.id.toString()), + ), + ListTile( + title: const Text('Fecha'), + subtitle: Text(event.date.toIso8601String()), + ), + ListTile( + title: const Text('Completado'), + subtitle: Text(event.completed == true ? "Sí" : "No"), + ), + ], + ), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], + ), + ); + }, + child: Padding( + padding: EdgeInsets.all(padding), + child: Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + txtDesc, + spacer, + txtDate, + ], + ), + const Expanded(child: SizedBox.shrink()), + ButtonBar( + children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + Navigator.of(context) + .pushNamed('/newevent', arguments: event); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + provider.deleteEvent(event.id); + }, + ) + ], + ), + ], + ), ), ), ); -- cgit v1.2.3