Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b60763c
feat: PaywallSource にピルシート終了ダイアログ用の2 source を追加
bannzai Jun 25, 2026
60cab83
feat: Remote Config に endedPillSheetDialogVariant を追加
bannzai Jun 25, 2026
9fa8b7e
refactor: missedPillDays の集合構築を分解し scheduledPillDays を追加
bannzai Jun 25, 2026
dc8ad8f
feat: ピルシート終了ダイアログの表示済みフラグ用 SharedPreferences キーを追加
bannzai Jun 25, 2026
6007ce2
feat: ピルシート終了時の課金転換ダイアログ(A/B)を追加
bannzai Jun 25, 2026
9ab08dc
feat: 終了ピルシートの free ユーザーに課金転換ダイアログを表示するトリガーを追加
bannzai Jun 25, 2026
d615da4
fix: ピルシート終了ダイアログの服用記録集計を修正
bannzai Jul 2, 2026
72c0c2e
fix: 終了ダイアログの表示済みフラグ保存を await し失敗をログする
bannzai Jul 2, 2026
723748e
style: Widget Test のグループ名に # プレフィックスを付ける
bannzai Jul 2, 2026
871b319
:shell: dart format ./
bannzai Jul 9, 2026
5ab5553
Merge origin/main into issue-385
bannzai Jul 9, 2026
2fa54c4
fix: 服用記録集計で .date() を使い、取り消し(revertTakenPill)を反映する
bannzai Jul 9, 2026
9521678
fix: 終了ダイアログの集計を実薬の服用予定日ベースにする
bannzai Jul 9, 2026
69b7e37
fix: 集計ティーザーで履歴ロード完了を待ち、対象グループの履歴に絞る
bannzai Jul 9, 2026
3edc953
fix: blur した履歴行を ExcludeSemantics で覆う
bannzai Jul 9, 2026
da16439
fix: 履歴がTTL切れで空の場合は集計メッセージを表示しない
bannzai Jul 9, 2026
07fd640
test: missedPillDays の revertTakenPill 反映を直接検証するテストを追加
bannzai Jul 9, 2026
07a7cec
test: missedPillDays の挙動変更に対する境界値テストを追加
bannzai Jul 9, 2026
5ab0993
fix: レビューコメント対応 - 終了ダイアログの対象絞り込みと集計抑止を強化
bannzai Jul 11, 2026
feee1ca
fix: 集計を提示できない場合は終了ダイアログ(summary_stats)の表示自体を抑止
bannzai Jul 11, 2026
5dc4a44
fix: レビューコメント対応 - まとめ記録の集計反映・起動時モーダル排他・履歴ロード中の空ティーザー抑止
bannzai Jul 11, 2026
97cceb7
fix: レビューコメント対応 - 記録対象日の導出を共通化し編集済み履歴・過去日記録を反映
bannzai Jul 11, 2026
a236956
fix: レビューコメント対応 - 取り消し・編集・同日複数回記録の集計とティーザー表示を精緻化
bannzai Jul 11, 2026
7a06139
Merge remote-tracking branch 'origin/main' into issue-385
bannzai Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 98 additions & 18 deletions lib/entity/pill_sheet_modified_history.codegen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -303,44 +303,113 @@ abstract class PillSheetModifiedHistoryServiceActionFactory {
}
}

/// 渡されたPillSheetModifiedHistory配列から飲み忘れ日数を計算する
int missedPillDays({
/// takenPill / automaticallyRecordedLastTakenDate の履歴が記録対象としたピル日付
/// (before/after の lastTakenDate 差分。日付のみに正規化)を返す。
/// まとめ記録では1回の操作で複数日分のピルが記録され、履歴の日時は操作時刻になるため、
/// 操作日ではなく記録対象日 = (記録前の最終服用日, 記録後の最終服用日] を展開する。
/// 記録前の最終服用日が無い(初回記録)場合はシート開始日から展開する。
/// 2錠飲み等で同じピル日に2回目を記録した履歴は lastTakenDate が変わらないため、その日を返す。
/// グループ情報の無い過去の履歴は記録対象日を特定できないため履歴日時を記録日とみなす。
List<DateTime> _takenPillLastTakenRangeDates(PillSheetModifiedHistory history) {
final afterPillSheet = history.afterPillSheetGroup?.lastTakenPillSheetOrFirstPillSheet;
final afterLastTakenDate = afterPillSheet?.lastTakenDate;
if (afterPillSheet == null || afterLastTakenDate == null) {
return [history.estimatedEventCausingDate.date()];
}
final exclusiveFloorDate =
(history.beforePillSheetGroup?.lastTakenPillSheetOrFirstPillSheet.lastTakenDate ?? afterPillSheet.beginDate.subtract(const Duration(days: 1)))
.date();
final rangeDates = [
for (var takenDate = afterLastTakenDate.date(); takenDate.isAfter(exclusiveFloorDate); takenDate = takenDate.subtract(const Duration(days: 1)))
takenDate,
Comment thread
bannzai marked this conversation as resolved.
];
if (rangeDates.isEmpty) {
return [afterLastTakenDate.date()];
}
return rangeDates;
}

/// takenPill / automaticallyRecordedLastTakenDate の履歴が服用記録日として集計される日付(日付のみに正規化)を返す。
/// 基本は記録対象のピル日付([_takenPillLastTakenRangeDates])。
/// 服用日時が編集された履歴は before/after の lastTakenDate が編集前のままのため、編集後の履歴日時を記録日とみなす。
List<DateTime> takenPillHistoryTargetDates(PillSheetModifiedHistory history) {
if (history.value.takenPill?.edited != null) {
return [history.estimatedEventCausingDate.date()];
Comment on lines +336 to +337

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 編集履歴の操作順で取り消しを反映してください

新たな根拠は、現在のコードが編集後の服用日時を estimatedEventCausingDate にして日付順処理するため、編集後日時が取り消し操作より後だと revert が先に処理される点です。服用日時を翌日などへ編集してからすぐ取り消すと、revert 時点では editedTakenDatesByOriginalDate が未登録で編集後日付を消せず、その後この分岐で取り消し済みの日付が takenDates に追加され、summary/history teaser に服用済みとして残ります。操作順(createdAt など)で取り消しを処理するか、revert 後にも編集済み対応を除去してください。

Useful? React with 👍 / 👎.

}
return _takenPillLastTakenRangeDates(history);
}

/// 渡されたPillSheetModifiedHistory配列から、服用予定日(集計期間の全日 − 服用お休み日)と
/// 服用記録のあった日の集合を構築する。集計対象が無い場合は null を返す。
/// 日付はすべて日付のみ(午前0時)に正規化して集合化する。時刻付きのまま差分すると服用日が一致せず記録が漏れるため。
/// [beginDate] を渡すと集計開始日として使い(シート開始日など)、省略時は最古の履歴日を開始日とする。
({Set<DateTime> scheduledDates, Set<DateTime> takenDates})? pillTakenDateSets({
required List<PillSheetModifiedHistory> histories,
required DateTime maxDate,
DateTime? beginDate,
}) {
if (histories.isEmpty) {
return 0;
}

// 昇順に並べ替える。服用お休み期間の集計時に、服用お休みが開始された後の差分の日付を集計するために順番を整える必要がある
final orderedHistories = histories.sortedBy(
(history) => history.estimatedEventCausingDate,
);

final minDate = orderedHistories.map((history) => history.estimatedEventCausingDate).reduce((a, b) => a.isBefore(b) ? a : b);
// 集計開始日: beginDate 指定があればそれを、なければ最古の履歴日を使う。時刻を持つと日付集合の差分がずれるため日付のみに正規化する
final DateTime minDate;
if (beginDate != null) {
minDate = beginDate.date();
} else {
if (orderedHistories.isEmpty) {
return null;
}
minDate = orderedHistories.first.estimatedEventCausingDate.date();
}
final normalizedMaxDate = maxDate.date();

final allDates = <DateTime>{};
final days = daysBetween(minDate, maxDate);
final days = daysBetween(minDate, normalizedMaxDate);
for (var i = 0; i < days; i++) {
allDates.add(minDate.add(Duration(days: i)));
}

// takenPill || automaticallyRecordedLastTakenDate アクションの日付を収集
final takenDates = <DateTime>{};
// 服用日時が編集された履歴は編集後の日付で takenDates に入る一方、取り消しは元のピル日付を対象とするため、
// 元のピル日付 → 編集後の記録日の対応を保持して取り消し時に編集後の日付も除外できるようにする
final editedTakenDatesByOriginalDate = <DateTime, List<DateTime>>{};
// beganRestDuration から endedRestDuration の間の日付を収集
final restDurationDates = <DateTime>{};

DateTime? historyBeginRestDurationDate;
for (final history in orderedHistories) {
// estimatedEventCausingDateの日付部分のみを使用
final date = DateTime(
history.estimatedEventCausingDate.year,
history.estimatedEventCausingDate.month,
history.estimatedEventCausingDate.day,
);
final date = history.estimatedEventCausingDate.date();
if (history.actionType == PillSheetModifiedActionType.takenPill.name ||
history.actionType == PillSheetModifiedActionType.automaticallyRecordedLastTakenDate.name) {
takenDates.add(date);
final targetDates = takenPillHistoryTargetDates(history);
takenDates.addAll(targetDates);
if (history.value.takenPill?.edited != null) {
for (final originalDate in _takenPillLastTakenRangeDates(history)) {
editedTakenDatesByOriginalDate.putIfAbsent(originalDate, () => []).addAll(targetDates);
}
}
}

// 服用記録の取り消しを反映する。取り消された日 = (取り消し後の最終服用日, 取り消し前の最終服用日] の範囲。
// 時系列順に処理しているため、取り消し後に再記録された日は後続の takenPill で再度追加される
if (history.actionType == PillSheetModifiedActionType.revertTakenPill.name) {
final beforeLastTakenDate = history.beforePillSheetGroup?.lastTakenPillSheetOrFirstPillSheet.lastTakenDate;
if (beforeLastTakenDate != null) {
final afterLastTakenDate = history.afterPillSheetGroup?.lastTakenPillSheetOrFirstPillSheet.lastTakenDate;
bool isRevertedDate(DateTime takenDate) =>
(afterLastTakenDate == null || takenDate.isAfter(afterLastTakenDate.date())) && !takenDate.isAfter(beforeLastTakenDate.date());
takenDates.removeWhere(isRevertedDate);
// 編集済み履歴の記録は編集後の日付で takenDates に入っているため、元のピル日付が取り消し範囲なら編集後の日付も除外する
for (final entry in editedTakenDatesByOriginalDate.entries) {
if (isRevertedDate(entry.key)) {
entry.value.forEach(takenDates.remove);
}
}
}
}

// 服用お休み中は記録されないので集計から除外
Expand All @@ -364,14 +433,25 @@ int missedPillDays({

// 現在まで服用お休み中の場合には、差分の日付をrestDurationDatesに追加する
if (historyBeginRestDurationDate != null) {
for (var i = 0; i < daysBetween(historyBeginRestDurationDate, maxDate); i++) {
for (var i = 0; i < daysBetween(historyBeginRestDurationDate, normalizedMaxDate); i++) {
restDurationDates.add(
historyBeginRestDurationDate.add(Duration(days: i)),
);
}
}

// 服用記録がない日数を計算
final missedDays = allDates.difference(takenDates).difference(restDurationDates).length;
return missedDays;
return (scheduledDates: allDates.difference(restDurationDates), takenDates: takenDates);
}

/// 渡されたPillSheetModifiedHistory配列から飲み忘れ日数を計算する
int missedPillDays({
required List<PillSheetModifiedHistory> histories,
required DateTime maxDate,
}) {
final sets = pillTakenDateSets(histories: histories, maxDate: maxDate);
if (sets == null) {
return 0;
}
// 服用予定日のうち服用記録がない日数を計算
return sets.scheduledDates.difference(sets.takenDates).length;
}
10 changes: 10 additions & 0 deletions lib/entity/remote_config_parameter.codegen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ abstract class RemoteConfigKeys {

/// 買い切りオファーの訴求コピーのA/Bテストバリアント識別子キー
static const lifetimeOfferCopyVariant = 'lifetimeOfferCopyVariant';

/// ピルシート終了ダイアログの A/B バリアント識別子キー
static const endedPillSheetDialogVariant = 'endedPillSheetDialogVariant';
}

/// Remote Configパラメータのデフォルト値定義
Expand Down Expand Up @@ -112,6 +115,9 @@ abstract class RemoteConfigParameterDefaultValues {
// default(A) or ownership(B) ...
/// 買い切りオファーの訴求コピーバリアントのデフォルト値(A/Bテスト用)
static const lifetimeOfferCopyVariant = 'default';

/// ピルシート終了ダイアログのバリアント(空文字 = 実験未参加 / 非表示)
static const endedPillSheetDialogVariant = '';
}

// [RemoteConfigDefaultValues] でgrepした場所に全て設定する
Expand Down Expand Up @@ -210,6 +216,10 @@ class RemoteConfigParameter with _$RemoteConfigParameter {
/// 買い切りオファーの訴求コピーのA/Bテストバリアント識別子
/// バー・オファー画面の文言を切り替える('default', 'ownership'等)
@Default(RemoteConfigParameterDefaultValues.lifetimeOfferCopyVariant) String lifetimeOfferCopyVariant,

/// ピルシート終了ダイアログの A/B バリアント識別子
/// 'history_blur' / 'summary_stats' / '' (非表示)。Firebase A/B Testing で配信する
@Default(RemoteConfigParameterDefaultValues.endedPillSheetDialogVariant) String endedPillSheetDialogVariant,
}) = _RemoteConfigParameter;
RemoteConfigParameter._();
factory RemoteConfigParameter.fromJson(Map<String, dynamic> json) => _$RemoteConfigParameterFromJson(json);
Expand Down
46 changes: 39 additions & 7 deletions lib/entity/remote_config_parameter.codegen.freezed.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading