From dc436c49e421b13f13b649c191b1a3e88da54012 Mon Sep 17 00:00:00 2001 From: otoshigo Date: Sun, 30 Nov 2025 07:08:17 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E4=BB=AE=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front_end/lib/mock/calendar_mock_data.dart | 168 +++++++++++++++++++ front_end/lib/pages/calendar_inbox_page.dart | 20 ++- 2 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 front_end/lib/mock/calendar_mock_data.dart diff --git a/front_end/lib/mock/calendar_mock_data.dart b/front_end/lib/mock/calendar_mock_data.dart new file mode 100644 index 0000000..19119d0 --- /dev/null +++ b/front_end/lib/mock/calendar_mock_data.dart @@ -0,0 +1,168 @@ +import '../models/calendar_event_proposal.dart'; +import '../models/event_time.dart' as proposal_time; +import '../models/models.dart' as api_models; +import '../models/reminder.dart' as proposal_reminder; + +/// カレンダー追加フローを手元で確認するためのモックデータ。 +/// UI テストやデモで `CalendarInboxProvider.addAll` に流し込む用途を想定。 +class CalendarMockData { + static final List<_EventSeed> _seeds = [ + _EventSeed( + summary: 'プロジェクトキックオフ', + description: '進行方法の合意と役割分担を決定するミーティング', + startDateTime: '2025-12-15T10:00:00+09:00', + endDateTime: '2025-12-15T11:00:00+09:00', + timeZone: 'Asia/Tokyo', + location: 'オンライン (Meet)', + attendees: ['pm@example.com', 'dev@example.com'], + useDefaultReminders: false, + reminders: const [ + ReminderSeed(method: 'popup', minutes: 10), + ReminderSeed(method: 'email', minutes: 60), + ], + ), + _EventSeed( + summary: 'チーム週次スタンドアップ', + description: '進捗共有とブロッカー確認', + startDateTime: '2025-12-17T09:30:00+09:00', + endDateTime: '2025-12-17T10:00:00+09:00', + timeZone: 'Asia/Tokyo', + location: 'A会議室', + attendees: ['lead@example.com'], + useDefaultReminders: true, + reminders: const [], + ), + _EventSeed( + summary: '有休申請', + description: '終日扱いの休暇申請', + startDate: '2025-12-01', + endDate: '2025-12-01', + timeZone: 'Asia/Tokyo', + location: null, + attendees: const [], + useDefaultReminders: false, + reminders: const [ + ReminderSeed(method: 'popup', minutes: 1440), // 前日通知 + ], + ), + ]; + + /// バックエンドレスポンスを模した CalendarEvent 一覧。 + static List get calendarEvents => + _seeds.map((seed) => seed.toCalendarEvent()).toList(); + + /// ボトムシート表示用の CalendarEventProposal 一覧。 + static List get proposals => + _seeds.map((seed) => seed.toProposal()).toList(); + + /// TwoStageResponse にまとめたモックレスポンス。 + static api_models.TwoStageResponse buildTwoStageResponse({ + String summarizedText = '音声入力から抽出されたイベントのサンプルです。', + }) { + return api_models.TwoStageResponse( + summarizedText: summarizedText, + calendarEvents: calendarEvents, + ); + } +} + +class _EventSeed { + final String summary; + final String? description; + final String? startDateTime; + final String? startDate; + final String? endDateTime; + final String? endDate; + final String? timeZone; + final String? location; + final List attendees; + final bool useDefaultReminders; + final List reminders; + + const _EventSeed({ + required this.summary, + required this.description, + this.startDateTime, + this.startDate, + this.endDateTime, + this.endDate, + this.timeZone, + this.location, + this.attendees = const [], + required this.useDefaultReminders, + this.reminders = const [], + }) : assert( + startDateTime != null || startDate != null, + 'startDateTimeまたはstartDateのいずれかは必須です', + ); + + api_models.CalendarEvent toCalendarEvent() { + return api_models.CalendarEvent( + summary: summary, + description: description, + start: api_models.EventTime( + dateTime: startDateTime, + date: startDate, + timeZone: timeZone, + ), + end: api_models.EventTime( + dateTime: endDateTime, + date: endDate, + timeZone: timeZone, + ), + location: location, + attendees: attendees.isNotEmpty ? attendees : null, + reminders: api_models.Reminders( + useDefault: useDefaultReminders, + overrides: reminders.isNotEmpty + ? reminders + .map((r) => api_models.ReminderMethod( + method: r.method, + minutes: r.minutes, + )) + .toList() + : null, + ), + ); + } + + CalendarEventProposal toProposal() { + return CalendarEventProposal( + summary: summary, + description: description, + start: proposal_time.EventTime( + dateTime: startDateTime, + date: startDate, + timeZone: timeZone, + ), + end: proposal_time.EventTime( + dateTime: endDateTime, + date: endDate, + timeZone: timeZone, + ), + location: location, + attendees: attendees.isNotEmpty ? attendees : null, + reminders: proposal_reminder.Reminders( + useDefault: useDefaultReminders, + overrides: reminders.isNotEmpty + ? reminders + .map((r) => proposal_reminder.ReminderMethod( + method: r.method, + minutes: r.minutes, + )) + .toList() + : null, + ), + ); + } +} + +class ReminderSeed { + final String method; + final int minutes; + + const ReminderSeed({ + required this.method, + required this.minutes, + }); +} diff --git a/front_end/lib/pages/calendar_inbox_page.dart b/front_end/lib/pages/calendar_inbox_page.dart index 0a3d341..8702674 100644 --- a/front_end/lib/pages/calendar_inbox_page.dart +++ b/front_end/lib/pages/calendar_inbox_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/calendar_inbox_provider.dart'; +import '../mock/calendar_mock_data.dart'; import '../services/googleCalendarService.dart'; import '../models/calendar_event_proposal.dart'; import '../widgets/editable_calendar_event_sheet.dart'; @@ -32,8 +33,23 @@ class CalendarInboxPage extends StatelessWidget { builder: (context, inbox, _) { final items = inbox.items; if (items.isEmpty) { - return const Center( - child: Text('インボックスは空です', style: TextStyle(color: Colors.black54)), + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('インボックスは空です', style: TextStyle(color: Colors.black54)), + const SizedBox(height: 12), + ElevatedButton( + onPressed: () { + context.read().addAll(CalendarMockData.proposals); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('モックデータを追加しました')), + ); + }, + child: const Text('モックデータを追加'), + ), + ], + ), ); } return ListView.separated( From 7e0ec0f08dc3fe37a476fd76d1d8898d297e42eb Mon Sep 17 00:00:00 2001 From: otoshigo Date: Sun, 30 Nov 2025 07:13:59 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E4=BB=A5=E5=89=8D=E3=81=AE?= =?UTF-8?q?=E8=87=AA=E5=8B=95=E3=82=AB=E3=83=AC=E3=83=B3=E3=83=80=E3=83=BC?= =?UTF-8?q?=E7=99=BB=E9=8C=B2=E5=87=A6=E7=90=86=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/services/voiceRecognitionService.dart | 90 ------------------- 1 file changed, 90 deletions(-) diff --git a/front_end/lib/services/voiceRecognitionService.dart b/front_end/lib/services/voiceRecognitionService.dart index 2dd6ca4..993883d 100644 --- a/front_end/lib/services/voiceRecognitionService.dart +++ b/front_end/lib/services/voiceRecognitionService.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:flutter_speech_to_text/services/googleCalendarService.dart'; import 'package:http/http.dart' as http; import '../providers/textsDataProvider.dart'; import '../providers/recognitionProvider.dart'; @@ -64,9 +62,6 @@ class VoiceRecognitionService { String _currentPhrasePrefix = ""; int maxWords = 100; - // 呼び出し済みのsummarizedTextsを追跡するセット - Set calledeventTime = {}; - // フレーズ変更時の更新処理 void updatePhraseIfNeeded(String newRecognizedText, String selectedClass, TextsDataProvider textsDataProvider) { @@ -179,9 +174,6 @@ class VoiceRecognitionService { print('キーワード "$keyword" を保存しました: $snippet'); - // 日時パターン検出とカレンダー登録 - await _processCalendarRegistration(snippet, keywordData.detectionTime); - // 保存が完了したらマップから削除 _pendingKeywordData.remove(uniqueKey); } catch (e) { @@ -191,88 +183,6 @@ class VoiceRecognitionService { } } - // カレンダー登録処理 - Future _processCalendarRegistration( - String snippet, DateTime detectionTime) async { - final now = detectionTime; - DateTime? eventDt; - - // 1. 相対日+時刻:「今日」「明日」「明後日」 - final rel = - RegExp(r'(今日|明日|明後日)(?:\s*(\d{1,2}:\d{2}))?').firstMatch(snippet); - if (rel != null) { - int days = rel.group(1) == '明日' - ? 1 - : rel.group(1) == '明後日' - ? 2 - : 0; - final base = now.add(Duration(days: days)); - if (rel.group(2) != null) { - final p = rel.group(2)!.split(':'); - eventDt = DateTime( - base.year, base.month, base.day, int.parse(p[0]), int.parse(p[1])); - } else { - eventDt = DateTime(base.year, base.month, base.day, 9, 0); - } - } - // 2. 「YYYY/MM/DD [HH:mm]」 - else { - final ymd = - RegExp(r'(\d{4})[/-](\d{1,2})[/-](\d{1,2})(?:\s*(\d{1,2}:\d{2}))?') - .firstMatch(snippet); - if (ymd != null) { - final y = int.parse(ymd.group(1)!), - m = int.parse(ymd.group(2)!), - d = int.parse(ymd.group(3)!); - if (ymd.group(4) != null) { - final p = ymd.group(4)!.split(':'); - eventDt = DateTime(y, m, d, int.parse(p[0]), int.parse(p[1])); - } else { - eventDt = DateTime(y, m, d, 9, 0); - } - } - // 3. 「M月D日 [HH:mm]」 - else { - final md = RegExp(r'(\d{1,2})月(\d{1,2})日(?:\s*(\d{1,2}:\d{2}))?') - .firstMatch(snippet); - if (md != null) { - final m = int.parse(md.group(1)!), d = int.parse(md.group(2)!); - if (md.group(3) != null) { - final p = md.group(3)!.split(':'); - eventDt = - DateTime(now.year, m, d, int.parse(p[0]), int.parse(p[1])); - } else { - eventDt = DateTime(now.year, m, d, 9, 0); - } - } - // 4. 時刻のみ「HH:mm」 - else { - final t = RegExp(r'(\d{1,2}:\d{2})').firstMatch(snippet); - if (t != null) { - final p = t.group(1)!.split(':'); - eventDt = DateTime( - now.year, now.month, now.day, int.parse(p[0]), int.parse(p[1])); - } - } - } - } - - if (eventDt != null && FirebaseAuth.instance.currentUser != null) { - try { - final service = GoogleCalendarService(); - await service.createEvent( - eventTime: eventDt, - summary: snippet, - duration: Duration(hours: 1), - timeZone: 'Asia/Tokyo', - ); - print('Googleカレンダーにイベントを追加しました'); - } catch (e) { - print('カレンダー登録エラー: $e'); - } - } - } - // キーワード検出処理 bool checkForKeyword(String text, KeywordProvider keywordProvider) { List keywords = keywordProvider.keywords; From ce8fcc496b90d61a0f3acfafda3cf3e91ed79164 Mon Sep 17 00:00:00 2001 From: otoshigo Date: Sun, 30 Nov 2025 07:40:58 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E6=97=A5=E6=99=82=E3=82=92=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=81=97=E3=81=9F=E3=81=A8=E3=81=8D=E3=81=AB=E9=96=8B?= =?UTF-8?q?=E5=A7=8B=E3=81=99=E3=82=8B=E6=97=A5=E6=99=82=E3=81=A8=E7=B5=82?= =?UTF-8?q?=E4=BA=86=E3=81=99=E3=82=8B=E6=97=A5=E6=99=82=E3=81=8C=E3=81=9A?= =?UTF-8?q?=E3=82=8C=E3=82=8B=E4=B8=8D=E5=85=B7=E5=90=88=E3=81=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/widgets/editable_calendar_event_sheet.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/front_end/lib/widgets/editable_calendar_event_sheet.dart b/front_end/lib/widgets/editable_calendar_event_sheet.dart index c457658..7307daf 100644 --- a/front_end/lib/widgets/editable_calendar_event_sheet.dart +++ b/front_end/lib/widgets/editable_calendar_event_sheet.dart @@ -104,6 +104,7 @@ class _EditableCalendarEventSheetState if (pickedDate != null) { setState(() { if (isStart) { + final prevEnd = _endDateTime; _startDateTime = DateTime( pickedDate.year, pickedDate.month, @@ -111,6 +112,19 @@ class _EditableCalendarEventSheetState _startDateTime.hour, _startDateTime.minute, ); + // 終了日付が開始とずれていた場合は同日にそろえる(時間は保持) + if (prevEnd != null && + (prevEnd.year != _startDateTime.year || + prevEnd.month != _startDateTime.month || + prevEnd.day != _startDateTime.day)) { + _endDateTime = DateTime( + _startDateTime.year, + _startDateTime.month, + _startDateTime.day, + prevEnd.hour, + prevEnd.minute, + ); + } // 開始日が終了日より後になった場合、終了日を調整 if (_endDateTime != null && _startDateTime.isAfter(_endDateTime!)) { _endDateTime = _startDateTime.add(Duration(hours: 1)); From 655fb995f5bf66fbdf2e8c2aa6896f9f8665b7dc Mon Sep 17 00:00:00 2001 From: otoshigo Date: Sun, 30 Nov 2025 07:53:48 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E4=BB=AE=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E3=81=AE=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front_end/lib/mock/calendar_mock_data.dart | 168 ------------------- front_end/lib/pages/calendar_inbox_page.dart | 48 +++--- 2 files changed, 23 insertions(+), 193 deletions(-) delete mode 100644 front_end/lib/mock/calendar_mock_data.dart diff --git a/front_end/lib/mock/calendar_mock_data.dart b/front_end/lib/mock/calendar_mock_data.dart deleted file mode 100644 index 19119d0..0000000 --- a/front_end/lib/mock/calendar_mock_data.dart +++ /dev/null @@ -1,168 +0,0 @@ -import '../models/calendar_event_proposal.dart'; -import '../models/event_time.dart' as proposal_time; -import '../models/models.dart' as api_models; -import '../models/reminder.dart' as proposal_reminder; - -/// カレンダー追加フローを手元で確認するためのモックデータ。 -/// UI テストやデモで `CalendarInboxProvider.addAll` に流し込む用途を想定。 -class CalendarMockData { - static final List<_EventSeed> _seeds = [ - _EventSeed( - summary: 'プロジェクトキックオフ', - description: '進行方法の合意と役割分担を決定するミーティング', - startDateTime: '2025-12-15T10:00:00+09:00', - endDateTime: '2025-12-15T11:00:00+09:00', - timeZone: 'Asia/Tokyo', - location: 'オンライン (Meet)', - attendees: ['pm@example.com', 'dev@example.com'], - useDefaultReminders: false, - reminders: const [ - ReminderSeed(method: 'popup', minutes: 10), - ReminderSeed(method: 'email', minutes: 60), - ], - ), - _EventSeed( - summary: 'チーム週次スタンドアップ', - description: '進捗共有とブロッカー確認', - startDateTime: '2025-12-17T09:30:00+09:00', - endDateTime: '2025-12-17T10:00:00+09:00', - timeZone: 'Asia/Tokyo', - location: 'A会議室', - attendees: ['lead@example.com'], - useDefaultReminders: true, - reminders: const [], - ), - _EventSeed( - summary: '有休申請', - description: '終日扱いの休暇申請', - startDate: '2025-12-01', - endDate: '2025-12-01', - timeZone: 'Asia/Tokyo', - location: null, - attendees: const [], - useDefaultReminders: false, - reminders: const [ - ReminderSeed(method: 'popup', minutes: 1440), // 前日通知 - ], - ), - ]; - - /// バックエンドレスポンスを模した CalendarEvent 一覧。 - static List get calendarEvents => - _seeds.map((seed) => seed.toCalendarEvent()).toList(); - - /// ボトムシート表示用の CalendarEventProposal 一覧。 - static List get proposals => - _seeds.map((seed) => seed.toProposal()).toList(); - - /// TwoStageResponse にまとめたモックレスポンス。 - static api_models.TwoStageResponse buildTwoStageResponse({ - String summarizedText = '音声入力から抽出されたイベントのサンプルです。', - }) { - return api_models.TwoStageResponse( - summarizedText: summarizedText, - calendarEvents: calendarEvents, - ); - } -} - -class _EventSeed { - final String summary; - final String? description; - final String? startDateTime; - final String? startDate; - final String? endDateTime; - final String? endDate; - final String? timeZone; - final String? location; - final List attendees; - final bool useDefaultReminders; - final List reminders; - - const _EventSeed({ - required this.summary, - required this.description, - this.startDateTime, - this.startDate, - this.endDateTime, - this.endDate, - this.timeZone, - this.location, - this.attendees = const [], - required this.useDefaultReminders, - this.reminders = const [], - }) : assert( - startDateTime != null || startDate != null, - 'startDateTimeまたはstartDateのいずれかは必須です', - ); - - api_models.CalendarEvent toCalendarEvent() { - return api_models.CalendarEvent( - summary: summary, - description: description, - start: api_models.EventTime( - dateTime: startDateTime, - date: startDate, - timeZone: timeZone, - ), - end: api_models.EventTime( - dateTime: endDateTime, - date: endDate, - timeZone: timeZone, - ), - location: location, - attendees: attendees.isNotEmpty ? attendees : null, - reminders: api_models.Reminders( - useDefault: useDefaultReminders, - overrides: reminders.isNotEmpty - ? reminders - .map((r) => api_models.ReminderMethod( - method: r.method, - minutes: r.minutes, - )) - .toList() - : null, - ), - ); - } - - CalendarEventProposal toProposal() { - return CalendarEventProposal( - summary: summary, - description: description, - start: proposal_time.EventTime( - dateTime: startDateTime, - date: startDate, - timeZone: timeZone, - ), - end: proposal_time.EventTime( - dateTime: endDateTime, - date: endDate, - timeZone: timeZone, - ), - location: location, - attendees: attendees.isNotEmpty ? attendees : null, - reminders: proposal_reminder.Reminders( - useDefault: useDefaultReminders, - overrides: reminders.isNotEmpty - ? reminders - .map((r) => proposal_reminder.ReminderMethod( - method: r.method, - minutes: r.minutes, - )) - .toList() - : null, - ), - ); - } -} - -class ReminderSeed { - final String method; - final int minutes; - - const ReminderSeed({ - required this.method, - required this.minutes, - }); -} diff --git a/front_end/lib/pages/calendar_inbox_page.dart b/front_end/lib/pages/calendar_inbox_page.dart index 8702674..342b407 100644 --- a/front_end/lib/pages/calendar_inbox_page.dart +++ b/front_end/lib/pages/calendar_inbox_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/calendar_inbox_provider.dart'; -import '../mock/calendar_mock_data.dart'; import '../services/googleCalendarService.dart'; import '../models/calendar_event_proposal.dart'; import '../widgets/editable_calendar_event_sheet.dart'; @@ -33,23 +32,9 @@ class CalendarInboxPage extends StatelessWidget { builder: (context, inbox, _) { final items = inbox.items; if (items.isEmpty) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('インボックスは空です', style: TextStyle(color: Colors.black54)), - const SizedBox(height: 12), - ElevatedButton( - onPressed: () { - context.read().addAll(CalendarMockData.proposals); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('モックデータを追加しました')), - ); - }, - child: const Text('モックデータを追加'), - ), - ], - ), + return const Center( + child: + Text('インボックスは空です', style: TextStyle(color: Colors.black54)), ); } return ListView.separated( @@ -59,9 +44,13 @@ class CalendarInboxPage extends StatelessWidget { final item = items[index]; final p = item.proposal; return ListTile( - title: Text(p.summary, style: const TextStyle(color: Colors.black87)), + title: Text(p.summary, + style: const TextStyle(color: Colors.black87)), subtitle: Text( - _formatDateTime(p) + (p.location != null && p.location!.isNotEmpty ? ' @ ${p.location}' : ''), + _formatDateTime(p) + + (p.location != null && p.location!.isNotEmpty + ? ' @ ${p.location}' + : ''), style: const TextStyle(color: Colors.black54), ), trailing: Row( @@ -79,10 +68,14 @@ class CalendarInboxPage extends StatelessWidget { uiService: null, onConfirm: (updated) { // 設定のみ保存して閉じる - context.read().updateProposal(item.id, updated); + context + .read() + .updateProposal(item.id, updated); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('設定を保存しました: ${updated.summary}')), + SnackBar( + content: Text( + '設定を保存しました: ${updated.summary}')), ); } }, @@ -99,9 +92,12 @@ class CalendarInboxPage extends StatelessWidget { await svc.createEventFromProposal(p); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('カレンダーに追加しました: ${p.summary}')), + SnackBar( + content: Text('カレンダーに追加しました: ${p.summary}')), ); - context.read().removeById(item.id); + context + .read() + .removeById(item.id); } } catch (e) { if (context.mounted) { @@ -115,7 +111,9 @@ class CalendarInboxPage extends StatelessWidget { ), TextButton( onPressed: () { - context.read().removeById(item.id); + context + .read() + .removeById(item.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('削除しました: ${p.summary}')), );