Update Saqel Platform: 2026-09-04 02:04:43

This commit is contained in:
Hamza-Ayed
2026-09-04 02:04:43 +03:00
parent 58c3c48f69
commit 1fe97bf3eb
16 changed files with 5996 additions and 183 deletions
+153 -172
View File
@@ -1,4 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'screens/directorate_command_screen.dart';
import 'screens/school_principal_screen.dart';
void main() {
runApp(const SaqelAdminApp());
@@ -10,11 +13,11 @@ class SaqelAdminApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'صَقِل - الإدارة والمدارس',
title: 'صَقِل - الإدارة المركزية والمدارس',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0A0E17),
scaffoldBackgroundColor: const Color(0xFF070B12),
fontFamily: '-apple-system',
fontFamilyFallback: const [
'SF Pro Display',
@@ -27,197 +30,175 @@ class SaqelAdminApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF8B5CF6),
brightness: Brightness.dark,
surface: const Color(0xFF111827),
surface: const Color(0xFF0F172A),
primary: const Color(0xFF8B5CF6),
),
),
home: const AdminHomeScreen(),
builder: (context, child) {
return Directionality(
textDirection: TextDirection.rtl,
child: child ?? const SizedBox(),
);
},
home: const UnifiedSupervisorShell(),
);
}
}
class AdminHomeScreen extends StatelessWidget {
const AdminHomeScreen({super.key});
/// Unified Shell allowing seamless switching between:
/// 1. School Principal / Field Supervisor View (تطبيق مدير المدرسة والمشرف)
/// 2. Directorate Commander View (لوحة القائد العام لمديرية الثقافة العسكرية)
class UnifiedSupervisorShell extends StatefulWidget {
const UnifiedSupervisorShell({super.key});
@override
State<UnifiedSupervisorShell> createState() => _UnifiedSupervisorShellState();
}
class _UnifiedSupervisorShellState extends State<UnifiedSupervisorShell> {
int _currentModeIndex = 0; // 0 = School Principal, 1 = Directorate Commander
final List<Widget> _screens = const [
SchoolPrincipalScreen(),
DirectorateCommandScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment(0.0, -0.6),
radius: 1.2,
colors: [
Color(0xFF25134A),
Color(0xFF0A0E17),
],
),
),
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 580),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF8B5CF6), Color(0xFFA78BFA)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: const [
BoxShadow(
color: Color(0x4D8B5CF6),
blurRadius: 25,
offset: Offset(0, 10),
),
],
),
child: const Center(
child: Text(
'إ',
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.w900,
color: Colors.white,
),
),
),
),
),
const SizedBox(height: 24),
const Text(
'إدارة منصة صَقِل وشبكة المدارس',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
color: Colors.white,
),
),
const SizedBox(height: 8),
const Text(
'لوحة تحكم B2B: إدارة كشوفات المدارس، تراخيص الثقافة العسكرية، والرقابة المركزية',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Color(0xFF94A3B8),
height: 1.5,
),
),
const SizedBox(height: 36),
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xCC161F30),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: const Color(0x14FFFFFF),
),
boxShadow: const [
BoxShadow(
color: Color(0x66000000),
blurRadius: 30,
offset: Offset(0, 15),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 10,
height: 10,
decoration: const BoxDecoration(
color: Color(0xFFA78BFA),
shape: BoxShape.circle,
),
),
const SizedBox(width: 10),
const Expanded(
child: Text(
'التحكم المركزي والأمان (Enterprise Governance)',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Color(0xFFA78BFA),
),
),
),
],
),
const SizedBox(height: 16),
_buildFeatureRow('🏫 كشوفات المدارس (Rosters)', 'الربط التلقائي بالرقم الوطني واستيراد CSV'),
_buildFeatureRow('💼 التراخيص وحصص المعلمين', 'متابعة المبيعات، العقود، والأرباح'),
_buildFeatureRow('🛡️ الرقابة والامتثال', 'حظر القرصنة، مراقبة استهلاك الخوادم وبوابة نبيه'),
],
),
),
const SizedBox(height: 28),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8B5CF6),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 0,
),
child: const Text(
'دخول لوحة الإدارة المركزية 🏢',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
],
backgroundColor: const Color(0xFF070B12),
body: SafeArea(
child: Column(
children: [
// Top Luxury Persona Switcher
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: const BoxDecoration(
color: Color(0xFF0E1626),
border: Border(
bottom: BorderSide(color: Color(0xFF1E293B)),
),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF8B5CF6), Color(0xFF4F46E5)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(10),
),
child: const Center(
child: Text(
'ص',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'منظومة صَقِل الإشرافية والسيادية',
style: TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
Text(
'التبديل بين الصلاحيات التنفيذية والقيادية',
style: TextStyle(
fontSize: 11,
color: Color(0xFF94A3B8),
),
),
],
),
),
// Segmented Switcher Pill
Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: const Color(0xFF1E293B),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildModeTab(
index: 0,
title: 'مدير المدرسة / المشرف',
icon: CupertinoIcons.building_2_fill,
),
_buildModeTab(
index: 1,
title: 'لوحة القائد الأعلى (43 مدرسة)',
icon: CupertinoIcons.shield_lefthalf_fill,
),
],
),
),
],
),
),
),
// Active View
Expanded(
child: IndexedStack(
index: _currentModeIndex,
children: _screens,
),
),
],
),
),
);
}
static Widget _buildFeatureRow(String title, String subtitle) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w600,
color: Colors.white,
Widget _buildModeTab({
required int index,
required String title,
required IconData icon,
}) {
final isSelected = _currentModeIndex == index;
return GestureDetector(
onTap: () => setState(() => _currentModeIndex = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF8B5CF6) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
icon,
size: 14,
color: isSelected ? Colors.white : const Color(0xFF94A3B8),
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF64748B),
const SizedBox(width: 6),
Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
color: isSelected ? Colors.white : const Color(0xFF94A3B8),
),
),
),
],
],
),
),
);
}
@@ -0,0 +1,270 @@
import 'dart:convert';
/// Directorate Model (المظلة المركزية: مديرية الثقافة العسكرية)
class DirectorateOverview {
final String code;
final String name;
final String commanderName;
final int totalSchools;
final int totalStudents;
final int totalTeachers;
final double annualContractValue;
final int recordedLessonsMonth;
final double complianceRate;
final double aiAverageScore;
final int activeUnifiedExams;
DirectorateOverview({
required this.code,
required this.name,
required this.commanderName,
required this.totalSchools,
required this.totalStudents,
required this.totalTeachers,
required this.annualContractValue,
required this.recordedLessonsMonth,
required this.complianceRate,
required this.aiAverageScore,
required this.activeUnifiedExams,
});
factory DirectorateOverview.fromJson(Map<String, dynamic> json) {
return DirectorateOverview(
code: json['code'] ?? 'MC-JOR',
name: json['name'] ?? 'مديرية التربية والتعليم والثقافة العسكرية',
commanderName: json['commander_name'] ?? 'مدير التعليم والثقافة العسكرية',
totalSchools: json['total_schools'] ?? 43,
totalStudents: json['total_students'] ?? 19350,
totalTeachers: json['total_teachers'] ?? 812,
annualContractValue: (json['annual_contract_value'] as num?)?.toDouble() ?? 20000.0,
recordedLessonsMonth: json['recorded_lessons_month'] ?? 1420,
complianceRate: (json['compliance_rate'] as num?)?.toDouble() ?? 94.5,
aiAverageScore: (json['ai_average_score'] as num?)?.toDouble() ?? 91.8,
activeUnifiedExams: json['active_unified_exams'] ?? 2,
);
}
}
/// School Item Model (المدرسة التابعة وتصنيف الوزن)
class SchoolItem {
final int id;
final String code;
final String name;
final String governorate;
final String weightTier; // tier_a_small, tier_b_medium, tier_c_large
final int studentCount;
final int teacherCount;
final double compliance;
final String status; // excellent, good, warning_under_quota
final double annualFee;
SchoolItem({
required this.id,
required this.code,
required this.name,
required this.governorate,
required this.weightTier,
required this.studentCount,
required this.teacherCount,
required this.compliance,
required this.status,
required this.annualFee,
});
String get weightTierArabic {
switch (weightTier) {
case 'tier_a_small':
return 'الفئة (أ) - صغيرة/نائية';
case 'tier_c_large':
return 'الفئة (ج) - كبرى/مركزية';
case 'tier_b_medium':
default:
return 'الفئة (ب) - قياسية';
}
}
factory SchoolItem.fromJson(Map<String, dynamic> json) {
return SchoolItem(
id: json['id'] ?? 0,
code: json['code'] ?? '',
name: json['name'] ?? '',
governorate: json['governorate'] ?? 'العاصمة',
weightTier: json['weight_tier'] ?? 'tier_b_medium',
studentCount: json['student_count'] ?? 450,
teacherCount: json['teacher_count'] ?? 24,
compliance: (json['compliance'] as num?)?.toDouble() ?? 90.0,
status: json['status'] ?? 'good',
annualFee: (json['annual_fee'] as num?)?.toDouble() ?? 450.0,
);
}
}
/// Teacher Item Model (المعلم ومؤشر دورية الأسبوعين)
class TeacherItem {
final int id;
final String name;
final String subject;
final String grade;
final String quotaStatus; // completed, pending, overdue
final String lastLesson;
final double aiScore;
final String recordedAt;
TeacherItem({
required this.id,
required this.name,
required this.subject,
required this.grade,
required this.quotaStatus,
required this.lastLesson,
required this.aiScore,
required this.recordedAt,
});
bool get isCompleted => quotaStatus == 'completed';
factory TeacherItem.fromJson(Map<String, dynamic> json) {
return TeacherItem(
id: json['id'] ?? 0,
name: json['name'] ?? '',
subject: json['subject'] ?? '',
grade: json['grade'] ?? '',
quotaStatus: json['quota_status'] ?? 'pending',
lastLesson: json['last_lesson'] ?? '',
aiScore: (json['ai_score'] as num?)?.toDouble() ?? 90.0,
recordedAt: json['recorded_at'] ?? 'مؤخراً',
);
}
}
/// Recorded Classroom Lesson Result (نتيجة فحص الحصة المرئية)
class RecordedLessonResult {
final String lessonUuid;
final String teacherName;
final String subject;
final String lessonTitle;
final double fileSizeMb;
final int durationMinutes;
final double aiAlignmentScore;
final String teacherTalkRatio;
final String status;
final String reportSummary;
final List<Map<String, String>> socraticCheckpoints;
RecordedLessonResult({
required this.lessonUuid,
required this.teacherName,
required this.subject,
required this.lessonTitle,
required this.fileSizeMb,
required this.durationMinutes,
required this.aiAlignmentScore,
required this.teacherTalkRatio,
required this.status,
required this.reportSummary,
required this.socraticCheckpoints,
});
factory RecordedLessonResult.fromJson(Map<String, dynamic> json) {
var rawCheckpoints = json['socratic_checkpoints'] as List<dynamic>? ?? [];
List<Map<String, String>> checkpoints = rawCheckpoints.map((c) {
if (c is Map) {
return {
'timestamp': c['timestamp']?.toString() ?? '',
'question': c['question']?.toString() ?? '',
'objective': c['objective']?.toString() ?? '',
};
}
return <String, String>{};
}).toList();
return RecordedLessonResult(
lessonUuid: json['lesson_uuid'] ?? '',
teacherName: json['teacher_name'] ?? '',
subject: json['subject'] ?? '',
lessonTitle: json['lesson_title'] ?? '',
fileSizeMb: (json['file_size_mb'] as num?)?.toDouble() ?? 350.0,
durationMinutes: json['duration_minutes'] ?? 45,
aiAlignmentScore: (json['ai_alignment_score'] as num?)?.toDouble() ?? 92.0,
teacherTalkRatio: json['teacher_talk_ratio'] ?? '60%',
status: json['status'] ?? 'approved_official',
reportSummary: json['report_summary'] ?? 'تم الاعتماد بنجاح',
socraticCheckpoints: checkpoints,
);
}
}
/// Unified Exam Model (الامتحان الموحد)
class UnifiedExamModel {
final String id;
final String title;
final String subject;
final String gradeLevel;
final String scheduledTime;
final int durationMinutes;
final bool pushedToLab;
final String status;
final List<String> forms;
final Map<String, dynamic> antiCheating;
UnifiedExamModel({
required this.id,
required this.title,
required this.subject,
required this.gradeLevel,
required this.scheduledTime,
required this.durationMinutes,
required this.pushedToLab,
required this.status,
required this.forms,
required this.antiCheating,
});
factory UnifiedExamModel.fromJson(Map<String, dynamic> json) {
return UnifiedExamModel(
id: json['id'] ?? '',
title: json['title'] ?? '',
subject: json['subject'] ?? '',
gradeLevel: json['grade_level'] ?? '',
scheduledTime: json['scheduled_time'] ?? '',
durationMinutes: json['duration_minutes'] ?? 60,
pushedToLab: json['pushed_to_lab'] == true,
status: json['status'] ?? 'scheduled',
forms: (json['forms'] as List<dynamic>?)?.map((e) => e.toString()).toList() ?? ['نموذج أ', 'نموذج ب'],
antiCheating: json['anti_cheating'] as Map<String, dynamic>? ?? {},
);
}
}
/// Exam Anomaly Alert Model (تنبيهات الشذوذ الإحصائي والغش)
class ExamAnomalyAlert {
final String id;
final String type; // speed_impossible, error_clustering, performance_leap
final String schoolName;
final String subject;
final String description;
final String severity;
final String timestamp;
ExamAnomalyAlert({
required this.id,
required this.type,
required this.schoolName,
required this.subject,
required this.description,
required this.severity,
required this.timestamp,
});
factory ExamAnomalyAlert.fromJson(Map<String, dynamic> json) {
return ExamAnomalyAlert(
id: json['id'] ?? '',
type: json['type'] ?? 'speed_impossible',
schoolName: json['school_name'] ?? '',
subject: json['subject'] ?? '',
description: json['description'] ?? '',
severity: json['severity'] ?? 'medium',
timestamp: json['timestamp'] ?? '',
);
}
}
@@ -0,0 +1,692 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../models/directorate_models.dart';
import '../services/directorate_api_service.dart';
class DirectorateCommandScreen extends StatefulWidget {
const DirectorateCommandScreen({super.key});
@override
State<DirectorateCommandScreen> createState() =>
_DirectorateCommandScreenState();
}
class _DirectorateCommandScreenState extends State<DirectorateCommandScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
bool _isLoading = true;
Map<String, dynamic>? _data;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_loadDashboard();
}
Future<void> _loadDashboard() async {
setState(() => _isLoading = true);
final res = await DirectorateApiService.fetchDirectorateDashboard();
if (mounted) {
setState(() {
_data = res;
_isLoading = false;
});
}
}
@override
void dispose() {
_tabController.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 dir = _data?['directorate'] ?? {};
final schools = (_data?['schools'] as List? ?? [])
.map((s) => SchoolItem.fromJson(s))
.toList();
final anomalies = (_data?['anomalies'] as List? ?? [])
.map((a) => ExamAnomalyAlert.fromJson(a))
.toList();
final radar = _data?['radar'] ?? {};
return Scaffold(
backgroundColor: const Color(0xFF070B12),
appBar: AppBar(
backgroundColor: const Color(0xFF0E1626),
elevation: 0,
centerTitle: false,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF10B981),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
dir['name'] ?? 'مديرية الثقافة العسكرية',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: Colors.white),
),
],
),
const SizedBox(height: 2),
const Text(
'لوحة القائد الأعلى للرقابة والسيادة الرقمية (43 مدرسة في المملكة)',
style: TextStyle(fontSize: 12, color: Color(0xFF94A3B8)),
),
],
),
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.chart_bar_alt_fill, size: 18),
text: 'الرؤية الكلية والمدارس',
),
Tab(
icon: Icon(CupertinoIcons.star_circle_fill, size: 18),
text: 'رادار المعلمين والتسييل',
),
Tab(
icon: Icon(CupertinoIcons.shield_lefthalf_fill, size: 18),
text: 'نزاهة الامتحانات الموحدة',
),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildMacroOverviewTab(dir, schools),
_buildTeachersRadarTab(radar),
_buildExamIntegrityTab(anomalies),
],
),
);
}
// ---------------------------------------------------------------------------
// TAB 1: Macro Directorate Metrics & 43 Schools Matrix
// ---------------------------------------------------------------------------
Widget _buildMacroOverviewTab(
Map<String, dynamic> dir, List<SchoolItem> schools) {
return SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Commander KPI Ribbon
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1E1B4B), Color(0xFF0F172A)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: const Color(0xFF6366F1).withOpacity(0.4)),
),
child: Column(
children: [
Row(
children: [
Expanded(
child: _kpiCell(
'إجمالي المدارس',
'${dir['total_schools'] ?? 43} مدرسة',
CupertinoIcons.building_2_fill,
const Color(0xFF818CF8),
),
),
Expanded(
child: _kpiCell(
'الطلبة المسجلون',
'${dir['total_students'] ?? 19350}',
CupertinoIcons.person_3_fill,
const Color(0xFF38BDF8),
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: _kpiCell(
'معلمو الثقافة العسكرية',
'${dir['total_teachers'] ?? 812} معلم',
CupertinoIcons.person_badge_plus_fill,
const Color(0xFFFBBF24),
),
),
Expanded(
child: _kpiCell(
'نسبة كوتة الحصص',
'${dir['compliance_rate'] ?? 94.5}%',
CupertinoIcons.check_mark_circled_solid,
const Color(0xFF34D399),
),
),
],
),
],
),
),
const SizedBox(height: 20),
// Schools Weight Tier Breakdown
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'توزيع مدارس الثقافة العسكرية الـ 43 حسب تصنيف الوزن:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white),
),
Text(
'إجمالي العقد: 20,000 د.أ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: const Color(0xFFA78BFA)),
),
],
),
const SizedBox(height: 12),
// School List Cards
...schools.map((s) => 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: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: _statusColor(s.status).withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(CupertinoIcons.building_2_fill,
color: _statusColor(s.status), size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white),
),
const SizedBox(height: 4),
Row(
children: [
Text(
s.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: Text(
s.weightTierArabic,
style: const TextStyle(
fontSize: 11, color: Color(0xFFA78BFA)),
),
),
],
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${s.compliance}% التزام',
style: TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w700,
color: _statusColor(s.status),
),
),
const SizedBox(height: 4),
Text(
'${s.studentCount} طالب · ${s.annualFee.toInt()} د.أ/سنة',
style: const TextStyle(
fontSize: 11, color: Color(0xFF64748B)),
),
],
),
],
),
)),
],
),
);
}
// ---------------------------------------------------------------------------
// TAB 2: Teachers Radar & Marketplace Monetization
// ---------------------------------------------------------------------------
Widget _buildTeachersRadarTab(Map<String, dynamic> radar) {
final topTeachers = (radar['top_teachers'] as List? ?? []);
final needingSupport = (radar['needing_support'] as List? ?? []);
return SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Monetization Model Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF14532D), Color(0xFF0F172A)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: const Color(0xFF22C55E).withOpacity(0.4)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(CupertinoIcons.money_dollar_circle_fill,
color: Color(0xFF4ADE80), size: 22),
SizedBox(width: 8),
Text(
'سوق تسييل المعلمين المتميزين (شراكة الأرباح)',
style: TextStyle(
fontSize: 14.5,
fontWeight: FontWeight.w700,
color: Colors.white),
),
],
),
const SizedBox(height: 8),
const Text(
'المعلمون الحاصلون على تقييم 95%+ يتم منحهم اعتماد "معلم صَقِل المعتمد" وتُباع شروحاتهم خارج الثقافة العسكرية مع توزيع العائد: 55% للمعلم، 15% للمديرية، 30% لصَقِل.',
style: TextStyle(
fontSize: 12.5, color: Color(0xFFCBD5E1), height: 1.5),
),
],
),
),
const SizedBox(height: 20),
// Top Teachers List
const Text(
'🌟 أفضل المعلمين المرشحين للتسييل والاعتماد الرسمي:',
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white),
),
const SizedBox(height: 12),
...topTeachers.map((t) => Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: const Color(0xFF10B981).withOpacity(0.4)),
),
child: Row(
children: [
const CircleAvatar(
radius: 20,
backgroundColor: Color(0xFF065F46),
child: Icon(CupertinoIcons.star_fill,
color: Color(0xFF34D399), size: 18),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t['name'] ?? '',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white),
),
const SizedBox(height: 2),
Text(
'${t['subject']} · ${t['school']}',
style: const TextStyle(
fontSize: 12, color: Color(0xFF94A3B8)),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${t['score']}% تقييم AI',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
color: Color(0xFF34D399)),
),
),
],
),
)),
const SizedBox(height: 18),
// Early Intervention Radar
const Text(
'⚠️ رادار الإنذار والتوجيه المبكر (معالجة القصور قبل التوجيهي):',
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white),
),
const SizedBox(height: 12),
...needingSupport.map((ns) => Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: const Color(0xFFEF4444).withOpacity(0.3)),
),
child: Row(
children: [
const CircleAvatar(
radius: 20,
backgroundColor: Color(0xFF7F1D1D),
child: Icon(CupertinoIcons.exclamationmark_triangle_fill,
color: Color(0xFFF87171), size: 18),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ns['name'] ?? '',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white),
),
const SizedBox(height: 2),
Text(
'${ns['school']} · ${ns['issue']}',
style: const TextStyle(
fontSize: 12, color: Color(0xFFF87171)),
),
],
),
),
Text(
'${ns['score']}%',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Color(0xFFF87171)),
),
],
),
)),
],
),
);
}
// ---------------------------------------------------------------------------
// TAB 3: Kingdom Exam Integrity & Statistical Fraud Detection
// ---------------------------------------------------------------------------
Widget _buildExamIntegrityTab(List<ExamAnomalyAlert> anomalies) {
return SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Live Exam Integrity Header
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1E293B), Color(0xFF0F172A)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border:
Border.all(color: const Color(0xFF0284C7).withOpacity(0.5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(CupertinoIcons.shield_fill,
color: Color(0xFF38BDF8), size: 20),
SizedBox(width: 8),
Text(
'غرفة عمليات نزاهة الامتحانات الموحدة',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
color: Colors.white),
),
],
),
const SizedBox(height: 8),
const Text(
'مراقبة لحظية لنحو 19,000 طالب يخوضون الامتحان الموحد المتزامن عبر 43 مدرسة، برصد فوري للسرعة المستحيلة وتكتل الأخطاء.',
style: TextStyle(
fontSize: 12.5, color: Color(0xFF94A3B8), height: 1.5),
),
],
),
),
const SizedBox(height: 20),
const Text(
'تنبيهات الشذوذ الإحصائي وشبهات التواطؤ:',
style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white),
),
const SizedBox(height: 12),
if (anomalies.isEmpty)
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF1E293B)),
),
child: const Center(
child: Text(
'✅ لا توجد أي تنبيهات شذوذ حالياً · كافة المدارس تعمل بانضباط تام',
style: TextStyle(color: Color(0xFF10B981), fontSize: 13.5),
),
),
)
else
...anomalies.map((anm) => Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF1E1B2E),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: const Color(0xFFEF4444).withOpacity(0.5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(
CupertinoIcons
.exclamationmark_triangle_fill,
color: Color(0xFFEF4444),
size: 18),
const SizedBox(width: 8),
Text(
anm.schoolName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white),
),
],
),
Text(
anm.timestamp,
style: const TextStyle(
fontSize: 11.5, color: Color(0xFF94A3B8)),
),
],
),
const SizedBox(height: 8),
Text(
anm.description,
style: const TextStyle(
fontSize: 13,
color: Color(0xFFFCA5A5),
height: 1.4),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'🎥 جاري فتح العينة البانورامية المسجلة (16.5 MB) لتلك الدقيقة للتحقق البصري المباشر'),
backgroundColor: Color(0xFF0284C7),
),
);
},
icon: const Icon(CupertinoIcons.play_circle_fill,
size: 16),
label: const Text(
'عرض العينة البانورامية المسجلة في هذه الدقيقة 👁️',
style: TextStyle(fontSize: 12.5),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0284C7),
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
),
],
),
)),
],
),
);
}
Widget _kpiCell(
String title, String value, IconData icon, Color accentColor) {
return Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: accentColor.withOpacity(0.15),
shape: BoxShape.circle,
),
child: Icon(icon, color: accentColor, size: 20),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 11.5, color: Color(0xFF94A3B8)),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
color: Colors.white),
),
],
)
],
);
}
Color _statusColor(String status) {
switch (status) {
case 'excellent':
return const Color(0xFF10B981);
case 'warning_under_quota':
return const Color(0xFFEF4444);
case 'good':
default:
return const Color(0xFF38BDF8);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,327 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/directorate_models.dart';
class DirectorateApiService {
static const String baseUrl = 'http://127.0.0.1:8000';
/// Fetch Macro Directorate Dashboard (لوحة القائد العام لمدارس الثقافة العسكرية)
static Future<Map<String, dynamic>> fetchDirectorateDashboard() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/api/directorate/dashboard'))
.timeout(const Duration(seconds: 4));
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes));
return data;
}
} catch (_) {
// Fallback for offline / demo mode
}
// High-fidelity fallback
return {
'status': 'success',
'directorate': {
'code': 'MC-JOR',
'name': 'مديرية التربية والتعليم والثقافة العسكرية',
'type': 'military_culture',
'commander_name': 'مدير التعليم والثقافة العسكرية',
'total_schools': 43,
'total_students': 19350,
'total_teachers': 812,
'annual_contract_value': 20000.00,
'recorded_lessons_month': 1420,
'compliance_rate': 94.5,
'ai_average_score': 91.8,
'active_unified_exams': 2,
},
'schools': [
{
'id': 1,
'code': 'SCH-MC-01',
'name': 'مدرسة الشهيد فيصل الثاني الثانوية العسكرية',
'governorate': 'العاصمة',
'weight_tier': 'tier_c_large',
'student_count': 980,
'teacher_count': 48,
'compliance': 98.0,
'status': 'excellent',
'annual_fee': 900.00,
},
{
'id': 2,
'code': 'SCH-MC-02',
'name': 'مدرسة الملك حسين الثانوية العسكرية',
'governorate': 'الزرقاء',
'weight_tier': 'tier_c_large',
'student_count': 890,
'teacher_count': 42,
'compliance': 95.5,
'status': 'excellent',
'annual_fee': 900.00,
},
{
'id': 3,
'code': 'SCH-MC-03',
'name': 'مدرسة صرح الشهيد العسكرية',
'governorate': 'إربد',
'weight_tier': 'tier_b_medium',
'student_count': 460,
'teacher_count': 24,
'compliance': 92.0,
'status': 'good',
'annual_fee': 450.00,
},
{
'id': 4,
'code': 'SCH-MC-04',
'name': 'مدرسة البادية الشمالية الثانوية العسكرية',
'governorate': 'المفرق',
'weight_tier': 'tier_a_small',
'student_count': 150,
'teacher_count': 12,
'compliance': 88.0,
'status': 'warning_under_quota',
'annual_fee': 250.00,
},
{
'id': 5,
'code': 'SCH-MC-05',
'name': 'مدرسة القويرة الثانوية العسكرية',
'governorate': 'العقبة',
'weight_tier': 'tier_a_small',
'student_count': 135,
'teacher_count': 11,
'compliance': 91.0,
'status': 'good',
'annual_fee': 250.00,
},
{
'id': 6,
'code': 'SCH-MC-06',
'name': 'مدرسة الكرك الثانوية العسكرية',
'governorate': 'الكرك',
'weight_tier': 'tier_b_medium',
'student_count': 420,
'teacher_count': 22,
'compliance': 94.0,
'status': 'good',
'annual_fee': 450.00,
},
],
'anomalies': [
{
'id': 'ANM-01',
'type': 'speed_impossible',
'school_name': 'مدرسة البادية الشمالية العسكرية',
'subject': 'الرياضيات العلمي',
'description': 'حل 18 طالباً لمسألة تفاضل في 14 ثانية (تحت التدقيق الإشرافي)',
'severity': 'medium',
'timestamp': 'منذ ساعتين',
}
],
'radar': {
'top_teachers': [
{'name': 'أحمد المجالي', 'school': 'الشهيد فيصل الثاني', 'subject': 'الفيزياء', 'score': 97.4, 'certified_badge': true},
{'name': 'خلدون بني هاني', 'school': 'صرح الشهيد إربد', 'subject': 'الرياضيات', 'score': 96.8, 'certified_badge': true},
{'name': 'طارق الحنيطي', 'school': 'الملك حسين الزرقاء', 'subject': 'الكيمياء', 'score': 95.9, 'certified_badge': true},
],
'needing_support': [
{'name': 'معلم لغة عربية', 'school': 'مدرسة القويرة', 'issue': 'ضعف الأسئلة السقراطية وتجاوز زمن الشرح 35 دقيقة', 'score': 74.2},
]
}
};
}
/// Fetch School Principal Dashboard (لوحة مدير المدرسة والمشرف الميداني)
static Future<Map<String, dynamic>> fetchSchoolDashboard({int schoolId = 1}) async {
try {
final response = await http
.get(Uri.parse('$baseUrl/api/supervisor/school-dashboard?school_id=$schoolId'))
.timeout(const Duration(seconds: 4));
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes));
return data;
}
} catch (_) {
// Fallback
}
return {
'status': 'success',
'school': {
'id': schoolId,
'name': 'مدرسة الشهيد فيصل الثاني الثانوية العسكرية',
'governorate': 'العاصمة - عمان',
'student_count': 980,
'teacher_count': 48,
'weight_tier': 'tier_c_large',
'director_name': 'المقدم الركن / مدير المدرسة',
'biweekly_cycle': 'الدورة الرابعة (الفصل الأول)',
'cycle_progress': '42 من 48 حصة مسجلة (87.5%)',
},
'teachers': [
{
'id': 101,
'name': 'أحمد المجالي',
'subject': 'الفيزياء',
'grade': 'العاشر + الأول ثانوي',
'quota_status': 'completed',
'last_lesson': 'تطبيقات قوانين نيوتن الثالث والمصاعد',
'ai_score': 97.4,
'recorded_at': 'منذ 3 أيام',
},
{
'id': 102,
'name': 'عمر الحباشنة',
'subject': 'الرياضيات العلمي',
'grade': 'التوجيهي 2008',
'quota_status': 'pending',
'last_lesson': 'تطبيقات القيم القصوى والمعدلات المرتبطة',
'ai_score': 94.0,
'recorded_at': 'منذ 12 يوماً',
},
{
'id': 103,
'name': 'سليمان الطراونة',
'subject': 'الكيمياء',
'grade': 'الأول ثانوي',
'quota_status': 'completed',
'last_lesson': 'سرعة التفاعلات ونظرية التصادم',
'ai_score': 92.5,
'recorded_at': 'أمس',
},
{
'id': 104,
'name': 'محمد الغويري',
'subject': 'اللغة الإنجليزية',
'grade': 'العاشر',
'quota_status': 'completed',
'last_lesson': 'Conditional Sentences (Type 2 & 3)',
'ai_score': 96.0,
'recorded_at': 'منذ 5 أيام',
},
],
'active_exams': [
{
'id': 'EX-MC-2026-01',
'title': 'الامتحان الموحد التجريبي - الرياضيات العلمي',
'subject': 'الرياضيات العلمي',
'grade_level': 'التوجيهي 2008',
'scheduled_time': 'اليوم - 09:00 صباحاً (موعد موحد لكافة المدارس)',
'duration_minutes': 60,
'pushed_to_lab': true,
'status': 'in_progress',
'forms': ['نموذج أ', 'نموذج ب'],
'anti_cheating': {
'kiosk_mode_locked': true,
'dynamic_numbers': true,
'smart_sampling': 'ثانيتان كل دقيقة (حجم الإجمالي 16.5 ميجابايت)',
}
}
]
};
}
/// Submit a Video Lesson for AI Pedagogical Audit (تسجيل حصة مرئية)
static Future<RecordedLessonResult> recordLesson({
required String teacherName,
required String subject,
required String lessonTitle,
required double fileSizeMb,
required int durationMinutes,
}) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/supervisor/record-lesson'),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'teacher_name': teacherName,
'subject': subject,
'lesson_title': lessonTitle,
'file_size_mb': fileSizeMb,
'duration_minutes': durationMinutes,
}),
)
.timeout(const Duration(seconds: 4));
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes));
return RecordedLessonResult.fromJson(data['data']);
}
} catch (_) {
// Fallback
}
// Fallback simulated result
return RecordedLessonResult(
lessonUuid: 'LREC-${DateTime.now().millisecondsSinceEpoch}',
teacherName: teacherName,
subject: subject,
lessonTitle: lessonTitle,
fileSizeMb: fileSizeMb,
durationMinutes: durationMinutes,
aiAlignmentScore: 95.5,
teacherTalkRatio: '62%',
status: 'approved_official',
reportSummary:
'التزام تام بالمنهاج الوزاري، تفاعل ممتاز من الطلاب، وطرح سليم للأسئلة السقراطية.',
socraticCheckpoints: [
{
'timestamp': '14:20',
'question': 'ما الفرق بين القصور الذاتي والكتلة التثاقلية؟',
'objective': 'الربط بمنهاج الفيزياء - نتاجات الوحدة الثانية'
},
{
'timestamp': '29:10',
'question': 'إذا انقطع حبل المصعد، ما القوة العمودية المؤثرة على الشخص؟',
'objective': 'تفكير استنتاجي سقراطي'
}
],
);
}
/// Push Exam to School Computer Labs (إرسال الامتحان لمختبرات الحاسوب)
static Future<bool> pushExamToLab(String examId) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/supervisor/exam/push-to-lab'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'exam_id': examId}),
)
.timeout(const Duration(seconds: 3));
return response.statusCode == 200;
} catch (_) {
return true; // Simulate success in offline mode
}
}
/// Upload Smart Sampled Panoramic Surveillance (رفع العينة البانورامية المقتضبة)
static Future<bool> uploadPanoramicSample({
required double fileSizeMb,
required int durationSeconds,
}) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/supervisor/exam/upload-panoramic'),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'file_size_mb': fileSizeMb,
'duration_seconds': durationSeconds,
}),
)
.timeout(const Duration(seconds: 3));
return response.statusCode == 200;
} catch (_) {
return true;
}
}
}