Update Saqel Platform: 2026-09-04 02:04:43
This commit is contained in:
+153
-172
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+797
@@ -0,0 +1,797 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
|
||||
/// ==============================================================================
|
||||
/// SAQEL ENTERPRISE - ARABIC INTERACTIVE LAB (شجرة الإعراب ومختبر العروض)
|
||||
/// ==============================================================================
|
||||
class ArabicInteractiveLabView extends StatefulWidget {
|
||||
final List<Map<String, dynamic>>? customSentences;
|
||||
final String? lessonTopic;
|
||||
|
||||
const ArabicInteractiveLabView({
|
||||
super.key,
|
||||
this.customSentences,
|
||||
this.lessonTopic,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ArabicInteractiveLabView> createState() =>
|
||||
_ArabicInteractiveLabViewState();
|
||||
}
|
||||
|
||||
class _ArabicInteractiveLabViewState extends State<ArabicInteractiveLabView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
// Syntax Tree State
|
||||
int _selectedSentenceIndex = 0;
|
||||
int? _selectedWordIndex;
|
||||
|
||||
List<Map<String, dynamic>> get _sentences =>
|
||||
(widget.customSentences != null && widget.customSentences!.isNotEmpty)
|
||||
? widget.customSentences!
|
||||
: _sampleSentences;
|
||||
|
||||
final List<Map<String, dynamic>> _sampleSentences = [
|
||||
{
|
||||
'fullText': 'إِنَّ العِلْمَ نُورٌ يَهْدِي العُقُولَ إِلَى الحَقِّ',
|
||||
'type': 'جملة اسمية منسوخة بـ (إنَّ)',
|
||||
'rootNode': 'جملة إنَّ وأخواتها',
|
||||
'words': [
|
||||
{
|
||||
'word': 'إِنَّ',
|
||||
'role': 'حرف توكيد ونصب (ناسخ)',
|
||||
'case': 'مبني على الفتح لا محل له من الإعراب',
|
||||
'explanation': 'يدخل على الجملة الاسمية فينصب المبتدأ ويرفع الخبر.',
|
||||
'tag': 'حرف ناسخ',
|
||||
},
|
||||
{
|
||||
'word': 'العِلْمَ',
|
||||
'role': 'اسم إنَّ',
|
||||
'case': 'منصوب وعلامة نصبه الفتحة الظاهرة على آخره',
|
||||
'explanation': 'هو المسند إليه في الأصل، نُصب لدخول الحرف الناسخ عليه.',
|
||||
'tag': 'اسم منصوب',
|
||||
},
|
||||
{
|
||||
'word': 'نُورٌ',
|
||||
'role': 'خبر إنَّ',
|
||||
'case': 'مرفوع وعلامة رفعه الضمة الظاهرة على آخره',
|
||||
'explanation': 'تم به المعنى وأخبر عن اسم إن، وجاء مفرداً.',
|
||||
'tag': 'خبر مرفوع',
|
||||
},
|
||||
{
|
||||
'word': 'يَهْدِي',
|
||||
'role': 'فعل مضارع مرفوع (وجملة فعلية في محل رفع نعت)',
|
||||
'case': 'مرفوع بالضمة المقدرة على الياء للثقل، والفاعل ضمير مستتر',
|
||||
'explanation': 'الجمل بعد النكرات صفات؛ فجملة (يهدي) نعت لكلمة (نور).',
|
||||
'tag': 'فعل + نعت',
|
||||
},
|
||||
{
|
||||
'word': 'العُقُولَ',
|
||||
'role': 'مفعول به',
|
||||
'case': 'منصوب وعلامة نصبه الفتحة الظاهرة',
|
||||
'explanation': 'وقع عليه فعل الهداية من الفاعل المستتر (هو).',
|
||||
'tag': 'مفعول به',
|
||||
},
|
||||
{
|
||||
'word': 'إِلَى الحَقِّ',
|
||||
'role': 'جار ومجرور',
|
||||
'case': 'شبه جملة متعلقة بالفعل (يهدي)',
|
||||
'explanation': 'إلى حرف جر، والحق اسم مجرور بالكسرة الظاهرة.',
|
||||
'tag': 'شبه جملة',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'fullText': 'قَرَأَ الطَّالِبُ المُجْتَهِدُ كِتَابَيْنِ مُفِيدَيْنِ صَبَاحاً',
|
||||
'type': 'جملة فعلية تامة',
|
||||
'rootNode': 'الجملة الفعلية (فعل + فاعل + مفعول به + فضلات)',
|
||||
'words': [
|
||||
{
|
||||
'word': 'قَرَأَ',
|
||||
'role': 'فعل ماضٍ مبني',
|
||||
'case': 'مبني على الفتح الظاهر على آخره',
|
||||
'explanation': 'فعل ماضٍ مجرد، مبني للمعلوم، يدل على حدث في الزمن الماضي.',
|
||||
'tag': 'فعل ماضٍ',
|
||||
},
|
||||
{
|
||||
'word': 'الطَّالِبُ',
|
||||
'role': 'فاعل مرفوع',
|
||||
'case': 'مرفوع وعلامة رفعه الضمة الظاهرة',
|
||||
'explanation': 'من قام بالفعل، معرف بأل، ركن أساسي في الجملة الفعلية.',
|
||||
'tag': 'فاعل مرفوع',
|
||||
},
|
||||
{
|
||||
'word': 'المُجْتَهِدُ',
|
||||
'role': 'نعت (صفة) للطالب',
|
||||
'case': 'مرفوع وعلامة رفعه الضمة الظاهرة',
|
||||
'explanation': 'طابق المنعوت في التعريف، الإفراد، التذكير، وحركة الإعراب.',
|
||||
'tag': 'نعت / صفة',
|
||||
},
|
||||
{
|
||||
'word': 'كِتَابَيْنِ',
|
||||
'role': 'مفعول به منصوب',
|
||||
'case': 'منصوب وعلامة نصبه الياء لأنه مثنى',
|
||||
'explanation': 'وقع عليه فعل القراءة، والمثنى يُنصب بالياء وتُكسر نونه.',
|
||||
'tag': 'مثنى منصوب',
|
||||
},
|
||||
{
|
||||
'word': 'مُفِيدَيْنِ',
|
||||
'role': 'نعت لكتابين',
|
||||
'case': 'منصوب وعلامة نصبه الياء لأنه مثنى',
|
||||
'explanation': 'صفة تابعة للمفعول به في التثنية والتنكير والنصب بالياء.',
|
||||
'tag': 'نعت مثنى',
|
||||
},
|
||||
{
|
||||
'word': 'صَبَاحاً',
|
||||
'role': 'ظرف زمان (مفعول فيه)',
|
||||
'case': 'منصوب وعلامة نصبه الفتحة الظاهرة',
|
||||
'explanation': 'اسم منصوب يدل على زمن وقوع الفعل، متضمن معنى (في).',
|
||||
'tag': 'ظرف زمان',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Poetry Meter State
|
||||
int _selectedMeterIndex = 0;
|
||||
bool _isPlayingBeat = false;
|
||||
int _currentBeatStep = 0;
|
||||
|
||||
final List<Map<String, dynamic>> _poetryMeters = [
|
||||
{
|
||||
'name': 'البحر البسيط',
|
||||
'key': 'إِنَّ البَسِيطَ لَدَيْهِ يُبْسَطُ الأَمَلُ .. مُسْتَفْعِلُنْ فاعِلُنْ مُسْتَفْعِلُنْ فَعِلُ',
|
||||
'verse': 'لِكُلِّ شَيْءٍ إِذَا مَا تَمَّ نُقْصَانُ .. فَلَا يُغَرَّ بِطِيبِ العَيْشِ إِنْسَانُ',
|
||||
'poet': 'أبو البقاء الرندي',
|
||||
'pattern': ['مُسْتَفْعِلُنْ', 'فَاعِلُنْ', 'مُسْتَفْعِلُنْ', 'فَعِلُنْ'],
|
||||
'scansion': ['//0//0', '/0//0', '//0//0', '///0'],
|
||||
'speedBpm': 90,
|
||||
},
|
||||
{
|
||||
'name': 'البحر الوافر',
|
||||
'key': 'بُحُورُ الشِّعْرِ وَافِرُهَا جَمِيلُ .. مُفَاعَلَتُنْ مُفَاعَلَتُنْ فَعُولُ',
|
||||
'verse': 'إِذَا غَامَرْتَ فِي شَرَفٍ مَرُومِ .. فَلَا تَقْنَعْ بِمَا دُونَ النُّجُومِ',
|
||||
'poet': 'المتنبي',
|
||||
'pattern': ['مُفَاعَلَتُنْ', 'مُفَاعَلَتُنْ', 'فَعُولُنْ'],
|
||||
'scansion': ['//0///0', '//0///0', '//0/0'],
|
||||
'speedBpm': 105,
|
||||
},
|
||||
{
|
||||
'name': 'البحر الكامل',
|
||||
'key': 'كَمُلَ الجَمَالُ مِنَ البُحُورِ الكَامِلُ .. مُتَفَاعِلُنْ مُتَفَاعِلُنْ مُتَفَاعِلُ',
|
||||
'verse': 'وَإِذَا صَحَوْتُ فَمَا أُقَصِّرُ عَنْ نَدَى .. وَكَمَا عَلِمْتِ شَمَائِلِي وَتَكَرُّمِي',
|
||||
'poet': 'عنترة بن شداد',
|
||||
'pattern': ['مُتَفَاعِلُنْ', 'مُتَفَاعِلُنْ', 'مُتَفَاعِلُنْ'],
|
||||
'scansion': ['///0//0', '///0//0', '///0//0'],
|
||||
'speedBpm': 115,
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0xFF070B12),
|
||||
child: Column(
|
||||
children: [
|
||||
// Luxury Tab Switcher
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF0E1626),
|
||||
border: Border(bottom: BorderSide(color: Color(0xFF1E293B))),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_tabButton(
|
||||
index: 0,
|
||||
title: 'شجرة الإعراب النحوية 🌳',
|
||||
icon: CupertinoIcons.arrow_branch,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_tabButton(
|
||||
index: 1,
|
||||
title: 'مختبر العروض والأوزان 🎵',
|
||||
icon: CupertinoIcons.music_note_list,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: IndexedStack(
|
||||
index: _tabController.index,
|
||||
children: [
|
||||
_buildSyntaxTreeTab(),
|
||||
_buildPoetryMeterTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabButton({
|
||||
required int index,
|
||||
required String title,
|
||||
required IconData icon,
|
||||
}) {
|
||||
final isSelected = _tabController.index == index;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_tabController.animateTo(index);
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF10B981) : const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 16,
|
||||
color: isSelected ? Colors.black : const Color(0xFF94A3B8)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w600,
|
||||
color: isSelected ? Colors.black : const Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TAB 1: Visual Interactive Syntax Tree
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildSyntaxTreeTab() {
|
||||
final safeIndex = _selectedSentenceIndex.clamp(0, _sentences.length - 1);
|
||||
final sentence = _sentences[safeIndex];
|
||||
final words = (sentence['words'] as List);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Selector for Sentences
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.lessonTopic != null
|
||||
? 'شجرة إعراب درس: ${widget.lessonTopic}'
|
||||
: 'اختر الجملة لتحليل شجرتها الإعرابية:',
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF10B981).withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
widget.customSentences != null ? 'أمثلة الكتاب + ذكاء اصطناعي' : 'وزاري · التوجيهي 2008',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF34D399)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Sentence Switcher Buttons
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(_sentences.length, (idx) {
|
||||
final isCur = safeIndex == idx;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_selectedSentenceIndex = idx;
|
||||
_selectedWordIndex = null;
|
||||
}),
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: idx == 0 ? 8 : 0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isCur
|
||||
? const Color(0xFF1E293B)
|
||||
: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isCur
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFF1E293B),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'الجملة ${idx + 1}',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: isCur ? FontWeight.w800 : FontWeight.w500,
|
||||
color: isCur ? Colors.white : const Color(0xFF64748B),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Main Sentence Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
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: Column(
|
||||
children: [
|
||||
Text(
|
||||
sentence['fullText'],
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
height: 1.6,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
sentence['type'],
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6EE7B7),
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Tree Interactive Decomposition (Word Cards)
|
||||
const Text(
|
||||
'انقر على أي كلمة لتفكيك عقدتها النحوية وأحكامها:',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: List.generate(words.length, (idx) {
|
||||
final w = words[idx];
|
||||
final isSel = _selectedWordIndex == idx;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedWordIndex = idx),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSel
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSel
|
||||
? const Color(0xFF34D399)
|
||||
: const Color(0xFF1E293B),
|
||||
width: isSel ? 2 : 1,
|
||||
),
|
||||
boxShadow: isSel
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF10B981).withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
)
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
w['word'],
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: isSel ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
w['tag'],
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSel
|
||||
? const Color(0xFF064E3B)
|
||||
: const Color(0xFF64748B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Detailed Syntactic Breakdown Card
|
||||
if (_selectedWordIndex != null)
|
||||
_buildWordDetailCard(words[_selectedWordIndex!])
|
||||
else
|
||||
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(0xFF64748B), fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWordDetailCard(Map<String, dynamic> wordData) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF10B981).withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF10B981).withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF10B981).withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(CupertinoIcons.checkmark_seal_fill,
|
||||
color: Color(0xFF10B981), size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'التحليل الإعرابي لكلمة: ( ${wordData['word']} )',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
wordData['tag'],
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF34D399),
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Role
|
||||
_infoRow('الموقع الإعرابي', wordData['role'], const Color(0xFF38BDF8)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Case & Sign
|
||||
_infoRow('الحالة والعلامة', wordData['case'], const Color(0xFFFBBF24)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Pedagogical Socratic explanation
|
||||
_infoRow('التعليل والتوجيه الوزاري', wordData['explanation'],
|
||||
const Color(0xFFCBD5E1)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(String label, String value, Color valueColor) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF64748B)),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: valueColor,
|
||||
height: 1.4),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TAB 2: Poetry Prosody & Metronome Lab
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildPoetryMeterTab() {
|
||||
final meter = _poetryMeters[_selectedMeterIndex];
|
||||
final patterns = (meter['pattern'] as List);
|
||||
final scansions = (meter['scansion'] as List);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Meter Selection Chips
|
||||
Row(
|
||||
children: List.generate(_poetryMeters.length, (idx) {
|
||||
final m = _poetryMeters[idx];
|
||||
final isCur = _selectedMeterIndex == idx;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_selectedMeterIndex = idx;
|
||||
_isPlayingBeat = false;
|
||||
}),
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: idx < _poetryMeters.length - 1 ? 8 : 0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isCur
|
||||
? const Color(0xFF047857)
|
||||
: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isCur
|
||||
? const Color(0xFF34D399)
|
||||
: const Color(0xFF1E293B),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
m['name'],
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: isCur ? FontWeight.w800 : FontWeight.w600,
|
||||
color: isCur ? Colors.white : const Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Key & Verse Banner
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF1E293B)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'مفتاح ${meter['name']}:',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF34D399)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
meter['key'],
|
||||
style: const TextStyle(
|
||||
fontSize: 14.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
height: 1.5),
|
||||
),
|
||||
const Divider(color: Color(0xFF1E293B), height: 24),
|
||||
Text(
|
||||
'بيت تطبيقي وزاري (${meter['poet']}):',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFFF59E0B)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
meter['verse'],
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFFFEF08A),
|
||||
height: 1.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Interactive Scansion / Tafilat Visualizer
|
||||
const Text(
|
||||
'التفعيلات والتقطيع العروضي الصوتي (متحرك / وساكن 0):',
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
children: List.generate(patterns.length, (idx) {
|
||||
final pat = patterns[idx];
|
||||
final scan = scansions[idx];
|
||||
return Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: idx < patterns.length - 1 ? 8 : 0),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF1E293B), Color(0xFF0F172A)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF334155)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
pat,
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFF38BDF8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
scan,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 2,
|
||||
color: Color(0xFF34D399),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Audio Metronome Simulation Button
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isPlayingBeat = !_isPlayingBeat;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
_isPlayingBeat
|
||||
? CupertinoIcons.pause_circle_fill
|
||||
: CupertinoIcons.play_circle_fill,
|
||||
size: 20),
|
||||
label: Text(
|
||||
_isPlayingBeat
|
||||
? 'إيقاف الإيقاع العروضي التفاعلي'
|
||||
: 'عزف الإيقاع العروضي الموزون (${meter['speedBpm']} نقرة/دقيقة) 🥁',
|
||||
style: const TextStyle(fontSize: 13.5, fontWeight: FontWeight.w700),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _isPlayingBeat
|
||||
? const Color(0xFFB91C1C)
|
||||
: const Color(0xFF059669),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
|
||||
/// ==============================================================================
|
||||
/// SAQEL ENTERPRISE - JORDAN HISTORY INTERACTIVE TIMELINE (الخط الزمني لتاريخ الأردن)
|
||||
/// ==============================================================================
|
||||
class HistoryInteractiveTimelineView extends StatefulWidget {
|
||||
const HistoryInteractiveTimelineView({super.key});
|
||||
|
||||
@override
|
||||
State<HistoryInteractiveTimelineView> createState() =>
|
||||
_HistoryInteractiveTimelineViewState();
|
||||
}
|
||||
|
||||
class _HistoryInteractiveTimelineViewState
|
||||
extends State<HistoryInteractiveTimelineView> {
|
||||
int _selectedMilestoneIndex = 0;
|
||||
|
||||
final List<Map<String, dynamic>> _milestones = [
|
||||
{
|
||||
'year': '1921',
|
||||
'exactDate': '2 آذار 1921',
|
||||
'title': 'تأسيس إمارة شرق الأردن',
|
||||
'era': 'عهد الملك المؤسس عبد الله الأول',
|
||||
'location': 'معان ➔ عمان',
|
||||
'summary':
|
||||
'وصول الأمير عبد الله الأول بن الحسين إلى معان في تشرين الثاني 1920 ثم إلى عمان في 2 آذار 1921، وتشكيل أول حكومة مركزية برئاسة رشيد طليع في 11 نيسان 1921.',
|
||||
'tawjihiFocus':
|
||||
'سؤال وزاري مكرر: من هو رئيس أول حكومة أردنية؟ (رشيد طليع)، وتاريخ وصول الأمير إلى عمان (2 آذار 1921).',
|
||||
'significance': 'نشأة الدولة الأردنية الحديثة بعد الثورة العربية الكبرى.',
|
||||
'accentColor': Color(0xFF8B5CF6),
|
||||
},
|
||||
{
|
||||
'year': '1928',
|
||||
'exactDate': '16 نيسان 1928',
|
||||
'title': 'صدور القانون الأساسي والمؤتمر الوطني الأول',
|
||||
'era': 'عهد الملك المؤسس عبد الله الأول',
|
||||
'location': 'عمان - مقهى حمدان',
|
||||
'summary':
|
||||
'توقيع المعاهدة الأردنية البريطانية، وصدور أول دستور للإمارة (القانون الأساسي)، وانعقاد المؤتمر الوطني الأول برئاسة حسين الطراونة ووضع الميثاق القومي.',
|
||||
'tawjihiFocus':
|
||||
'سؤال وزاري مكرر: من ترأس المؤتمر الوطني الأول عام 1928؟ (حسين الطراونة)، وما اسم أول دستور؟ (القانون الأساسي 1928).',
|
||||
'significance': 'أول وثيقة دستورية وطنية تنظم الحياة السياسية والنيابية.',
|
||||
'accentColor': Color(0xFF38BDF8),
|
||||
},
|
||||
{
|
||||
'year': '1946',
|
||||
'exactDate': '25 أيار 1946',
|
||||
'title': 'استقلال المملكة الأردنية الهاشمية',
|
||||
'era': 'عهد الملك المؤسس عبد الله الأول',
|
||||
'location': 'عمان - قصر رغدان العامر',
|
||||
'summary':
|
||||
'إعلان المجلس التشريعي الأردني الخامس استقلال البلاد التام ومبايعة الملك عبد الله الأول ملكاً دستورياً، وتحويل اسم الدولة إلى المملكة الأردنية الهاشمية.',
|
||||
'tawjihiFocus':
|
||||
'سؤال وزاري مكرر: ما هو رقم المجلس التشريعي الذي أعلن الاستقلال؟ (المجلس الخامس)، واليوم الوطني (25 أيار 1946).',
|
||||
'significance': 'إنهاء الانتداب البريطاني واعتراف دولي كامل بسيادة المملكة.',
|
||||
'accentColor': Color(0xFF10B981),
|
||||
},
|
||||
{
|
||||
'year': '1952',
|
||||
'exactDate': '8 كانون الثاني 1952',
|
||||
'title': 'صدور الدستور الأردني المتطور وديوان المحاسبة',
|
||||
'era': 'عهد الملك طلال بن عبد الله',
|
||||
'location': 'عمان',
|
||||
'summary':
|
||||
'إصدار دستور 1952 العصري الذي نص على أن الأمة مصدر السلطات والوزارة مسؤولة أمام مجلس النواب، وتأسيس ديوان المحاسبة وإقرار مجانية وإلزامية التعليم.',
|
||||
'tawjihiFocus':
|
||||
'سؤال وزاري مكرر: في عهد من صدر دستور 1952 وتأسس ديوان المحاسبة؟ (الملك طلال بن عبد الله).',
|
||||
'significance': 'ترسيخ المبادئ الديمقراطية ومسؤولية السلطة التنفيذية أمام البرلمان.',
|
||||
'accentColor': Color(0xFFF59E0B),
|
||||
},
|
||||
{
|
||||
'year': '1956',
|
||||
'exactDate': '1 آذار 1956',
|
||||
'title': 'تعريب قيادة الجيش العربي وطرد كلوب باشا',
|
||||
'era': 'عهد الملك الباني الحسين بن طلال',
|
||||
'location': 'القيادة العامة - عمان',
|
||||
'summary':
|
||||
'القرار الوطني الشجاع لجلالة الملك الحسين بن طلال بإنهاء خدمة الفريق كلوب والضباط الإنجليز، وتعيين اللواء راضي عناب رئيساً للأركان، لتصبح القيادة أردنية خالصة.',
|
||||
'tawjihiFocus':
|
||||
'سؤال وزاري مكرر: من هو أول رئيس أركان أردني بعد التعريب؟ (راضي عناب)، والتاريخ (1 آذار 1956).',
|
||||
'significance': 'استرداد السيادة العسكرية الوطنية الكاملة للجيش العربي.',
|
||||
'accentColor': Color(0xFFEF4444),
|
||||
},
|
||||
{
|
||||
'year': '1968',
|
||||
'exactDate': '21 آذار 1968',
|
||||
'title': 'معركة الكرامة الخالدة وتحطيم أسطورة العدو',
|
||||
'era': 'عهد الملك الباني الحسين بن طلال',
|
||||
'location': 'غور الأردن - بلدة الكرامة',
|
||||
'summary':
|
||||
'تصدي القوات المسلحة الأردنية - الجيش العربي الباسل للعدوان الإسرائيلي وإلحاق أول هزيمة عسكرية مدوية بجيش الاحتلال، وإجباره على طلب وقف إطلاق النار لأول مرة في تاريخه.',
|
||||
'tawjihiFocus':
|
||||
'سؤال وزاري مكرر: ما هي أول معركة عربية حطمت أسطورة الجيش الإسرائيلي؟ (معركة الكرامة 21 آذار 1968).',
|
||||
'significance': 'استعادة الكرامة العسكرية العربية وإثبات صلابة الجندي الأردني.',
|
||||
'accentColor': Color(0xFF10B981),
|
||||
},
|
||||
{
|
||||
'year': '1999',
|
||||
'exactDate': '7 شباط 1999 - الحاضر',
|
||||
'title': 'تولي الملك عبد الله الثاني ومسارات التحديث',
|
||||
'era': 'عهد الملك المعزز عبد الله الثاني ابن الحسين',
|
||||
'location': 'المملكة الأردنية الهاشمية',
|
||||
'summary':
|
||||
'انطلاق عهد التعزيز والتحول الرقمي، وإطلاق الأوراق النقاشية الملكية السبع لتطوير الديمقراطية، ومنظومة التحديث السياسي والاقتصادي والإداري لمئوية الدولة الثانية.',
|
||||
'tawjihiFocus':
|
||||
'سؤال وزاري مكرر: موضوع الورقة النقاشية الأولى (بناء الديمقراطية) والسابعة (التعليم وتطوير الموارد البشرية).',
|
||||
'significance': 'تحديث منظومة الدولة الأردنية ومواكبة الثورة الصناعية والذكاء الاصطناعي.',
|
||||
'accentColor': Color(0xFFFBBF24),
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cur = _milestones[_selectedMilestoneIndex];
|
||||
final accent = cur['accentColor'] as Color;
|
||||
|
||||
return Container(
|
||||
color: const Color(0xFF070B12),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Top Timeline Navigation Bar (Scrubbable Milestones)
|
||||
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),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
cur['year'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: accent),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Horizontal Years Scroller
|
||||
SizedBox(
|
||||
height: 58,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _milestones.length,
|
||||
itemBuilder: (ctx, idx) {
|
||||
final m = _milestones[idx];
|
||||
final isSel = _selectedMilestoneIndex == idx;
|
||||
final mColor = m['accentColor'] as Color;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
setState(() => _selectedMilestoneIndex = idx),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.only(left: 10),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSel
|
||||
? mColor
|
||||
: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSel ? Colors.white : Colors.transparent,
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: isSel
|
||||
? [
|
||||
BoxShadow(
|
||||
color: mColor.withOpacity(0.4),
|
||||
blurRadius: 10,
|
||||
)
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
m['year'],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isSel ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
m['title'].toString().split(' ').first,
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSel
|
||||
? Colors.black.withOpacity(0.8)
|
||||
: const Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Active Milestone Hero Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [accent.withOpacity(0.2), const Color(0xFF0F172A)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: accent.withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accent.withOpacity(0.15),
|
||||
blurRadius: 20,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
cur['title'],
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
cur['exactDate'],
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: accent),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.person_crop_circle_badge_checkmark,
|
||||
size: 14, color: Color(0xFF94A3B8)),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
cur['era'],
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
const Icon(CupertinoIcons.placemark_fill,
|
||||
size: 14, color: Color(0xFF94A3B8)),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
cur['location'],
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: Color(0xFF1E293B), height: 24),
|
||||
|
||||
// Summary
|
||||
const Text(
|
||||
'الوقائع والأحداث التاريخية:',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFFCBD5E1)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
cur['summary'],
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5, color: Colors.white, height: 1.5),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Significance
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.flag_fill,
|
||||
color: Color(0xFF38BDF8), size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'الأثر والنتيجة: ${cur['significance']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: Color(0xFFE2E8F0),
|
||||
height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Tawjihi Ministerial Focus Box
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF451A03),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF59E0B).withOpacity(0.5)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.lightbulb_fill,
|
||||
color: Color(0xFFFBBF24), size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'💡 زاوية أسئلة التوجيهي الوزارية المتكررة:',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFFFBBF24)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
cur['tawjihiFocus'],
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: Color(0xFFFEF3C7),
|
||||
height: 1.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+946
@@ -0,0 +1,946 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
|
||||
/// ==============================================================================
|
||||
/// SAQEL ENTERPRISE - ISLAMIC INHERITANCE & TAJWEED LAB (حاسبة المواريث والتجويد)
|
||||
/// ==============================================================================
|
||||
class IslamicInteractiveLabView extends StatefulWidget {
|
||||
const IslamicInteractiveLabView({super.key});
|
||||
|
||||
@override
|
||||
State<IslamicInteractiveLabView> createState() =>
|
||||
_IslamicInteractiveLabViewState();
|
||||
}
|
||||
|
||||
class _IslamicInteractiveLabViewState extends State<IslamicInteractiveLabView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
// Inheritance Calculator State
|
||||
bool _deceasedIsMale = true; // True = توفي رجل (له زوجة), False = توفيت امرأة (لها زوج)
|
||||
double _estateAmountJod = 120000.0;
|
||||
bool _hasFather = true;
|
||||
bool _hasMother = true;
|
||||
int _sonsCount = 2;
|
||||
int _daughtersCount = 1;
|
||||
int _brothersCount = 2;
|
||||
|
||||
// Calculation Results
|
||||
List<Map<String, dynamic>> _sharesResults = [];
|
||||
String _issueBaseNumber = '24'; // أصل المسألة
|
||||
String _shariaSummary = '';
|
||||
|
||||
// Tajweed State
|
||||
int _selectedRuleIndex = 0;
|
||||
final List<Map<String, dynamic>> _tajweedRules = [
|
||||
{
|
||||
'title': 'الإظهار الحلقي',
|
||||
'letters': 'ء ، هـ ، ع ، ح ، غ ، خ (أخي هاك علماً حازه غير خاسر)',
|
||||
'definition': 'إخراج النون الساكنة أو التنوين من مخرجها بغير غنة ظاهرة عند ملاقاة أحد حروف الحلق الستة.',
|
||||
'examples': [
|
||||
{'text': 'مَنْ آمَنَ', 'highlight': 'نْ + ء', 'surah': 'البقرة'},
|
||||
{'text': 'يَنْهَوْنَ', 'highlight': 'نْ + هـ', 'surah': 'الأنعام'},
|
||||
{'text': 'عَلِيمٌ حَكِيمٌ', 'highlight': 'تنوين + ح', 'surah': 'النساء'},
|
||||
],
|
||||
'accentColor': Color(0xFF10B981),
|
||||
},
|
||||
{
|
||||
'title': 'الإدغام (بغنة وبغير غنة)',
|
||||
'letters': 'ي ، ر ، م ، ل ، و ، ن (يرملون)',
|
||||
'definition': 'إدخال حرف ساكن في حرف متحرك بحيث يصيران حرفاً واحداً مشدداً. بغنة في (ينمو) وبغير غنة في (الراء واللام).',
|
||||
'examples': [
|
||||
{'text': 'مَن يَقُولُ', 'highlight': 'إدغام بغنة (نْ + ي)', 'surah': 'البقرة'},
|
||||
{'text': 'مِن رَّبِّهِمْ', 'highlight': 'إدغام بغير غنة (نْ + ر)', 'surah': 'البقرة'},
|
||||
{'text': 'مِّن وَالٍ', 'highlight': 'إدغام بغنة (نْ + و)', 'surah': 'الرعد'},
|
||||
],
|
||||
'accentColor': Color(0xFF38BDF8),
|
||||
},
|
||||
{
|
||||
'title': 'الإقلاب',
|
||||
'letters': 'حرف الباء (ب) فقط مع وضع ميم صغيرة (مـ)',
|
||||
'definition': 'قلب النون الساكنة أو التنوين ميماً مخفاة بغنة عند ملاقاة حرف الباء.',
|
||||
'examples': [
|
||||
{'text': 'مِن بَعْدِ', 'highlight': 'نْ + ب ➔ ميم مخفاة', 'surah': 'البقرة'},
|
||||
{'text': 'أَنبِئْهُم', 'highlight': 'نْ + ب في كلمة واحدة', 'surah': 'البقرة'},
|
||||
{'text': 'سَمِيعٌ بَصِيرٌ', 'highlight': 'تنوين + ب', 'surah': 'لقمان'},
|
||||
],
|
||||
'accentColor': Color(0xFFF59E0B),
|
||||
},
|
||||
{
|
||||
'title': 'الإخفاء الحقيقي',
|
||||
'letters': '15 حرفاً (ص، ذ، ث، ك، ج، ش، ق، س، د، ط، ز، ف، ت، ض، ظ)',
|
||||
'definition': 'النطق بالنون الساكنة أو التنوين بحالة بين الإظهار والإدغام مع بقاء الغنة بمقدار حركتين.',
|
||||
'examples': [
|
||||
{'text': 'مِن قَبْلُ', 'highlight': 'نْ + ق', 'surah': 'البقرة'},
|
||||
{'text': 'كِتَابٌ كَرِيمٌ', 'highlight': 'تنوين + ك', 'surah': 'النمل'},
|
||||
{'text': 'أَنفُسَكُمْ', 'highlight': 'نْ + ف', 'surah': 'النساء'},
|
||||
],
|
||||
'accentColor': Color(0xFFA78BFA),
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_calculateInheritance();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _calculateInheritance() {
|
||||
final results = <Map<String, dynamic>>[];
|
||||
final hasChildren = (_sonsCount + _daughtersCount) > 0;
|
||||
double remainingEstate = _estateAmountJod;
|
||||
|
||||
// 1. Spouse Share (الزوج أو الزوجة)
|
||||
if (_deceasedIsMale) {
|
||||
// Deceased is male -> Wife inherits
|
||||
final shareFraction = hasChildren ? '1/8 (الثمن)' : '1/4 (الربع)';
|
||||
final double amount = hasChildren
|
||||
? (_estateAmountJod * (1.0 / 8.0))
|
||||
: (_estateAmountJod * (1.0 / 4.0));
|
||||
remainingEstate -= amount;
|
||||
results.add({
|
||||
'heir': 'الزوجة',
|
||||
'share': shareFraction,
|
||||
'amount': amount,
|
||||
'basis': hasChildren
|
||||
? 'تستحق الثمن لوجود الفرع الوارث (الأبناء والبنات)'
|
||||
: 'تستحق الربع لعدم وجود فرع وارث',
|
||||
});
|
||||
} else {
|
||||
// Deceased is female -> Husband inherits
|
||||
final shareFraction = hasChildren ? '1/4 (الربع)' : '1/2 (النصف)';
|
||||
final double amount = hasChildren
|
||||
? (_estateAmountJod * (1.0 / 4.0))
|
||||
: (_estateAmountJod * (1.0 / 2.0));
|
||||
remainingEstate -= amount;
|
||||
results.add({
|
||||
'heir': 'الزوج',
|
||||
'share': shareFraction,
|
||||
'amount': amount,
|
||||
'basis': hasChildren
|
||||
? 'يستحق الربع لوجود الفرع الوارث'
|
||||
: 'يستحق النصف لعدم وجود الفرع الوارث',
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Mother Share (الأم)
|
||||
if (_hasMother) {
|
||||
final shareFraction = (hasChildren || _brothersCount > 1)
|
||||
? '1/6 (السدس)'
|
||||
: '1/3 (الثلث)';
|
||||
final double amount = (hasChildren || _brothersCount > 1)
|
||||
? (_estateAmountJod * (1.0 / 6.0))
|
||||
: (_estateAmountJod * (1.0 / 3.0));
|
||||
remainingEstate -= amount;
|
||||
results.add({
|
||||
'heir': 'الأم',
|
||||
'share': shareFraction,
|
||||
'amount': amount,
|
||||
'basis': (hasChildren || _brothersCount > 1)
|
||||
? 'تستحق السدس لوجود الفرع الوارث أو جمع من الإخوة'
|
||||
: 'تستحق الثلث لعدم وجود فرع وارث أو جمع من الإخوة',
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Father Share (الأب)
|
||||
if (_hasFather) {
|
||||
if (_sonsCount > 0) {
|
||||
// Sons exist -> Father takes 1/6 strictly
|
||||
final double amount = _estateAmountJod * (1.0 / 6.0);
|
||||
remainingEstate -= amount;
|
||||
results.add({
|
||||
'heir': 'الأب',
|
||||
'share': '1/6 (السدس فرضاً)',
|
||||
'amount': amount,
|
||||
'basis': 'يستحق السدس فرضاً فقط لوجود فرع وارث مذكر (الابن)',
|
||||
});
|
||||
} else if (_daughtersCount > 0) {
|
||||
// Only daughters -> Father takes 1/6 + Asabah (residuary)
|
||||
final double fardh = _estateAmountJod * (1.0 / 6.0);
|
||||
remainingEstate -= fardh;
|
||||
results.add({
|
||||
'heir': 'الأب',
|
||||
'share': '1/6 فرضاً + الباقي تعصيباً',
|
||||
'amount': fardh,
|
||||
'basis': 'يستحق السدس فرضاً مع البنت + الباقي تعصيباً إن تبقّى شيء',
|
||||
});
|
||||
} else {
|
||||
// No children -> Father is pure Asabah
|
||||
results.add({
|
||||
'heir': 'الأب',
|
||||
'share': 'عصبة بالنفس (الباقي بالكامل)',
|
||||
'amount': remainingEstate,
|
||||
'basis': 'عصبة بالنفس لعدم وجود فرع وارث، يحوز الباقي بعد أصحاب الفروض',
|
||||
});
|
||||
remainingEstate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Children Share (الأبناء والبنات - للذكر مثل حظ الأنثيين)
|
||||
if (hasChildren && remainingEstate > 0) {
|
||||
final totalShares = (_sonsCount * 2) + _daughtersCount;
|
||||
if (totalShares > 0) {
|
||||
final shareUnit = remainingEstate / totalShares;
|
||||
|
||||
if (_sonsCount > 0) {
|
||||
final totalSonsAmount = shareUnit * 2 * _sonsCount;
|
||||
results.add({
|
||||
'heir': 'الأبناء الذكور ($_sonsCount)',
|
||||
'share': 'عصبة بالغير (للذكر مثل حظ الأنثيين)',
|
||||
'amount': totalSonsAmount,
|
||||
'basis':
|
||||
'لكل ابن ${((shareUnit * 2)).toStringAsFixed(0)} د.أ (ضعف حصة البنت)',
|
||||
});
|
||||
}
|
||||
|
||||
if (_daughtersCount > 0) {
|
||||
final totalDaughtersAmount = shareUnit * _daughtersCount;
|
||||
results.add({
|
||||
'heir': 'البنات الإناث ($_daughtersCount)',
|
||||
'share': _sonsCount > 0
|
||||
? 'عصبة بالغير مع الأبناء'
|
||||
: (_daughtersCount == 1 ? '1/2 (النصف)' : '2/3 (الثلثان)'),
|
||||
'amount': totalDaughtersAmount,
|
||||
'basis': 'لكل بنت ${(shareUnit).toStringAsFixed(0)} د.أ بالتساوي',
|
||||
});
|
||||
}
|
||||
remainingEstate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Brothers & Hijb check (حجب الإخوة)
|
||||
if (_brothersCount > 0) {
|
||||
if (_hasFather || _sonsCount > 0) {
|
||||
results.add({
|
||||
'heir': 'الإخوة الأشقاء ($_brothersCount)',
|
||||
'share': 'محجوبون حجب حرمان (صفر)',
|
||||
'amount': 0.0,
|
||||
'basis': _sonsCount > 0
|
||||
? 'حُجبوا بالأصل والفرع المذكر (حجب حرمان تام بالابن)'
|
||||
: 'حُجبوا بوجود الأب المباشر',
|
||||
});
|
||||
} else if (!hasChildren && remainingEstate > 0) {
|
||||
results.add({
|
||||
'heir': 'الإخوة الأشقاء ($_brothersCount)',
|
||||
'share': 'عصبة بالنفس (اقتسام الباقي)',
|
||||
'amount': remainingEstate,
|
||||
'basis': 'يقتسمون الباقي تعصيباً لعدم وجود من يحجبهم',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_sharesResults = results;
|
||||
_issueBaseNumber = hasChildren ? '24' : '12';
|
||||
_shariaSummary =
|
||||
'أصل المسألة من ($_issueBaseNumber) سهماً، تم استيفاء أصحاب الفروض ثم انتقال الباقي للعصبات مع مراعاة قواعد الحجب الشرعي المقررة بوزارة التربية والتعليم.';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0xFF070B12),
|
||||
child: Column(
|
||||
children: [
|
||||
// Sub-Tab Switcher
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF0E1626),
|
||||
border: Border(bottom: BorderSide(color: Color(0xFF1E293B))),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_tabButton(
|
||||
index: 0,
|
||||
title: 'حاسبة المواريث والتركات ⚖️',
|
||||
icon: CupertinoIcons.money_dollar_circle_fill,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_tabButton(
|
||||
index: 1,
|
||||
title: 'مختبر أحكام التجويد 📖',
|
||||
icon: CupertinoIcons.book_circle_fill,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: IndexedStack(
|
||||
index: _tabController.index,
|
||||
children: [
|
||||
_buildInheritanceCalculatorTab(),
|
||||
_buildTajweedLabTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabButton({
|
||||
required int index,
|
||||
required String title,
|
||||
required IconData icon,
|
||||
}) {
|
||||
final isSelected = _tabController.index == index;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _tabController.animateTo(index)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFFF59E0B) : const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 16,
|
||||
color: isSelected ? Colors.black : const Color(0xFF94A3B8)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w600,
|
||||
color: isSelected ? Colors.black : const Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TAB 1: Islamic Inheritance & Estate Calculator
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildInheritanceCalculatorTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Banner
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF78350F), Color(0xFF0F172A)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border:
|
||||
Border.all(color: const Color(0xFFF59E0B).withOpacity(0.4)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.sparkles, color: Color(0xFFFBBF24), size: 22),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'حاسبة المواريث المطابقة لكتاب التربية الإسلامية (التوجيهي الوزاري 2008) مع أصول المسائل وقواعد الحجب والعصبات.',
|
||||
style: TextStyle(
|
||||
fontSize: 12.5, color: Colors.white, height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Inputs Card
|
||||
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: [
|
||||
const Text(
|
||||
'1. المتوفى ومقدار التركة:',
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Gender Switcher
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _genderButton(
|
||||
title: 'المتوفى رجل (له زوجة)',
|
||||
isSelected: _deceasedIsMale,
|
||||
onTap: () {
|
||||
setState(() => _deceasedIsMale = true);
|
||||
_calculateInheritance();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _genderButton(
|
||||
title: 'المتوفاة امرأة (لها زوج)',
|
||||
isSelected: !_deceasedIsMale,
|
||||
onTap: () {
|
||||
setState(() => _deceasedIsMale = false);
|
||||
_calculateInheritance();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Estate Slider
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('مقدار التركة المالية:',
|
||||
style:
|
||||
TextStyle(fontSize: 12, color: Color(0xFF94A3B8))),
|
||||
Text('${_estateAmountJod.toStringAsFixed(0)} دينار أردني',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFFFBBF24))),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _estateAmountJod,
|
||||
min: 10000,
|
||||
max: 500000,
|
||||
divisions: 49,
|
||||
activeColor: const Color(0xFFF59E0B),
|
||||
inactiveColor: const Color(0xFF1E293B),
|
||||
onChanged: (val) {
|
||||
setState(() => _estateAmountJod = val);
|
||||
_calculateInheritance();
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(color: Color(0xFF1E293B), height: 20),
|
||||
|
||||
// Heirs Configuration
|
||||
const Text(
|
||||
'2. تحديد الورثة الشرعيين:',
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Parents Checkboxes
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
value: _hasFather,
|
||||
title: const Text('الأب حي',
|
||||
style:
|
||||
TextStyle(color: Colors.white, fontSize: 13)),
|
||||
activeColor: const Color(0xFFF59E0B),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (val) {
|
||||
setState(() => _hasFather = val ?? true);
|
||||
_calculateInheritance();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
value: _hasMother,
|
||||
title: const Text('الأم حية',
|
||||
style:
|
||||
TextStyle(color: Colors.white, fontSize: 13)),
|
||||
activeColor: const Color(0xFFF59E0B),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (val) {
|
||||
setState(() => _hasMother = val ?? true);
|
||||
_calculateInheritance();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Sons & Daughters Counters
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _counterControl(
|
||||
label: 'الأبناء الذكور',
|
||||
count: _sonsCount,
|
||||
onChanged: (val) {
|
||||
setState(() => _sonsCount = val);
|
||||
_calculateInheritance();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _counterControl(
|
||||
label: 'البنات الإناث',
|
||||
count: _daughtersCount,
|
||||
onChanged: (val) {
|
||||
setState(() => _daughtersCount = val);
|
||||
_calculateInheritance();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Brothers Counter
|
||||
_counterControl(
|
||||
label: 'الإخوة الأشقاء (تطبيق قواعد الحجب)',
|
||||
count: _brothersCount,
|
||||
onChanged: (val) {
|
||||
setState(() => _brothersCount = val);
|
||||
_calculateInheritance();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Output Distribution Table
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'جدول توزيع السهام والأنصبة الشرعية:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF59E0B).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'أصل المسألة: $_issueBaseNumber',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFFFBBF24)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
..._sharesResults.map((share) => 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: (share['amount'] as double) > 0
|
||||
? const Color(0xFFF59E0B).withOpacity(0.3)
|
||||
: const Color(0xFFEF4444).withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: (share['amount'] as double) > 0
|
||||
? const Color(0xFF78350F)
|
||||
: const Color(0xFF7F1D1D),
|
||||
child: Text(
|
||||
(share['heir'] as String).substring(0, 1),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: (share['amount'] as double) > 0
|
||||
? const Color(0xFFFBBF24)
|
||||
: const Color(0xFFF87171),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
share['heir'],
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
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(
|
||||
share['share'],
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF38BDF8),
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
share['basis'],
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: Color(0xFF94A3B8),
|
||||
height: 1.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${(share['amount'] as double).toStringAsFixed(0)} د.أ',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: (share['amount'] as double) > 0
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFFEF4444),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(share['amount'] as double) > 0
|
||||
? 'مستحق'
|
||||
: 'محجوب حجب حرمان',
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: (share['amount'] as double) > 0
|
||||
? const Color(0xFF34D399)
|
||||
: const Color(0xFFF87171),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
_shariaSummary,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Color(0xFFCBD5E1), height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _genderButton({
|
||||
required String title,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFFF59E0B) : const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w600,
|
||||
color: isSelected ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _counterControl({
|
||||
required String label,
|
||||
required int count,
|
||||
required ValueChanged<int> onChanged,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161F30),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white)),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.minus_circle,
|
||||
size: 20, color: Color(0xFF94A3B8)),
|
||||
onPressed: count > 0 ? () => onChanged(count - 1) : null,
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('$count',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white)),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(CupertinoIcons.plus_circle,
|
||||
size: 20, color: Color(0xFFF59E0B)),
|
||||
onPressed: () => onChanged(count + 1),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TAB 2: Tajweed & Phonetics Lab
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildTajweedLabTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'أحكام النون الساكنة والتنوين (الوحدة الأولى - منهاج التوجيهي):',
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Rule Selector Chips
|
||||
Row(
|
||||
children: List.generate(_tajweedRules.length, (idx) {
|
||||
final rule = _tajweedRules[idx];
|
||||
final isCur = _selectedRuleIndex == idx;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _selectedRuleIndex = idx),
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: idx < _tajweedRules.length - 1 ? 6 : 0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: isCur
|
||||
? rule['accentColor']
|
||||
: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isCur
|
||||
? (rule['accentColor'] as Color)
|
||||
: const Color(0xFF1E293B),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
rule['title'].toString().split(' ').first,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: isCur ? FontWeight.w800 : FontWeight.w600,
|
||||
color: isCur ? Colors.black : const Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Rule Details Card
|
||||
Builder(builder: (ctx) {
|
||||
final rule = _tajweedRules[_selectedRuleIndex];
|
||||
final examples = (rule['examples'] as List);
|
||||
final accentColor = rule['accentColor'] as Color;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F172A),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: accentColor.withOpacity(0.4)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
rule['title'],
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: accentColor,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const Text(
|
||||
'تجويد وزاري',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
rule['definition'],
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: Color(0xFFCBD5E1), height: 1.5),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Letters Box
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.textformat,
|
||||
color: Color(0xFFFBBF24), size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'الحروف: ${rule['letters']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const Text(
|
||||
'أمثلة قرآنية مقررة وطريقة النطق:',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
...examples.map((ex) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161F30),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
ex['text'],
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFFFEF08A),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
ex['highlight'],
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: accentColor),
|
||||
),
|
||||
Text(
|
||||
'سورة ${ex['surah']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10.5, color: Color(0xFF64748B)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ import '../player/socratic_video_player_screen.dart';
|
||||
import '../exams/adaptive_exam_screen.dart';
|
||||
import 'curriculum_document_viewer_screen.dart';
|
||||
import 'physics_interactive_lab_view.dart';
|
||||
import 'arabic_interactive_lab_view.dart';
|
||||
import 'islamic_interactive_lab_view.dart';
|
||||
import 'history_interactive_timeline_view.dart';
|
||||
import '../../../data/models/exam_model.dart';
|
||||
import '../../../data/repositories/app_repositories.dart';
|
||||
|
||||
@@ -39,11 +42,74 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
|
||||
late TabController _tabController;
|
||||
late Future<List<ExamModel>> _examsFuture;
|
||||
|
||||
bool get _hasLab =>
|
||||
bool get _isScienceSubject =>
|
||||
widget.subject.id.contains('physic') ||
|
||||
widget.subject.title.contains('فيزياء') ||
|
||||
widget.subject.id.contains('chem') ||
|
||||
widget.subject.id.contains('science');
|
||||
widget.subject.id.contains('كيمياء') ||
|
||||
widget.subject.id.contains('science') ||
|
||||
widget.subject.title.contains('علوم');
|
||||
|
||||
bool get _isArabicSubject =>
|
||||
widget.subject.id.contains('arab') ||
|
||||
widget.subject.title.contains('عرب') ||
|
||||
widget.subject.title.contains('ضاد') ||
|
||||
widget.subject.title.contains('لغة عربية');
|
||||
|
||||
bool get _isIslamicSubject =>
|
||||
widget.subject.id.contains('islam') ||
|
||||
widget.subject.title.contains('إسلام') ||
|
||||
widget.subject.title.contains('دين') ||
|
||||
widget.subject.title.contains('شريع');
|
||||
|
||||
bool get _isHistorySubject =>
|
||||
widget.subject.id.contains('hist') ||
|
||||
widget.subject.title.contains('تاريخ') ||
|
||||
widget.subject.title.contains('أردن') ||
|
||||
widget.subject.title.contains('جغرافيا') ||
|
||||
widget.subject.title.contains('دراسات');
|
||||
|
||||
bool get _hasLab =>
|
||||
_isScienceSubject ||
|
||||
_isArabicSubject ||
|
||||
_isIslamicSubject ||
|
||||
_isHistorySubject;
|
||||
|
||||
Tab get _dynamicLabTab {
|
||||
if (_isArabicSubject) {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.arrow_branch, size: 18),
|
||||
text: 'مختبر الإعراب والعروض 📜',
|
||||
);
|
||||
} else if (_isIslamicSubject) {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.money_dollar_circle_fill, size: 18),
|
||||
text: 'حاسبة المواريث والتجويد ⚖️',
|
||||
);
|
||||
} else if (_isHistorySubject) {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.time_solid, size: 18),
|
||||
text: 'الخط الزمني لتاريخ الأردن 🏛️',
|
||||
);
|
||||
} else {
|
||||
return const Tab(
|
||||
icon: Icon(CupertinoIcons.lab_flask_solid, size: 18),
|
||||
text: 'المختبر التفاعلي 🧪',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget get _dynamicLabWidget {
|
||||
if (_isArabicSubject) {
|
||||
return const ArabicInteractiveLabView();
|
||||
} else if (_isIslamicSubject) {
|
||||
return const IslamicInteractiveLabView();
|
||||
} else if (_isHistorySubject) {
|
||||
return const HistoryInteractiveTimelineView();
|
||||
} else {
|
||||
return const PhysicsInteractiveLabView();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -83,7 +149,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||
tabs: [
|
||||
const Tab(icon: Icon(CupertinoIcons.play_rectangle_fill, size: 18), text: 'الفيديوهات'),
|
||||
if (_hasLab) const Tab(icon: Icon(CupertinoIcons.lab_flask_solid, size: 18), text: 'المختبر التفاعلي 🧪'),
|
||||
if (_hasLab) _dynamicLabTab,
|
||||
const Tab(icon: Icon(CupertinoIcons.sparkles, size: 18), text: 'بنك الأسئلة'),
|
||||
const Tab(icon: Icon(CupertinoIcons.doc_text_fill, size: 18), text: 'أوراق العمل'),
|
||||
const Tab(icon: Icon(CupertinoIcons.book_fill, size: 18), text: 'الكتب المقررة'),
|
||||
@@ -94,7 +160,7 @@ class _SubjectHubScreenState extends State<SubjectHubScreen> with SingleTickerPr
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildLessonsTab(context),
|
||||
if (_hasLab) const PhysicsInteractiveLabView(),
|
||||
if (_hasLab) _dynamicLabWidget,
|
||||
_buildExamsTab(context),
|
||||
_buildWorksheetsTab(context),
|
||||
_buildTextbooksTab(context),
|
||||
|
||||
Reference in New Issue
Block a user