import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/Components/common_loader.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_api_new/api.dart'; import 'package:provider/provider.dart'; class NotificationsScreen extends StatefulWidget { const NotificationsScreen({Key? key}) : super(key: key); @override _NotificationsScreenState createState() => _NotificationsScreenState(); } class _NotificationsScreenState extends State { List _notifications = []; bool _loading = true; String? _statusFilter; int _page = 0; static const _pageSize = 15; int get _totalCount => _notifications.length; int get _sentCount => _notifications.where((n) => n.status == 'Sent').length; int get _scheduledCount => _notifications.where((n) => n.status == 'Scheduled').length; int get _failedCount => _notifications.where((n) => n.status == 'Failed').length; List get _filtered => _statusFilter == null ? _notifications : _notifications.where((n) => n.status == _statusFilter).toList(); int get _pageCount => (_filtered.isEmpty ? 1 : (_filtered.length / _pageSize).ceil()); List get _paginated => _filtered.skip(_page * _pageSize).take(_pageSize).toList(); void _setFilter(String? status) { setState(() { _statusFilter = _statusFilter == status ? null : status; _page = 0; }); } Future _load(ManagerAppContext ctx) async { try { final response = await ctx.clientAPI!.notificationApi!.notificationGetWithHttpInfo(); if (response.statusCode == 200) { final List json = jsonDecode(utf8.decode(response.bodyBytes)); setState(() { _notifications = PushNotificationDTO.listFromJson(json); _loading = false; }); } } catch (e) { setState(() => _loading = false); } } Future _send(ManagerAppContext ctx, String title, String body) async { final response = await ctx.clientAPI!.notificationApi!.notificationSendWithHttpInfo( SendNotificationRequest(title: title, body: body), ); return response.statusCode == 200; } Future _schedule(ManagerAppContext ctx, String title, String body, DateTime scheduledAt) async { final response = await ctx.clientAPI!.notificationApi!.notificationScheduleWithHttpInfo( ScheduleNotificationRequest(title: title, body: body, scheduledAt: scheduledAt), ); return response.statusCode == 200; } Future _cancel(ManagerAppContext ctx, String id) async { final response = await ctx.clientAPI!.notificationApi!.notificationCancelWithHttpInfo(id); return response.statusCode == 202; } void _showSendModal(BuildContext context, ManagerAppContext ctx) { final l = AppLocalizations.of(context)!; final titleCtrl = TextEditingController(); final bodyCtrl = TextEditingController(); bool isScheduled = false; DateTime? scheduledDate; TimeOfDay? scheduledTime; showDialog( context: context, builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) { return AlertDialog( title: Text(l.newMessage), content: SizedBox( width: 480, child: Column(mainAxisSize: MainAxisSize.min, children: [ TextField( controller: titleCtrl, decoration: InputDecoration(labelText: l.messageTitle), ), const SizedBox(height: 8), TextField( controller: bodyCtrl, decoration: InputDecoration(labelText: l.messageBody), maxLines: 3, ), const SizedBox(height: 16), Row( children: [ Checkbox( value: isScheduled, onChanged: (v) => setLocal(() => isScheduled = v ?? false), ), Text(l.schedule), ], ), if (isScheduled) ...[ const SizedBox(height: 8), Row(children: [ Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.calendar_today, size: 16), label: Text(scheduledDate != null ? '${scheduledDate!.day.toString().padLeft(2, '0')}/${scheduledDate!.month.toString().padLeft(2, '0')}/${scheduledDate!.year}' : l.date), onPressed: () async { final d = await showDatePicker( context: ctx2, initialDate: DateTime.now().add(const Duration(hours: 1)), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 365)), ); if (d != null) setLocal(() => scheduledDate = d); }, ), ), const SizedBox(width: 8), Expanded( child: OutlinedButton.icon( icon: const Icon(Icons.access_time, size: 16), label: Text(scheduledTime != null ? scheduledTime!.format(ctx2) : l.time), onPressed: () async { final t = await showTimePicker( context: ctx2, initialTime: TimeOfDay.now(), ); if (t != null) setLocal(() => scheduledTime = t); }, ), ), ]), ], ]), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx2), child: Text(l.cancel), ), ElevatedButton( onPressed: titleCtrl.text.isEmpty ? null : () async { Navigator.pop(ctx2); bool ok; if (isScheduled && scheduledDate != null && scheduledTime != null) { final dt = DateTime( scheduledDate!.year, scheduledDate!.month, scheduledDate!.day, scheduledTime!.hour, scheduledTime!.minute, ); ok = await _schedule(ctx, titleCtrl.text, bodyCtrl.text, dt); } else { ok = await _send(ctx, titleCtrl.text, bodyCtrl.text); } if (ok && context.mounted) { await _load(ctx); } }, child: Text(l.send), ), ], ); }), ); } void _confirmCancel(BuildContext context, ManagerAppContext ctx, PushNotificationDTO notif) { final l = AppLocalizations.of(context)!; showDialog( context: context, builder: (_) => AlertDialog( title: Text(l.cancelNotification), content: Text(l.cancelNotificationConfirm(notif.title ?? '')), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: Text(l.no)), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () async { Navigator.pop(context); final ok = await _cancel(ctx, notif.id!); if (ok && context.mounted) await _load(ctx); }, child: Text(l.cancel, style: const TextStyle(color: Colors.white)), ), ], ), ); } static String _formatDate(DateTime? dt) { if (dt == null) return '—'; final d = dt.toLocal(); return '${d.day.toString().padLeft(2, '0')}/${d.month.toString().padLeft(2, '0')}/${d.year} ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; } static Widget _statusChip(String status) { Color bg; Color fg; switch (status) { case 'Sent': bg = Colors.green.shade50; fg = Colors.green.shade700; break; case 'Scheduled': bg = Colors.blue.shade50; fg = Colors.blue.shade700; break; case 'Failed': bg = Colors.red.shade50; fg = Colors.red.shade700; break; default: bg = Colors.grey.shade100; fg = Colors.grey.shade700; } return Chip( label: Text(status, style: TextStyle(color: fg, fontSize: 12)), backgroundColor: bg, side: BorderSide(color: fg.withValues(alpha: 0.3)), padding: const EdgeInsets.symmetric(horizontal: 4), visualDensity: VisualDensity.compact, ); } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final ctx = Provider.of(context, listen: false).getContext() as ManagerAppContext; _load(ctx); }); } @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; final appContext = Provider.of(context); final ctx = appContext.getContext() as ManagerAppContext; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l.notificationsTitle, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: kPrimaryColor)), ElevatedButton.icon( onPressed: () => _showSendModal(context, ctx), icon: const Icon(Icons.add), label: Text(l.newMessage), ), ], ), const SizedBox(height: 16), Row( children: [ _KpiCard( label: l.allNotifications, count: _totalCount, color: Colors.blueGrey, selected: _statusFilter == null, onTap: () => _setFilter(null), ), const SizedBox(width: 12), _KpiCard( label: l.sentNotifications, count: _sentCount, color: Colors.green, selected: _statusFilter == 'Sent', onTap: () => _setFilter('Sent'), ), const SizedBox(width: 12), _KpiCard( label: l.scheduledNotifications, count: _scheduledCount, color: Colors.blue, selected: _statusFilter == 'Scheduled', onTap: () => _setFilter('Scheduled'), ), const SizedBox(width: 12), _KpiCard( label: l.failedNotifications, count: _failedCount, color: Colors.red, selected: _statusFilter == 'Failed', onTap: () => _setFilter('Failed'), ), ], ), const SizedBox(height: 16), if (_loading) const CommonLoader() else if (_notifications.isEmpty) Center(child: Text(l.noNotifications)) else if (_filtered.isEmpty) Center(child: Text(l.noNotificationsForFilter)) else Expanded( child: Card( elevation: 0, color: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide(color: Colors.grey.shade200), ), clipBehavior: Clip.antiAlias, child: Column( children: [ Expanded( child: SingleChildScrollView( child: SizedBox( width: double.infinity, child: DataTable( horizontalMargin: 16, columnSpacing: 24, headingRowColor: WidgetStateProperty.all(Colors.grey.shade50), dividerThickness: 1, columns: [ DataColumn(label: Text(l.messageTitle, style: const TextStyle(fontWeight: FontWeight.w600))), DataColumn(label: Text(l.topic, style: const TextStyle(fontWeight: FontWeight.w600))), DataColumn(label: Text(l.date, style: const TextStyle(fontWeight: FontWeight.w600))), DataColumn(label: Text(l.status, style: const TextStyle(fontWeight: FontWeight.w600))), const DataColumn(label: Text('')), ], rows: _paginated.map((n) { final isScheduled = n.status == 'Scheduled'; final date = isScheduled ? n.scheduledAt : n.sentAt; return DataRow(cells: [ DataCell(Text(n.title ?? '')), DataCell(Text(n.topic ?? '', style: const TextStyle(color: Colors.grey))), DataCell(Text(_formatDate(date), style: const TextStyle(color: Colors.grey))), DataCell(_statusChip(n.status ?? '')), DataCell(isScheduled ? IconButton( icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20), tooltip: l.tooltipCancelNotification, onPressed: () => _confirmCancel(context, ctx, n), ) : const SizedBox()), ]); }).toList(), ), ), ), ), Container( decoration: BoxDecoration( border: Border(top: BorderSide(color: Colors.grey.shade200)), ), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.chevron_left), onPressed: _page > 0 ? () => setState(() => _page--) : null, ), Text('${_page + 1} / $_pageCount', style: const TextStyle(fontSize: 13)), IconButton( icon: const Icon(Icons.chevron_right), onPressed: _page < _pageCount - 1 ? () => setState(() => _page++) : null, ), const SizedBox(width: 16), Text( l.resultsCount(_filtered.length), style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], ), ), ], ), ), ), ], ); } } class _KpiCard extends StatelessWidget { final String label; final int count; final MaterialColor color; final bool selected; final VoidCallback onTap; const _KpiCard({ required this.label, required this.count, required this.color, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: 120, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: selected ? color.shade100 : color.shade50, borderRadius: BorderRadius.circular(8), border: Border.all( color: selected ? color.shade400 : color.shade200, width: selected ? 2 : 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('$count', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: color.shade700)), const SizedBox(height: 2), Text(label, style: TextStyle(fontSize: 12, color: color.shade600)), ], ), ), ); } }