// 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 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 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; } }