1511 lines
63 KiB
Dart
1511 lines
63 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import '../../data/models/directorate_models.dart';
|
|
import '../../services/directorate_api_service.dart';
|
|
|
|
class SchoolPrincipalScreen extends StatefulWidget {
|
|
const SchoolPrincipalScreen({super.key});
|
|
|
|
@override
|
|
State<SchoolPrincipalScreen> createState() => _SchoolPrincipalScreenState();
|
|
}
|
|
|
|
class _SchoolPrincipalScreenState extends State<SchoolPrincipalScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
late TabController _tabController;
|
|
bool _isLoading = true;
|
|
Map<String, dynamic>? _dashboardData;
|
|
|
|
// Recording State
|
|
bool _isRecording = false;
|
|
int _recordDurationSeconds = 0;
|
|
Timer? _recordTimer;
|
|
double _recordedSizeMb = 0.0;
|
|
int? _selectedTeacherId;
|
|
String? _selectedTeacher;
|
|
String _selectedSubject = '';
|
|
final TextEditingController _lessonTitleController =
|
|
TextEditingController();
|
|
RecordedLessonResult? _lastResult;
|
|
|
|
// Exam state
|
|
bool _examPushedToLab = false;
|
|
bool _isPanoramicActive = false;
|
|
double _panoramicSizeMb = 16.5;
|
|
|
|
// Enterprise Integrity, Parent Reports & Roster State
|
|
bool _isRunningIntegrityAudit = false;
|
|
Map<String, dynamic>? _integrityResult;
|
|
bool _isDispatchingParentReports = false;
|
|
bool _isImportingRoster = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tabController = TabController(length: 3, vsync: this);
|
|
_loadData();
|
|
}
|
|
|
|
Future<void> _loadData() async {
|
|
setState(() => _isLoading = true);
|
|
final data = await DirectorateApiService.fetchSchoolDashboard();
|
|
if (mounted) {
|
|
setState(() {
|
|
_dashboardData = data;
|
|
final teachers = data['teachers'] as List? ?? const [];
|
|
if (teachers.isNotEmpty) {
|
|
final first = Map<String, dynamic>.from(teachers.first as Map);
|
|
_selectedTeacherId = (first['id'] as num?)?.toInt();
|
|
_selectedTeacher = first['name']?.toString();
|
|
_selectedSubject = first['subject']?.toString() ?? '';
|
|
}
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _selectAndUploadRecording() async {
|
|
if (_selectedTeacherId == null || _lessonTitleController.text.trim().isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('اختر معلماً وأدخل عنوان الحصة أولاً.')));
|
|
return;
|
|
}
|
|
final selection = await FilePicker.platform.pickFiles(
|
|
type: FileType.custom,
|
|
allowedExtensions: const ['mp4', 'mov', 'webm'],
|
|
withData: true,
|
|
);
|
|
if (selection == null || selection.files.isEmpty) return;
|
|
final file = selection.files.single;
|
|
setState(() {
|
|
_isRecording = true;
|
|
_recordedSizeMb = file.size / 1048576;
|
|
_lastResult = null;
|
|
});
|
|
|
|
// Show quick processing dialog
|
|
showCupertinoDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) => const Center(
|
|
child: CupertinoActivityIndicator(radius: 20),
|
|
),
|
|
);
|
|
|
|
try {
|
|
final result = await DirectorateApiService.recordLesson(
|
|
schoolId: (_dashboardData?['school']?['id'] as num?)?.toInt() ?? 0,
|
|
teacherId: _selectedTeacherId!,
|
|
subject: _selectedSubject,
|
|
gradeLevel: 'grade_10',
|
|
lessonTitle: _lessonTitleController.text.trim(),
|
|
durationMinutes: 0,
|
|
filePath: file.path,
|
|
fileBytes: file.bytes,
|
|
fileName: file.name,
|
|
);
|
|
if (mounted) setState(() => _lastResult = result);
|
|
} catch (e) {
|
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
|
} finally {
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
setState(() => _isRecording = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showDualFormsPreview() async {
|
|
await DirectorateApiService.fetchDualForms();
|
|
if (!mounted) return;
|
|
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (ctx) => Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Container(
|
|
height: MediaQuery.of(context).size.height * 0.8,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
border: Border(top: BorderSide(color: Color(0xFF8B5CF6), width: 2)),
|
|
),
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF334155),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'معاينة النماذج الموحدة (نموذج أ ونموذج ب)',
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(CupertinoIcons.xmark, color: Colors.white70),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1E1B4B),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: const Color(0xFF8B5CF6).withOpacity(0.3)),
|
|
),
|
|
child: const Text(
|
|
'المخطط الهجين: 70% أسئلة موضوعية مبرمجة على محطات الحاسوب + 30% خطوات إنشائية ورقية مطبوعة مع باركود تسلسلي مشفر.',
|
|
style: TextStyle(fontSize: 12, color: Color(0xFFC4B5FD), height: 1.4),
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Expanded(
|
|
child: Row(
|
|
children: [
|
|
// Form A Card
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161F30),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFF10B981).withOpacity(0.5)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(CupertinoIcons.doc_checkmark_fill, color: Color(0xFF10B981), size: 16),
|
|
SizedBox(width: 6),
|
|
Text('النموذج (أ) — الفا', style: TextStyle(fontWeight: FontWeight.w800, color: Color(0xFF34D399), fontSize: 13)),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text('الباركود التسلسلي:\nSAQEL-EXAM-A-ALPHA', style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
|
const Divider(color: Color(0xFF1E293B)),
|
|
const Text('• 20 مسألة موضوعية (70 علامة)\n• مسألتان إنشائيتان (30 علامة)\n• باركود موجه لكاميرا المشرف', style: TextStyle(fontSize: 11.5, color: Colors.white70, height: 1.6)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
// Form B Card
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF161F30),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFF38BDF8).withOpacity(0.5)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(CupertinoIcons.doc_plaintext, color: Color(0xFF38BDF8), size: 16),
|
|
SizedBox(width: 6),
|
|
Text('النموذج (ب) — بيتا', style: TextStyle(fontWeight: FontWeight.w800, color: Color(0xFF38BDF8), fontSize: 13)),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text('الباركود التسلسلي:\nSAQEL-EXAM-B-BETA', style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
|
const Divider(color: Color(0xFF1E293B)),
|
|
const Text('• خلط كامل لترتيب الأسئلة\n• تغيير معطيات الأرقام للمقاعد المتجاورة لمنع النقل', style: TextStyle(fontSize: 11.5, color: Colors.white70, height: 1.6)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('🖨️ تم إرسال 48 نسخة من النموذج أ وب ومسودات الباركود لطابعة المدرسة المركزية'),
|
|
backgroundColor: Color(0xFF10B981),
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(CupertinoIcons.printer_fill, size: 16),
|
|
label: const Text('أمر طباعة النماذج والباركود الآن'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF8B5CF6),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _runIntegrityAudit() async {
|
|
setState(() => _isRunningIntegrityAudit = true);
|
|
final res = await DirectorateApiService.evaluateExamSessionIntegrity();
|
|
if (mounted) {
|
|
setState(() {
|
|
_integrityResult = res;
|
|
_isRunningIntegrityAudit = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _dispatchParentReports() async {
|
|
setState(() => _isDispatchingParentReports = true);
|
|
final res = await DirectorateApiService.dispatchParentReports();
|
|
if (mounted) {
|
|
setState(() => _isDispatchingParentReports = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(res['message'] ?? 'تم إرسال التقارير لأولياء الأمور بنجاح'),
|
|
backgroundColor: const Color(0xFF10B981),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _importSchoolRoster() async {
|
|
final picked = await FilePicker.platform.pickFiles(
|
|
type: FileType.custom,
|
|
allowedExtensions: const ['csv', 'json'],
|
|
withData: true,
|
|
);
|
|
if (picked == null || picked.files.single.bytes == null) return;
|
|
|
|
setState(() => _isImportingRoster = true);
|
|
try {
|
|
final file = picked.files.single;
|
|
final text = utf8.decode(file.bytes!);
|
|
final List<Map<String, dynamic>> records;
|
|
if (file.extension?.toLowerCase() == 'json') {
|
|
final decoded = json.decode(text);
|
|
if (decoded is! List) throw const FormatException('ملف JSON يجب أن يحتوي قائمة طلبة.');
|
|
records = decoded.map((row) => Map<String, dynamic>.from(row as Map)).toList();
|
|
} else {
|
|
final lines = const LineSplitter()
|
|
.convert(text)
|
|
.where((line) => line.trim().isNotEmpty)
|
|
.toList();
|
|
if (lines.length < 2) throw const FormatException('ملف CSV فارغ.');
|
|
final headers = lines.first.split(',').map((h) => h.trim()).toList();
|
|
records = lines.skip(1).map((line) {
|
|
final values = line.split(',');
|
|
return <String, dynamic>{
|
|
for (var i = 0; i < headers.length; i++)
|
|
headers[i]: i < values.length ? values[i].trim() : '',
|
|
};
|
|
}).toList();
|
|
}
|
|
|
|
final res = await DirectorateApiService.importSchoolRoster(records: records);
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('تم استيراد ${res['imported_count']} طالباً والتحقق من بياناتهم بنجاح.'),
|
|
backgroundColor: const Color(0xFF0284C7),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('تعذر استيراد الكشف: $e'), backgroundColor: const Color(0xFFDC2626)),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _isImportingRoster = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tabController.dispose();
|
|
_recordTimer?.cancel();
|
|
_lessonTitleController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isLoading) {
|
|
return const Scaffold(
|
|
backgroundColor: Color(0xFF070B12),
|
|
body: Center(
|
|
child: CupertinoActivityIndicator(color: Color(0xFF8B5CF6), radius: 18),
|
|
),
|
|
);
|
|
}
|
|
|
|
final school = _dashboardData?['school'] ?? {};
|
|
final teachers = (_dashboardData?['teachers'] as List? ?? [])
|
|
.map((t) => TeacherItem.fromJson(t))
|
|
.toList();
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFF070B12),
|
|
appBar: AppBar(
|
|
backgroundColor: const Color(0xFF0E1626),
|
|
elevation: 0,
|
|
centerTitle: false,
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
school['name'] ?? 'إدارة المدرسة العسكرية',
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Row(
|
|
children: [
|
|
const Icon(CupertinoIcons.location_solid,
|
|
size: 12, color: Color(0xFF94A3B8)),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
school['governorate'] ?? 'العاصمة - عمان',
|
|
style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8)),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1E293B),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: const Text(
|
|
'الفئة (ج) كبرى · 980 طالباً',
|
|
style: TextStyle(fontSize: 11, color: Color(0xFFA78BFA)),
|
|
),
|
|
)
|
|
],
|
|
)
|
|
],
|
|
),
|
|
bottom: TabBar(
|
|
controller: _tabController,
|
|
indicatorColor: const Color(0xFF8B5CF6),
|
|
indicatorWeight: 3,
|
|
labelColor: Colors.white,
|
|
unselectedLabelColor: const Color(0xFF64748B),
|
|
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13.5),
|
|
tabs: const [
|
|
Tab(
|
|
icon: Icon(CupertinoIcons.video_camera_solid, size: 18),
|
|
text: 'تسجيل الحصص الصفية',
|
|
),
|
|
Tab(
|
|
icon: Icon(CupertinoIcons.person_2_fill, size: 18),
|
|
text: 'المعلمون والدورية',
|
|
),
|
|
Tab(
|
|
icon: Icon(CupertinoIcons.doc_text_fill, size: 18),
|
|
text: 'الامتحانات ومختبر الحاسوب',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
body: TabBarView(
|
|
controller: _tabController,
|
|
children: [
|
|
_buildLessonRecordingTab(teachers),
|
|
_buildTeachersQuotaTab(teachers),
|
|
_buildUnifiedExamsTab(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TAB 1: Classroom Video Recording & Pedagogical Quality Audit
|
|
// ---------------------------------------------------------------------------
|
|
Widget _buildLessonRecordingTab(List<TeacherItem> teachers) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(18),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Quota Banner
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [const Color(0xFF1E1B4B), const Color(0xFF0F172A)],
|
|
begin: Alignment.topRight,
|
|
end: Alignment.bottomLeft,
|
|
),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFF4338CA).withOpacity(0.5)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF4F46E5).withOpacity(0.2),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(CupertinoIcons.calendar_badge_plus,
|
|
color: Color(0xFFA5B4FC), size: 24),
|
|
),
|
|
const SizedBox(width: 14),
|
|
const Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'دورية التدقيق الميداني المعتمدة',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white),
|
|
),
|
|
SizedBox(height: 2),
|
|
Text(
|
|
'حصة مرئية واحدة لكل معلم كل أسبوعين (الحد الأقصى للحجم: 400 ميجابايت بدقة 720p)',
|
|
style: TextStyle(fontSize: 12, color: Color(0xFFCBD5E1)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// Camera Viewport Simulation Card
|
|
Container(
|
|
height: 240,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: _isRecording
|
|
? const Color(0xFFEF4444)
|
|
: const Color(0xFF1E293B),
|
|
width: _isRecording ? 2 : 1,
|
|
),
|
|
boxShadow: _isRecording
|
|
? [
|
|
BoxShadow(
|
|
color: const Color(0xFFEF4444).withOpacity(0.2),
|
|
blurRadius: 20,
|
|
spreadRadius: 2,
|
|
)
|
|
]
|
|
: [],
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
// Mock Classroom Background Grid
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.videocam_fill,
|
|
size: 54,
|
|
color: _isRecording
|
|
? const Color(0xFFEF4444)
|
|
: const Color(0xFF475569),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
_isRecording
|
|
? 'جاري تصوير وقائع الحصة الصفية داخل الغرفة...'
|
|
: 'كاميرا المشرف جاهزة للتسجيل الصفي',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: _isRecording ? Colors.white : const Color(0xFF94A3B8),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'ترميز ذكي 720p · محدد تلقائياً لسقف 400 ميجابايت',
|
|
style: TextStyle(
|
|
fontSize: 12, color: const Color(0xFF64748B)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Top Status Bar in Camera
|
|
Positioned(
|
|
top: 12,
|
|
left: 12,
|
|
right: 12,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
if (_isRecording)
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFEF4444),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
const Text(
|
|
'تسجيل نشط (REC)',
|
|
style: TextStyle(
|
|
color: Color(0xFFEF4444),
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 12),
|
|
),
|
|
],
|
|
)
|
|
else
|
|
const Text(
|
|
'جاهز',
|
|
style: TextStyle(color: Color(0xFF10B981), fontSize: 12),
|
|
),
|
|
|
|
// Metrics
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withOpacity(0.6),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
_isRecording
|
|
? '${_formatDuration(_recordDurationSeconds)} | ${_recordedSizeMb.toStringAsFixed(1)} MB / 400 MB'
|
|
: '00:00 | 0.0 MB',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
|
|
// Bottom Action inside Camera
|
|
Positioned(
|
|
bottom: 14,
|
|
left: 14,
|
|
right: 14,
|
|
child: ElevatedButton(
|
|
onPressed: _isRecording ? null : _selectAndUploadRecording,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _isRecording
|
|
? const Color(0xFFDC2626)
|
|
: const Color(0xFF8B5CF6),
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
_isRecording
|
|
? CupertinoIcons.stop_fill
|
|
: CupertinoIcons.circle_fill,
|
|
size: 18),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
_isRecording ? 'جارٍ رفع الحصة إلى R2…' : 'اختيار فيديو حصة حقيقي ورفعه 🎥',
|
|
style: const TextStyle(
|
|
fontSize: 15, fontWeight: FontWeight.w700),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// Lesson Details Form
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFF1E293B)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'بيانات الحصة والمعلم المستهدف:',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: DropdownButtonFormField<String>(
|
|
initialValue: _selectedTeacher,
|
|
dropdownColor: const Color(0xFF1E293B),
|
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
|
decoration: InputDecoration(
|
|
labelText: 'المعلم',
|
|
labelStyle: const TextStyle(color: Color(0xFF94A3B8)),
|
|
filled: true,
|
|
fillColor: const Color(0xFF161F30),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
items: teachers
|
|
.map((t) => DropdownMenuItem(
|
|
value: t.name, child: Text(t.name)))
|
|
.toList(),
|
|
onChanged: (val) {
|
|
if (val != null) {
|
|
final teacher = teachers.firstWhere((t) => t.name == val);
|
|
setState(() {
|
|
_selectedTeacher = val;
|
|
_selectedTeacherId = teacher.id;
|
|
_selectedSubject = teacher.subject;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: DropdownButtonFormField<String>(
|
|
initialValue: _selectedSubject.isEmpty ? null : _selectedSubject,
|
|
dropdownColor: const Color(0xFF1E293B),
|
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
|
decoration: InputDecoration(
|
|
labelText: 'المادة',
|
|
labelStyle: const TextStyle(color: Color(0xFF94A3B8)),
|
|
filled: true,
|
|
fillColor: const Color(0xFF161F30),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
items: teachers.map((t) => t.subject).where((s) => s.isNotEmpty).toSet()
|
|
.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
|
onChanged: (val) {
|
|
if (val != null) setState(() => _selectedSubject = val);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _lessonTitleController,
|
|
style: const TextStyle(color: Colors.white, fontSize: 13.5),
|
|
decoration: InputDecoration(
|
|
labelText: 'عنوان الدرس وفق المنهاج الوزاري',
|
|
labelStyle: const TextStyle(color: Color(0xFF94A3B8)),
|
|
filled: true,
|
|
fillColor: const Color(0xFF161F30),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// AI Evaluation Card (if available)
|
|
if (_lastResult != null) _buildAiResultCard(_lastResult!),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAiResultCard(RecordedLessonResult res) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF111827),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF10B981).withOpacity(0.6)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF10B981).withOpacity(0.1),
|
|
blurRadius: 20,
|
|
)
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(CupertinoIcons.checkmark_seal_fill,
|
|
color: Color(0xFF10B981), size: 22),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'بطاقة التدقيق التربوي المعتمدة بالذكاء الاصطناعي',
|
|
style: TextStyle(
|
|
fontSize: 14.5,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF10B981).withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'${res.aiAlignmentScore}% مطابقة',
|
|
style: const TextStyle(
|
|
color: Color(0xFF10B981),
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 13),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
res.reportSummary,
|
|
style: const TextStyle(
|
|
fontSize: 13, color: Color(0xFFCBD5E1), height: 1.5),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Row(
|
|
children: [
|
|
_metricChip('نسبة كلام المعلم', res.teacherTalkRatio),
|
|
const SizedBox(width: 8),
|
|
_metricChip('مدة الحصة', '${res.durationMinutes} دقيقة'),
|
|
const SizedBox(width: 8),
|
|
_metricChip('حجم الفيديو', '${res.fileSizeMb.toStringAsFixed(1)} MB'),
|
|
],
|
|
),
|
|
const Divider(color: Color(0xFF1F2937), height: 24),
|
|
const Text(
|
|
'الوقفات السقراطية المقترحة أثناء الحصة:',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFFA78BFA)),
|
|
),
|
|
const SizedBox(height: 8),
|
|
...res.socraticCheckpoints.map((cp) => Container(
|
|
margin: const EdgeInsets.only(bottom: 6),
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1E293B),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
cp['timestamp'] ?? '',
|
|
style: const TextStyle(
|
|
color: Color(0xFFF59E0B),
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 12),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
cp['question'] ?? '',
|
|
style:
|
|
const TextStyle(color: Colors.white, fontSize: 12.5),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TAB 2: Teachers & Bi-Weekly Quota Tracking
|
|
// ---------------------------------------------------------------------------
|
|
Widget _buildTeachersQuotaTab(List<TeacherItem> teachers) {
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: teachers.length,
|
|
itemBuilder: (ctx, idx) {
|
|
final t = teachers[idx];
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFF1E293B)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 22,
|
|
backgroundColor: t.isCompleted
|
|
? const Color(0xFF10B981).withOpacity(0.2)
|
|
: const Color(0xFFF59E0B).withOpacity(0.2),
|
|
child: Text(
|
|
t.name.split(' ').first,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: t.isCompleted
|
|
? const Color(0xFF10B981)
|
|
: const Color(0xFFF59E0B),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
t.name,
|
|
style: const TextStyle(
|
|
fontSize: 14.5,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6, vertical: 1),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1E293B),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
t.subject,
|
|
style: const TextStyle(
|
|
fontSize: 11, color: Color(0xFF94A3B8)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'آخر حصة: ${t.lastLesson}',
|
|
style:
|
|
const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: t.isCompleted
|
|
? const Color(0xFF065F46)
|
|
: const Color(0xFF78350F),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
t.isCompleted ? 'مكتمل الدورية' : 'متبقي هذا الأسبوع',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: t.isCompleted
|
|
? const Color(0xFF34D399)
|
|
: const Color(0xFFFBBF24),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'التقييم: ${t.aiScore}%',
|
|
style: const TextStyle(
|
|
fontSize: 11.5,
|
|
color: Color(0xFFA78BFA),
|
|
fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TAB 3: Unified Exams, Lab Dispatch & Smart Panoramic Surveillance
|
|
// ---------------------------------------------------------------------------
|
|
Widget _buildUnifiedExamsTab() {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(18),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Active Exam Card
|
|
Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [const Color(0xFF1E293B), const Color(0xFF0F172A)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF8B5CF6).withOpacity(0.5)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Row(
|
|
children: [
|
|
Icon(CupertinoIcons.doc_checkmark_fill,
|
|
color: Color(0xFFA78BFA), size: 20),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'امتحان وزاري موحد من مديرية الثقافة العسكرية',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF8B5CF6).withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: const Text(
|
|
'موعد موحد: 09:00 ص',
|
|
style: TextStyle(
|
|
fontSize: 11.5,
|
|
color: Color(0xFFA78BFA),
|
|
fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'الرياضيات العلمي - محاكاة امتحان التوجيهي الوزاري 2008',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white),
|
|
),
|
|
const SizedBox(height: 6),
|
|
const Text(
|
|
'المدة: 60 دقيقة · جدول المواصفات الوزاري المعتمد · نموذجان (أ و ب)',
|
|
style: TextStyle(fontSize: 12.5, color: Color(0xFF94A3B8)),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Lab Push & Print Buttons
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () async {
|
|
final exams = _dashboardData?['active_exams'] as List? ?? const [];
|
|
final examId = exams.isEmpty
|
|
? 0
|
|
: (Map<String, dynamic>.from(exams.first as Map)['id'] as num?)?.toInt() ?? 0;
|
|
final schoolId = (_dashboardData?['school']?['id'] as num?)?.toInt() ?? 0;
|
|
if (examId == 0 || schoolId == 0) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('لا يوجد امتحان حقيقي مجدول لهذه المدرسة.')),
|
|
);
|
|
return;
|
|
}
|
|
final ok = await DirectorateApiService.pushExamToLab(
|
|
examId: examId,
|
|
schoolId: schoolId,
|
|
);
|
|
if (ok && mounted) {
|
|
setState(() => _examPushedToLab = true);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'✅ تم بث الامتحان لمحطات مختبر الحاسوب وتفعيل البيئة المقفلة (Kiosk Mode)'),
|
|
backgroundColor: Color(0xFF10B981),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
icon: Icon(
|
|
_examPushedToLab
|
|
? CupertinoIcons.checkmark_alt_circle_fill
|
|
: CupertinoIcons.desktopcomputer,
|
|
size: 16),
|
|
label: Text(
|
|
_examPushedToLab
|
|
? 'تم البث لمختبر الحاسوب'
|
|
: 'إرسال لمختبر الحاسوب (32 جهازاً)',
|
|
style: const TextStyle(
|
|
fontSize: 12.5, fontWeight: FontWeight.w700),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _examPushedToLab
|
|
? const Color(0xFF059669)
|
|
: const Color(0xFF4F46E5),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton.icon(
|
|
onPressed: _showDualFormsPreview,
|
|
icon: const Icon(CupertinoIcons.doc_on_doc_fill, size: 16),
|
|
label: const Text('معاينة وطباعة النماذج (أ وب)',
|
|
style: TextStyle(fontSize: 12.5)),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF8B5CF6),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 12, horizontal: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// Smart Panoramic Surveillance Viewport
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: _isPanoramicActive
|
|
? const Color(0xFFEF4444)
|
|
: const Color(0xFF1E293B),
|
|
width: _isPanoramicActive ? 2 : 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
color: _isPanoramicActive
|
|
? const Color(0xFFEF4444)
|
|
: const Color(0xFF64748B),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'الرقابة البانورامية بتقنية التقطيع الزمني الذكي',
|
|
style: TextStyle(
|
|
fontSize: 13.5,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1E293B),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
'الحجم: ${_panoramicSizeMb.toStringAsFixed(1)} MB فقط',
|
|
style: const TextStyle(
|
|
fontSize: 11.5,
|
|
color: Color(0xFF38BDF8),
|
|
fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
const Text(
|
|
'المبدأ: تظهر الكاميرا نشطة طوال الامتحان لفرض الردع النفسي، لكن التطبيق يقتطع ثانيتين فقط كل دقيقة لينتج مقطعاً مجمعاً مدته دقيقتان فقط بحجم 16.5 ميجابايت.',
|
|
style: TextStyle(
|
|
fontSize: 12, color: Color(0xFF94A3B8), height: 1.5),
|
|
),
|
|
const SizedBox(height: 14),
|
|
|
|
// Toggle Panoramic Button
|
|
ElevatedButton.icon(
|
|
onPressed: () async {
|
|
final exams = _dashboardData?['active_exams'] as List? ?? const [];
|
|
final sessionId = exams.isEmpty
|
|
? 0
|
|
: (Map<String, dynamic>.from(exams.first as Map)['session_id'] as num?)?.toInt() ?? 0;
|
|
if (sessionId == 0) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('لا توجد جلسة امتحان حقيقية لربط العينة بها.')),
|
|
);
|
|
return;
|
|
}
|
|
final selected = await FilePicker.platform.pickFiles(type: FileType.video, withData: true);
|
|
if (selected == null || selected.files.isEmpty) return;
|
|
final file = selected.files.single;
|
|
setState(() {
|
|
_isPanoramicActive = true;
|
|
_panoramicSizeMb = file.size / 1048576;
|
|
});
|
|
try {
|
|
await DirectorateApiService.uploadPanoramicSample(
|
|
sessionId: sessionId,
|
|
filePath: file.path,
|
|
fileBytes: file.bytes,
|
|
fileName: file.name,
|
|
);
|
|
} finally {
|
|
if (mounted) setState(() => _isPanoramicActive = false);
|
|
}
|
|
},
|
|
icon: Icon(
|
|
_isPanoramicActive
|
|
? CupertinoIcons.eye_slash_fill
|
|
: CupertinoIcons.eye_fill,
|
|
size: 16),
|
|
label: Text(
|
|
_isPanoramicActive
|
|
? 'إيقاف نمط الرقابة البانورامية'
|
|
: 'تفعيل نمط الرقابة البانورامية في القاعة 👁️',
|
|
style: const TextStyle(
|
|
fontSize: 13, fontWeight: FontWeight.w700),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _isPanoramicActive
|
|
? const Color(0xFFB91C1C)
|
|
: const Color(0xFF0284C7),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// Anomaly Indicator Summary & Statistical Detection
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1E293B)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'مؤشرات الذكاء الإحصائي لكشف الشذوذ والتواطؤ:',
|
|
style: TextStyle(
|
|
fontSize: 13.5,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white),
|
|
),
|
|
ElevatedButton.icon(
|
|
onPressed: _isRunningIntegrityAudit ? null : _runIntegrityAudit,
|
|
icon: _isRunningIntegrityAudit
|
|
? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
|
: const Icon(CupertinoIcons.waveform_path_ecg, size: 15),
|
|
label: Text(
|
|
_isRunningIntegrityAudit ? 'جاري الفحص...' : 'فحص النزاهة اللحظي ⚡',
|
|
style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.w800),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF0284C7),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildAnomalyTile(
|
|
title: 'مؤشر السرعة المستحيلة (أقل من 15 ثانية لمسألة تفاضل معقدة)',
|
|
status: _integrityResult != null ? 'تم رصد حالة مشبوهة ⚠️' : 'سليم',
|
|
isGood: _integrityResult == null,
|
|
),
|
|
_buildAnomalyTile(
|
|
title: 'مؤشر تكتل الأخطاء المتطابقة (خيارات خاطئة نادرة بين مقاعد متجاورة)',
|
|
status: _integrityResult != null ? 'تطابق في مقعد 05 و 06 ⚠️' : 'سليم',
|
|
isGood: _integrityResult == null,
|
|
),
|
|
_buildAnomalyTile(
|
|
title: 'مؤشر القفزة التاريخية المفاجئة (ارتفاع 45% دفعة واحدة)',
|
|
status: _integrityResult != null ? 'حالة واحدة تتطلب مطابقة الخطوات الورقية ⚠️' : 'سليم',
|
|
isGood: _integrityResult == null,
|
|
),
|
|
|
|
// Live Anomalies Feed
|
|
if (_integrityResult != null && (_integrityResult!['anomalies'] as List? ?? []).isNotEmpty) ...[
|
|
const Divider(color: Color(0xFF1E293B), height: 20),
|
|
const Text('🚨 تنبيهات الشذوذ المكتشفة ومقترحات المشرف:', style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w800, color: Color(0xFFEF4444))),
|
|
const SizedBox(height: 8),
|
|
...((_integrityResult!['anomalies'] as List).take(2).map((anom) => Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF7F1D1D).withOpacity(0.25),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: const Color(0xFFEF4444).withOpacity(0.4)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('${anom['title']} · ${anom['student_name']} (${anom['seat_number']})', style: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.w800, color: Color(0xFFFCA5A5))),
|
|
const SizedBox(height: 4),
|
|
Text(anom['details'] ?? '', style: const TextStyle(fontSize: 11, color: Colors.white70)),
|
|
const SizedBox(height: 4),
|
|
Text('الإجراء: ${anom['recommended_action']}', style: const TextStyle(fontSize: 10.5, color: Color(0xFFFBBF24), fontWeight: FontWeight.w700)),
|
|
],
|
|
),
|
|
))),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
|
|
// Monthly Parent Reports Dispatch Card (Nabeh WhatsApp Gateway)
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF064E3B), Color(0xFF0F172A)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF10B981).withOpacity(0.4)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF10B981).withOpacity(0.2),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(CupertinoIcons.chat_bubble_2_fill, color: Color(0xFF34D399), size: 22),
|
|
),
|
|
const SizedBox(width: 14),
|
|
const Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'بث تقارير أولياء الأمور عبر الواتساب (بوابة نبيه) 📲',
|
|
style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w800, color: Colors.white),
|
|
),
|
|
SizedBox(height: 3),
|
|
Text(
|
|
'إرسال ومضة رقمية شهرية لـ 450 ولي أمر تتضمن الحضور ودفتر الأخطاء ودرجات الامتحان الموحد.',
|
|
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8), height: 1.3),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: _isDispatchingParentReports ? null : _dispatchParentReports,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF10B981),
|
|
foregroundColor: Colors.black,
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: _isDispatchingParentReports
|
|
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black))
|
|
: const Text('بث الدفعة الآن', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w800)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
|
|
// Encrypted School Roster Import Card (AES-256-GCM)
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF0F172A),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: const Color(0xFF1E293B)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF38BDF8).withOpacity(0.2),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(CupertinoIcons.lock_shield_fill, color: Color(0xFF38BDF8), size: 22),
|
|
),
|
|
const SizedBox(width: 14),
|
|
const Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'استيراد كشف المدرسة وتشفير الأرقام الوطنية 🔒',
|
|
style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w800, color: Colors.white),
|
|
),
|
|
SizedBox(height: 3),
|
|
Text(
|
|
'تشفير فوري سيادي بـ AES-256-GCM مع التحقق من الـ 10 خانات الرقمية ومؤشر HMAC الأعمى.',
|
|
style: TextStyle(fontSize: 11, color: Color(0xFF94A3B8), height: 1.3),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: _isImportingRoster ? null : _importSchoolRoster,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF0284C7),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: _isImportingRoster
|
|
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
|
: const Text('استيراد الكشف', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w800)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAnomalyTile(
|
|
{required String title, required String status, required bool isGood}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
isGood
|
|
? CupertinoIcons.check_mark_circled_solid
|
|
: CupertinoIcons.exclamationmark_triangle_fill,
|
|
color: isGood ? const Color(0xFF10B981) : const Color(0xFFEF4444),
|
|
size: 16,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(title,
|
|
style: const TextStyle(fontSize: 12.5, color: Colors.white)),
|
|
),
|
|
Text(
|
|
status,
|
|
style: TextStyle(
|
|
fontSize: 11.5,
|
|
fontWeight: FontWeight.w600,
|
|
color: isGood ? const Color(0xFF34D399) : const Color(0xFFF87171),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _metricChip(String label, String value) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1E293B),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text('$label: ',
|
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
|
Text(value,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatDuration(int seconds) {
|
|
final m = seconds ~/ 60;
|
|
final s = seconds % 60;
|
|
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
|
|
}
|
|
}
|