didvan-app/lib/views/notification_time/notification_time_state.dart

178 lines
5.9 KiB
Dart

// lib/views/notification_time/notification_time_state.dart
//
// State for the per-user push-notification schedule preference.
//
// Backend: `GET /notification-preferences` and `PATCH /notification-preferences`
// on api2.didvan.com. The server stores:
// { enabled, deliverAtHour, deliverAtMinute, timezone }
// and, when enabled=true, delays content pushes until the next occurrence of
// deliverAtHour:deliverAtMinute in the user's timezone. When enabled=false
// (default), pushes fire at the admin-picked time on the content, immediately.
//
// UI contract:
// - selectedTime: current chosen time (backed by deliverAtHour/deliverAtMinute).
// - enabled: on/off switch. Off means "use admin's time".
// - When enabled=false the time picker is disabled in the UI.
// - Saving pushes {enabled, deliverAtHour, deliverAtMinute} — timezone is
// left server-side (defaults to Asia/Tehran).
import 'dart:async';
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../models/day_time.dart';
import '../../models/view/alert_data.dart';
import '../../services/network/request.dart';
import '../../services/network/request_helper.dart';
import '../../utils/action_sheet.dart';
import '../../utils/date_time.dart';
const _timeout = Duration(seconds: 10);
class NotificationTimeState extends CoreProvier {
/// On/off switch. When false (default), notifications fire at the admin's
/// picked time; when true, they wait for `selectedTime` in the user's TZ.
bool enabled = false;
/// The time the user wants pushes to arrive when `enabled == true`.
/// Default 21:00 matches the backend's own default for a fresh preference.
DayTime selectedTime = DateTimeUtils.handleDayTime('21:00');
bool fromFav = false;
late void Function() onTimeChanged = () {};
/// Legacy field kept for the widgets that still read it. Semantically it
/// now mirrors `!enabled` — "anytime" (دریافت آنی) means the user did NOT
/// pick a custom time, so pushes fire at the admin's time. When `true`,
/// `enabled` on the new API is `false`, and vice versa.
bool get isAnytime => !enabled;
set isAnytime(bool value) {
enabled = !value;
}
Future<void> getTime() async {
appState = AppState.busy;
try {
final service = RequestService(RequestHelper.notificationPreference());
await service.httpGet().timeout(_timeout);
if (service.isSuccess) {
final int hour = _asInt(service.data('deliverAtHour'), 21);
final int minute = _asInt(service.data('deliverAtMinute'), 0);
enabled = _asBool(service.data('enabled'), false);
selectedTime = _dayTimeFromHM(hour, minute);
} else {
debugPrint('⚠️ getPreference: status=${service.statusCode}');
_loadDefault();
}
} on TimeoutException {
debugPrint('⏱️ getPreference timeout');
_loadDefault();
} catch (e) {
debugPrint('❌ getPreference failed: $e');
_loadDefault();
}
appState = AppState.idle;
update();
}
void _loadDefault() {
enabled = false;
selectedTime = DateTimeUtils.handleDayTime('21:00');
}
/// Convert DayTime (hour string + AM/PM) → 24-hour int.
int _selectedHour24() {
int hour24 = int.tryParse(selectedTime.hour) ?? 12;
if (selectedTime.meridiem == Meridiem.PM && hour24 != 12) hour24 += 12;
if (selectedTime.meridiem == Meridiem.AM && hour24 == 12) hour24 = 0;
return hour24;
}
int _selectedMinute() => int.tryParse(selectedTime.minute) ?? 0;
DayTime _dayTimeFromHM(int hour24, int minute) {
final meridiem = hour24 >= 12 ? Meridiem.PM : Meridiem.AM;
int display = hour24 % 12;
if (display == 0) display = 12;
return DayTime(
hour: display.toString(),
minute: minute.toString().padLeft(2, '0'),
meridiem: meridiem,
);
}
Future<void> putTime(BuildContext context) async {
appState = AppState.busy;
final body = {
'enabled': enabled,
'deliverAtHour': _selectedHour24(),
'deliverAtMinute': _selectedMinute(),
};
final service = RequestService(
RequestHelper.notificationPreference(),
body: body,
);
try {
// The generic RequestService exposes `.put()` for updates; for a PATCH
// semantic the server accepts either — the endpoint is idempotent on
// the (enabled, hour, minute) triple.
await service.put().timeout(_timeout);
if (service.isSuccess) {
onTimeChanged();
Future.delayed(Duration.zero, () {
if (fromFav) {
Navigator.of(context).pop();
Navigator.of(context).pop();
} else {
Navigator.of(context).pop();
}
});
Future.delayed(
Duration.zero,
() => ActionSheetUtils(context).showAlert(AlertData(
message: 'تغییرات شما با موفقیت اعمال شد.',
aLertType: ALertType.success,
)),
);
appState = AppState.idle;
return;
}
ActionSheetUtils(context).showAlert(AlertData(
message: service.errorMessage,
));
} catch (e) {
debugPrint('❌ putPreference failed: $e');
ActionSheetUtils(context).showAlert(AlertData(
message: 'ذخیره‌ی تنظیمات اعلان با خطا مواجه شد.',
));
}
appState = AppState.failed;
}
static int _asInt(dynamic v, int fallback) {
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? fallback;
return fallback;
}
static bool _asBool(dynamic v, bool fallback) {
if (v is bool) return v;
if (v is String) {
final s = v.toLowerCase();
if (s == 'true' || s == '1') return true;
if (s == 'false' || s == '0') return false;
}
if (v is num) return v != 0;
return fallback;
}
}