Neda/admin_panel/lib/screens/notifications_screen.dart

493 lines
15 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/notification_model.dart';
import '../providers/notification_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/app_sidebar.dart';
import '../widgets/responsive_layout.dart';
class NotificationsScreen extends StatelessWidget {
const NotificationsScreen({super.key});
@override
Widget build(BuildContext context) {
return const ResponsiveLayout(
title: 'اعلان‌ها',
sidebar: AppSidebar(),
body: _NotificationsBody(),
);
}
}
class _NotificationsBody extends StatefulWidget {
const _NotificationsBody();
@override
State<_NotificationsBody> createState() => _NotificationsBodyState();
}
class _NotificationsBodyState extends State<_NotificationsBody> {
final _searchCtrl = TextEditingController();
String _search = '';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<NotificationProvider>().loadNotifications();
});
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Header ────────────────────────────────────────────────
Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'اعلان‌های سیستم',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 4),
Text(
'مشاهده اعلان‌ها و ارسال اعلان عمومی',
style: TextStyle(
fontSize: 14, color: AppTheme.textSecondary),
),
],
),
),
ElevatedButton.icon(
onPressed: () => _showBroadcastDialog(context),
icon: const Icon(Icons.campaign_rounded, size: 18),
label: const Text('ارسال اعلان عمومی'),
),
],
),
const SizedBox(height: 20),
// ── Search ───────────────────────────────────────────────
TextField(
controller: _searchCtrl,
onChanged: (v) => setState(() => _search = v.toLowerCase()),
decoration: const InputDecoration(
hintText: 'جستجوی اعلان...',
prefixIcon: Icon(Icons.search_rounded),
),
),
const SizedBox(height: 16),
// ── Table ────────────────────────────────────────────────
Expanded(
child: Consumer<NotificationProvider>(
builder: (_, provider, __) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.status == NotificationLoadStatus.error) {
return _ErrorView(
message: provider.error ?? 'خطا',
onRetry: () => provider.loadNotifications(),
);
}
final filtered = provider.items.where((n) {
if (_search.isEmpty) return true;
final text = [
n.title,
n.description ?? '',
n.receiverId,
n.senderId ?? '',
].join(' ').toLowerCase();
return text.contains(_search);
}).toList();
if (filtered.isEmpty) {
return const _EmptyView(
icon: Icons.notifications_none_rounded,
message: 'اعلانی یافت نشد',
);
}
return _NotificationsTable(items: filtered);
},
),
),
],
),
);
}
void _showBroadcastDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => const _BroadcastDialog(),
);
}
}
// ── Broadcast Dialog ─────────────────────────────────────────────────────────
class _BroadcastDialog extends StatefulWidget {
const _BroadcastDialog();
@override
State<_BroadcastDialog> createState() => _BroadcastDialogState();
}
class _BroadcastDialogState extends State<_BroadcastDialog> {
final _formKey = GlobalKey<FormState>();
final _titleCtrl = TextEditingController();
final _descCtrl = TextEditingController();
bool _loading = false;
String? _error;
bool _sent = false;
@override
void dispose() {
_titleCtrl.dispose();
_descCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
final title = _titleCtrl.text.trim();
final desc = _descCtrl.text.trim();
final provider = context.read<NotificationProvider>();
final success = await provider.sendPublic(title, desc);
if (!mounted) return;
setState(() => _loading = false);
if (success) {
setState(() => _sent = true);
} else {
setState(() => _error = provider.error ?? 'خطا در ارسال اعلان');
}
}
@override
Widget build(BuildContext context) {
if (_sent) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.check_circle_rounded, color: AppTheme.success),
SizedBox(width: 10),
Text('اعلان ارسال شد'),
],
),
content: const Text('اعلان عمومی با موفقیت به همه کاربران ارسال شد.'),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('بستن'),
),
],
);
}
return AlertDialog(
title: const Row(
children: [
Icon(Icons.campaign_rounded, color: AppTheme.primary),
SizedBox(width: 10),
Text('ارسال اعلان عمومی'),
],
),
content: SizedBox(
width: 440,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _titleCtrl,
decoration: const InputDecoration(
labelText: 'عنوان اعلان',
prefixIcon: Icon(Icons.title_rounded),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'عنوان الزامی است' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _descCtrl,
maxLines: 4,
decoration: const InputDecoration(
labelText: 'متن اعلان (اختیاری)',
prefixIcon: Icon(Icons.description_outlined),
alignLabelWithHint: true,
),
),
if (_error != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.danger.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.danger.withValues(alpha: 0.3)),
),
child: Text(
_error!,
style: const TextStyle(
color: AppTheme.danger, fontSize: 13),
),
),
],
],
),
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('انصراف'),
),
ElevatedButton.icon(
onPressed: _loading ? null : _submit,
icon: _loading
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: const Icon(Icons.send_rounded, size: 16),
label: const Text('ارسال به همه'),
),
],
);
}
}
// ── Notifications Table ─────────────────────────────────────────────────────
class _NotificationsTable extends StatelessWidget {
final List<NotificationModel> items;
const _NotificationsTable({required this.items});
@override
Widget build(BuildContext context) {
return Card(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: const [
DataColumn(label: Text('عنوان')),
DataColumn(label: Text('نوع')),
DataColumn(label: Text('گیرنده')),
DataColumn(label: Text('وضعیت')),
],
rows: items.map((n) => _buildRow(n)).toList(),
),
),
),
),
);
}
DataRow _buildRow(NotificationModel n) {
return DataRow(
cells: [
DataCell(
SizedBox(
width: 280,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
n.title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
if ((n.description ?? '').isNotEmpty) ...[
const SizedBox(height: 4),
Text(
n.description!,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
),
),
],
if (n.groupId != null) ...[
const SizedBox(height: 4),
Text(
'گروه: ${n.groupId}',
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 11,
),
),
],
],
),
),
),
DataCell(_TypeBadge(type: n.type)),
DataCell(
Text(
n.receiverId,
style: const TextStyle(color: AppTheme.textSecondary),
),
),
DataCell(_StatusBadge(type: n.type, isAccepted: n.isAccepted)),
],
);
}
}
class _TypeBadge extends StatelessWidget {
final NotificationType type;
const _TypeBadge({required this.type});
@override
Widget build(BuildContext context) {
final color = type == NotificationType.public
? AppTheme.primary
: const Color(0xFF0891B2);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Text(
type.label,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _StatusBadge extends StatelessWidget {
final NotificationType type;
final bool? isAccepted;
const _StatusBadge({required this.type, required this.isAccepted});
@override
Widget build(BuildContext context) {
String label;
Color color;
if (type == NotificationType.public) {
label = 'ارسال شد';
color = AppTheme.success;
} else {
if (isAccepted == true) {
label = 'تأیید شد';
color = AppTheme.success;
} else if (isAccepted == false) {
label = 'رد شد';
color = AppTheme.danger;
} else {
label = 'در انتظار';
color = AppTheme.warning;
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Text(
label,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _EmptyView extends StatelessWidget {
final IconData icon;
final String message;
const _EmptyView({required this.icon, required this.message});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 64, color: AppTheme.border),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(color: AppTheme.textSecondary, fontSize: 16),
),
],
),
);
}
}
class _ErrorView extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorView({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline_rounded,
size: 64, color: AppTheme.danger),
const SizedBox(height: 16),
Text(message, style: const TextStyle(color: AppTheme.textSecondary)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh_rounded),
label: const Text('تلاش مجدد'),
),
],
),
);
}
}