From b56c0fc02d59bc3e017478dd52eb77244d20d966 Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Mon, 6 Apr 2026 10:25:52 -0400 Subject: [PATCH] feat: add table_calendar package and CalendarEvent model with tests Co-Authored-By: Claude Sonnet 4.6 --- lib/data/models/calendar_event.dart | 47 ++++++++++++++++++++++ pubspec.lock | 24 ++++++++++++ pubspec.yaml | 1 + test/widget_test.dart | 60 +++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 lib/data/models/calendar_event.dart diff --git a/lib/data/models/calendar_event.dart b/lib/data/models/calendar_event.dart new file mode 100644 index 0000000..827a2ff --- /dev/null +++ b/lib/data/models/calendar_event.dart @@ -0,0 +1,47 @@ +class CalendarEvent { + final int id; + final String title; + final DateTime startDt; + final DateTime? endDt; + final bool allDay; + final String description; + final String location; + final String color; + final String? recurrence; + final int? projectId; + final int? reminderMinutes; + + const CalendarEvent({ + required this.id, + required this.title, + required this.startDt, + this.endDt, + required this.allDay, + required this.description, + required this.location, + required this.color, + this.recurrence, + this.projectId, + this.reminderMinutes, + }); + + factory CalendarEvent.fromJson(Map json) => CalendarEvent( + id: json['id'] as int, + title: json['title'] as String? ?? '', + startDt: DateTime.parse(json['start_dt'] as String), + endDt: json['end_dt'] != null + ? DateTime.parse(json['end_dt'] as String) + : null, + allDay: json['all_day'] as bool? ?? false, + description: json['description'] as String? ?? '', + location: json['location'] as String? ?? '', + color: json['color'] as String? ?? '', + recurrence: json['recurrence'] as String?, + projectId: json['project_id'] as int?, + reminderMinutes: json['reminder_minutes'] as int?, + ); +} + +/// Strips time from a DateTime, returning midnight local. +/// Used as map keys in CalendarState.eventsByDay. +DateTime dateOnly(DateTime dt) => DateTime(dt.year, dt.month, dt.day); diff --git a/pubspec.lock b/pubspec.lock index 44961eb..5f15306 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -408,6 +408,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: @@ -952,6 +960,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + simple_gesture_detector: + dependency: transitive + description: + name: simple_gesture_detector + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 + url: "https://pub.dev" + source: hosted + version: "0.2.1" sky_engine: dependency: transitive description: flutter @@ -1021,6 +1037,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.0" + table_calendar: + dependency: "direct main" + description: + name: table_calendar + sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" + url: "https://pub.dev" + source: hosted + version: "3.2.0" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 064c763..b76d5f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: url_launcher: ^6.3.1 record: ^5.0.0 just_audio: ^0.9.39 + table_calendar: ^3.1.2 dependency_overrides: record_linux: ^1.3.0 diff --git a/test/widget_test.dart b/test/widget_test.dart index 3820264..df2f476 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,3 +1,4 @@ +import 'package:fabled_app/data/models/calendar_event.dart'; import 'package:fabled_app/data/api/voice_api.dart'; import 'package:fabled_app/providers/voice_provider.dart'; import 'package:fabled_app/data/models/knowledge_item.dart'; @@ -190,6 +191,65 @@ void main() { }); }); + group('CalendarEvent.fromJson', () { + test('parses all fields', () { + final json = { + 'id': 10, + 'title': 'Team meeting', + 'start_dt': '2026-04-07T09:00:00+00:00', + 'end_dt': '2026-04-07T10:00:00+00:00', + 'all_day': false, + 'description': 'Weekly sync', + 'location': 'Room 4', + 'color': '#6366F1', + 'recurrence': 'FREQ=WEEKLY', + 'project_id': 3, + 'reminder_minutes': 15, + }; + final event = CalendarEvent.fromJson(json); + expect(event.id, equals(10)); + expect(event.title, equals('Team meeting')); + expect(event.startDt, equals(DateTime.parse('2026-04-07T09:00:00+00:00'))); + expect(event.endDt, equals(DateTime.parse('2026-04-07T10:00:00+00:00'))); + expect(event.allDay, isFalse); + expect(event.description, equals('Weekly sync')); + expect(event.location, equals('Room 4')); + expect(event.color, equals('#6366F1')); + expect(event.recurrence, equals('FREQ=WEEKLY')); + expect(event.projectId, equals(3)); + expect(event.reminderMinutes, equals(15)); + }); + + test('handles null optional fields', () { + final json = { + 'id': 11, + 'title': 'Birthday', + 'start_dt': '2026-05-01T00:00:00+00:00', + 'end_dt': null, + 'all_day': true, + 'description': '', + 'location': '', + 'color': '', + 'recurrence': null, + 'project_id': null, + 'reminder_minutes': null, + }; + final event = CalendarEvent.fromJson(json); + expect(event.endDt, isNull); + expect(event.allDay, isTrue); + expect(event.recurrence, isNull); + expect(event.projectId, isNull); + expect(event.reminderMinutes, isNull); + }); + }); + + group('dateOnly', () { + test('strips time from datetime', () { + final dt = DateTime(2026, 4, 7, 14, 30, 45); + expect(dateOnly(dt), equals(DateTime(2026, 4, 7))); + }); + }); + group('BriefingFeed.fromJson', () { test('parses all fields', () { final json = {