manager-app/lib/Screens/Notifications/notifications_screen.dart

455 lines
17 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/app_context.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<NotificationsScreen> {
List<PushNotificationDTO> _notifications = [];
bool _loading = true;
String? _statusFilter; // null = toutes
int _page = 0;
static const _pageSize = 15;
// ─── KPI counts ───────────────────────────────────────────
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;
// ─── Filtre + pagination ───────────────────────────────────
List<PushNotificationDTO> get _filtered => _statusFilter == null
? _notifications
: _notifications.where((n) => n.status == _statusFilter).toList();
int get _pageCount => (_filtered.isEmpty ? 1 : (_filtered.length / _pageSize).ceil());
List<PushNotificationDTO> get _paginated =>
_filtered.skip(_page * _pageSize).take(_pageSize).toList();
void _setFilter(String? status) {
setState(() {
_statusFilter = _statusFilter == status ? null : status;
_page = 0;
});
}
// ─── API helpers ──────────────────────────────────────────
Future<void> _load(ManagerAppContext ctx) async {
try {
final response = await ctx.clientAPI!.notificationApi!.notificationGetWithHttpInfo();
if (response.statusCode == 200) {
final List<dynamic> json = jsonDecode(utf8.decode(response.bodyBytes));
setState(() {
_notifications = PushNotificationDTO.listFromJson(json);
_loading = false;
});
}
} catch (e) {
setState(() => _loading = false);
}
}
Future<bool> _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<bool> _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<bool> _cancel(ManagerAppContext ctx, String id) async {
final response = await ctx.clientAPI!.notificationApi!.notificationCancelWithHttpInfo(id);
return response.statusCode == 202;
}
// ─── Dialogs ──────────────────────────────────────────────
void _showSendModal(BuildContext context, ManagerAppContext ctx) {
final titleCtrl = TextEditingController();
final bodyCtrl = TextEditingController();
bool isScheduled = false;
DateTime? scheduledDate;
TimeOfDay? scheduledTime;
showDialog(
context: context,
builder: (_) => StatefulBuilder(builder: (ctx2, setLocal) {
return AlertDialog(
title: const Text('Nouveau message'),
content: SizedBox(
width: 480,
child: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
controller: titleCtrl,
decoration: const InputDecoration(labelText: 'Titre'),
),
const SizedBox(height: 8),
TextField(
controller: bodyCtrl,
decoration: const InputDecoration(labelText: 'Message'),
maxLines: 3,
),
const SizedBox(height: 16),
Row(
children: [
Checkbox(
value: isScheduled,
onChanged: (v) => setLocal(() => isScheduled = v ?? false),
),
const Text('Planifier'),
],
),
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}'
: '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)
: 'Heure'),
onPressed: () async {
final t = await showTimePicker(
context: ctx2,
initialTime: TimeOfDay.now(),
);
if (t != null) setLocal(() => scheduledTime = t);
},
),
),
]),
],
]),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx2),
child: const Text('Annuler'),
),
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: const Text('Envoyer'),
),
],
);
}),
);
}
void _confirmCancel(BuildContext context, ManagerAppContext ctx, PushNotificationDTO notif) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Annuler la notification'),
content: Text('Annuler « ${notif.title} » ?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Non')),
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: const Text('Annuler', style: TextStyle(color: Colors.white)),
),
],
),
);
}
// ─── Helpers ──────────────────────────────────────────────
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,
);
}
// ─── Build ────────────────────────────────────────────────
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final ctx = Provider.of<AppContext>(context, listen: false).getContext() as ManagerAppContext;
_load(ctx);
});
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
final ctx = appContext.getContext() as ManagerAppContext;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Header ──
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Notifications',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: kPrimaryColor)),
ElevatedButton.icon(
onPressed: () => _showSendModal(context, ctx),
icon: const Icon(Icons.add),
label: const Text('Nouveau message'),
),
],
),
const SizedBox(height: 16),
// ── KPIs ──
Row(
children: [
_KpiCard(
label: 'Toutes',
count: _totalCount,
color: Colors.blueGrey,
selected: _statusFilter == null,
onTap: () => _setFilter(null),
),
const SizedBox(width: 12),
_KpiCard(
label: 'Envoyées',
count: _sentCount,
color: Colors.green,
selected: _statusFilter == 'Sent',
onTap: () => _setFilter('Sent'),
),
const SizedBox(width: 12),
_KpiCard(
label: 'Planifiées',
count: _scheduledCount,
color: Colors.blue,
selected: _statusFilter == 'Scheduled',
onTap: () => _setFilter('Scheduled'),
),
const SizedBox(width: 12),
_KpiCard(
label: 'Échouées',
count: _failedCount,
color: Colors.red,
selected: _statusFilter == 'Failed',
onTap: () => _setFilter('Failed'),
),
],
),
const SizedBox(height: 16),
// ── List ──
if (_loading)
const Center(child: CircularProgressIndicator())
else if (_notifications.isEmpty)
const Center(child: Text('Aucune notification envoyée'))
else if (_filtered.isEmpty)
const Center(child: Text('Aucune notification pour ce filtre'))
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: const [
DataColumn(label: Text('Titre', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Topic', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Date', style: TextStyle(fontWeight: FontWeight.w600))),
DataColumn(label: Text('Statut', style: TextStyle(fontWeight: FontWeight.w600))),
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: 'Annuler',
onPressed: () => _confirmCancel(context, ctx, n),
)
: const SizedBox()),
]);
}).toList(),
),
),
),
),
// ── Pagination ──
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(
'${_filtered.length} résultat${_filtered.length > 1 ? 's' : ''}',
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)),
],
),
),
);
}
}