Update Saqel Platform: 2026-08-31 01:52:23

This commit is contained in:
Hamza-Ayed
2026-08-31 01:52:24 +03:00
parent 4e29915383
commit ccf5f13a09
419 changed files with 19777 additions and 41 deletions
@@ -0,0 +1,27 @@
/// Global Application Configuration
class AppConfig {
AppConfig._();
// Official Live Saqel API Gateway
static const String productionBaseUrl = 'https://saqel.intaleqapp.com';
static const String localBaseUrl = 'http://127.0.0.1:8000';
// Active Base URL (Default to live production API Gateway)
static String baseUrl = productionBaseUrl;
static const String appNameAr = 'صَقِل';
static const String appNameEn = 'Saqel';
static const String appTaglineAr = 'المنصة التعليمية الشاملة للثانوية العامة';
static const String appTaglineEn = 'Comprehensive EdTech Platform for High School';
// API Endpoints
static const String otpRequestEndpoint = '/api/auth/otp/request';
static const String otpVerifyEndpoint = '/api/auth/otp/verify';
static const String loginNationalIdEndpoint = '/api/auth/student/login-national-id';
static const String meEndpoint = '/api/auth/me';
static const String logoutEndpoint = '/api/auth/logout';
static const String studentLessonsEndpoint = '/api/student/lessons';
static const String guardianDashboardEndpoint = '/api/guardian/dashboard';
static const String studentProfileStatusEndpoint = '/api/student/profile/status';
static const String studentProfileSetupEndpoint = '/api/student/profile/setup';
}
@@ -0,0 +1,47 @@
/// Multi-language Localization Dictionary for Saqel Platform
class AppStrings {
AppStrings._();
static bool isArabic = true;
// General
static String get appName => isArabic ? 'صَقِل' : 'Saqel';
static String get studentPortal => isArabic ? 'منصة الطالب' : 'Student Portal';
static String get guardianPortal => isArabic ? 'بوابة ولي الأمر' : 'Guardian Portal';
static String get studentRole => isArabic ? 'الطالب 🎓' : 'Student 🎓';
static String get guardianRole => isArabic ? 'ولي الأمر 👨‍👧‍👦' : 'Guardian 👨‍👧‍👦';
static String get logout => isArabic ? 'تسجيل الخروج' : 'Logout';
static String get retry => isArabic ? 'إعادة المحاولة' : 'Retry';
static String get loading => isArabic ? 'جارٍ التحميل...' : 'Loading...';
static String get emptyData => isArabic ? 'لا توجد بيانات حالياً' : 'No data available';
// Auth
static String get enterPhoneTitle => isArabic ? 'تسجيل الدخول عبر الواتساب' : 'Login via WhatsApp';
static String get enterPhoneSubtitle => isArabic
? 'أدخل رقم هاتفك لاستلام رمز التحقق الفوري (OTP) بدون كلمات مرور'
: 'Enter your phone number to receive instant OTP verification';
static String get phoneNumber => isArabic ? 'رقم الهاتف (مثال: 079XXXXXXX)' : 'Phone Number';
static String get requestOtp => isArabic ? 'إرسال رمز التحقق 📲' : 'Send OTP Code 📲';
static String get enterOtpTitle => isArabic ? 'تأكيد رمز التحقق' : 'Verify OTP Code';
static String get enterOtpSubtitle => isArabic
? 'تم إرسال رمز التحقق عبر الواتساب إلى رقمك'
: 'Verification code sent via WhatsApp to your number';
static String get verifyOtp => isArabic ? 'تأكيد ودخول المنصة' : 'Verify & Continue';
static String get enterNationalIdTitle => isArabic ? 'الرقم الوطني للطالب' : 'National ID';
static String get enterNationalIdSubtitle => isArabic
? 'يرجى إدخال الرقم الوطني الخاص بالطالب لفتح الملف الأكاديمي'
: 'Enter the student National ID to access the academic profile';
static String get nationalId => isArabic ? 'الرقم الوطني (10 أرقام)' : 'National ID (10 digits)';
static String get continueBtn => isArabic ? 'متابعة' : 'Continue';
// Student Dashboard
static String get tawjihiCurriculum => isArabic ? 'المنهاج الوزاري المعتمد 📚' : 'Ministry Curriculum 📚';
static String get readinessScore => isArabic ? 'مؤشر الجاهزية للوزاري' : 'Tawjihi Readiness Score';
static String get socraticCheckpoint => isArabic ? 'فحص سقراطي 🎯' : 'Socratic Checkpoint 🎯';
static String get noLessonsFound => isArabic ? 'لا توجد دروس مسجلة للمرحلة الحالية' : 'No lessons recorded for this grade yet';
// Guardian Dashboard
static String get linkedChildren => isArabic ? 'الأبناء المسجلون برقم الهاتف 📱' : 'Linked Children 📱';
static String get noChildrenLinked => isArabic ? 'لا يوجد أبناء مرتبطون بهذا الرقم حالياً' : 'No children linked to this number';
static String get whatsappReports => isArabic ? 'تقارير نبيه التلقائية' : 'Nabeh Automated Reports';
}
@@ -0,0 +1,141 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../config/app_config.dart';
import '../services/storage_service.dart';
import '../utils/app_logger.dart';
/// Unified Secure REST API Client with Automatic JWT, Device Fingerprint & Debug Telemetry
class ApiClient {
final http.Client _client;
final StorageService _storage;
ApiClient({http.Client? client, StorageService? storage})
: _client = client ?? http.Client(),
_storage = storage ?? StorageService();
Future<Map<String, String>> _buildHeaders({bool requiresAuth = true}) async {
final headers = <String, String>{
'Content-Type': 'application/json',
'Accept': 'application/json',
};
final deviceId = await _storage.getDeviceId();
if (deviceId != null) {
headers['X-Device-Fingerprint'] = deviceId;
}
if (requiresAuth) {
final token = await _storage.getToken();
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
}
return headers;
}
Uri _buildUri(String path, [Map<String, dynamic>? queryParams]) {
final base = AppConfig.baseUrl.replaceAll(RegExp(r'/+$'), '');
final cleanPath = path.startsWith('/') ? path : '/$path';
final uriString = '$base$cleanPath';
final uri = Uri.parse(uriString);
if (queryParams != null && queryParams.isNotEmpty) {
return uri.replace(queryParameters: queryParams.map((k, v) => MapEntry(k, v.toString())));
}
return uri;
}
Future<dynamic> get(String path, {Map<String, dynamic>? queryParams, bool requiresAuth = true}) async {
final uri = _buildUri(path, queryParams);
final headers = await _buildHeaders(requiresAuth: requiresAuth);
AppLogger.request(method: 'GET', uri: uri, headers: headers);
final stopwatch = Stopwatch()..start();
try {
final response = await _client.get(uri, headers: headers);
stopwatch.stop();
return _handleResponse(response, method: 'GET', uri: uri, duration: stopwatch.elapsed);
} catch (e, stack) {
stopwatch.stop();
AppLogger.error('GET Request Failed ($uri)', error: e, stackTrace: stack);
throw ApiException('فشل الاتصال بالخادم: ${e.toString()}');
}
}
Future<dynamic> post(String path, {Map<String, dynamic>? body, bool requiresAuth = true}) async {
final uri = _buildUri(path);
final headers = await _buildHeaders(requiresAuth: requiresAuth);
AppLogger.request(method: 'POST', uri: uri, headers: headers, body: body);
final stopwatch = Stopwatch()..start();
try {
final response = await _client.post(
uri,
headers: headers,
body: body != null ? jsonEncode(body) : null,
);
stopwatch.stop();
return _handleResponse(response, method: 'POST', uri: uri, duration: stopwatch.elapsed);
} catch (e, stack) {
stopwatch.stop();
AppLogger.error('POST Request Failed ($uri)', error: e, stackTrace: stack);
throw ApiException('فشل إرسال الطلب للخادم: ${e.toString()}');
}
}
dynamic _handleResponse(
http.Response response, {
required String method,
required Uri uri,
required Duration duration,
}) {
dynamic data;
try {
data = jsonDecode(utf8.decode(response.bodyBytes));
} catch (_) {
data = response.body;
}
AppLogger.response(
method: method,
uri: uri,
statusCode: response.statusCode,
responseBody: data,
duration: duration,
);
if (response.statusCode >= 200 && response.statusCode < 300) {
return data;
}
final message = (data is Map && data['message'] != null)
? data['message']
: (data is Map && data['error'] != null)
? data['error']
: 'خطأ من الخادم (${response.statusCode})';
if (response.statusCode == 401) {
AppLogger.log('Unauthorized (401) received, clearing session.', tag: 'AUTH');
_storage.clearSession();
throw UnauthorizedException(message.toString());
}
throw ApiException(message.toString(), statusCode: response.statusCode);
}
}
class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, {this.statusCode});
@override
String toString() => message;
}
class UnauthorizedException extends ApiException {
UnauthorizedException(super.message) : super(statusCode: 401);
}
@@ -0,0 +1,109 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../data/models/user_model.dart';
import '../utils/app_logger.dart';
/// Resilient Storage Service for JWT Tokens, Device ID, and User Data
/// Supports macOS Keychain + In-Memory Fallback to prevent -34018 crashes
class StorageService {
final FlutterSecureStorage _storage;
static final Map<String, String> _memoryFallback = {};
StorageService({FlutterSecureStorage? storage})
: _storage = storage ??
const FlutterSecureStorage(
mOptions: MacOsOptions(
accessibility: KeychainAccessibility.first_unlock,
),
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
);
static const String _keyToken = 'saqel_jwt_token';
static const String _keyUser = 'saqel_user_data';
static const String _keyRole = 'saqel_active_role';
static const String _keyIdentityToken = 'saqel_identity_token';
static const String _keyDeviceId = 'saqel_device_id';
Future<void> _writeSafe(String key, String value) async {
_memoryFallback[key] = value;
try {
await _storage.write(key: key, value: value);
} on PlatformException catch (e) {
AppLogger.log('Keychain write notice ($key): ${e.message} (Using secure memory fallback)', tag: 'STORAGE');
} catch (_) {}
}
Future<String?> _readSafe(String key) async {
try {
final val = await _storage.read(key: key);
if (val != null) return val;
} on PlatformException catch (e) {
AppLogger.log('Keychain read notice ($key): ${e.message}', tag: 'STORAGE');
} catch (_) {}
return _memoryFallback[key];
}
Future<void> _deleteSafe(String key) async {
_memoryFallback.remove(key);
try {
await _storage.delete(key: key);
} catch (_) {}
}
Future<void> saveToken(String token) async {
await _writeSafe(_keyToken, token);
}
Future<String?> getToken() async {
return await _readSafe(_keyToken);
}
Future<void> saveIdentityToken(String token) async {
await _writeSafe(_keyIdentityToken, token);
}
Future<String?> getIdentityToken() async {
return await _readSafe(_keyIdentityToken);
}
Future<void> saveUser(UserModel user) async {
await _writeSafe(_keyUser, jsonEncode(user.toJson()));
}
Future<UserModel?> getUser() async {
final raw = await _readSafe(_keyUser);
if (raw == null || raw.isEmpty) return null;
try {
final json = jsonDecode(raw);
return UserModel.fromJson(json);
} catch (_) {
return null;
}
}
Future<void> saveActiveRole(String role) async {
await _writeSafe(_keyRole, role);
}
Future<String?> getActiveRole() async {
return await _readSafe(_keyRole);
}
Future<void> saveDeviceId(String deviceId) async {
await _writeSafe(_keyDeviceId, deviceId);
}
Future<String?> getDeviceId() async {
return await _readSafe(_keyDeviceId);
}
Future<void> clearSession() async {
await _deleteSafe(_keyToken);
await _deleteSafe(_keyUser);
await _deleteSafe(_keyRole);
await _deleteSafe(_keyIdentityToken);
}
}
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
/// Apple Luxury Palette & Brand Tokens for Saqel Platform
class AppColors {
AppColors._();
// Dark OLED Palette
static const Color darkBackground = Color(0xFF06090F);
static const Color darkSurface = Color(0xFF0D111A);
static const Color darkCard = Color(0xFF131A26);
static const Color darkCardBorder = Color(0x1FFFFFFF);
// Light Palette
static const Color lightBackground = Color(0xFFF8FAFC);
static const Color lightSurface = Color(0xFFFFFFFF);
static const Color lightCard = Color(0xFFF1F5F9);
static const Color lightCardBorder = Color(0x0F000000);
// Accent Colors
static const Color appleBlue = Color(0xFF0071E3);
static const Color saqelCyan = Color(0xFF00F5D4);
static const Color guardianAmber = Color(0xFFF59E0B);
static const Color teacherEmerald = Color(0xFF10B981);
static const Color adminViolet = Color(0xFF8B5CF6);
static const Color whatsappGreen = Color(0xFF25D366);
static const Color errorRed = Color(0xFFEF4444);
// Text Colors
static const Color textPrimaryDark = Color(0xFFF8FAFC);
static const Color textSecondaryDark = Color(0xFF94A3B8);
static const Color textMutedDark = Color(0xFF64748B);
static const Color textPrimaryLight = Color(0xFF0F172A);
static const Color textSecondaryLight = Color(0xFF475569);
static const Color textMutedLight = Color(0xFF94A3B8);
}
@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
/// Apple Official San Francisco & SF Arabic Typography Standards
class AppTypography {
AppTypography._();
static const String primaryFont = '-apple-system';
static const List<String> fallbackFonts = [
'SF Pro Display',
'SF Pro Text',
'SF Arabic',
'Geeza Pro',
'Cairo',
'system-ui',
];
static const TextStyle displayLarge = TextStyle(
fontFamily: primaryFont,
fontFamilyFallback: fallbackFonts,
fontSize: 28,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
);
static const TextStyle headlineMedium = TextStyle(
fontFamily: primaryFont,
fontFamilyFallback: fallbackFonts,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
);
static const TextStyle titleMedium = TextStyle(
fontFamily: primaryFont,
fontFamilyFallback: fallbackFonts,
fontSize: 15,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
);
static const TextStyle bodyMedium = TextStyle(
fontFamily: primaryFont,
fontFamilyFallback: fallbackFonts,
fontSize: 13.5,
fontWeight: FontWeight.w400,
height: 1.5,
);
static const TextStyle labelSmall = TextStyle(
fontFamily: primaryFont,
fontFamilyFallback: fallbackFonts,
fontSize: 11,
fontWeight: FontWeight.w600,
);
}
/// Official Apple Dark & Light Theme Factory
class AppTheme {
AppTheme._();
static ThemeData get darkTheme {
return ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: AppColors.darkBackground,
fontFamily: AppTypography.primaryFont,
fontFamilyFallback: AppTypography.fallbackFonts,
colorScheme: const ColorScheme.dark(
primary: AppColors.appleBlue,
secondary: AppColors.saqelCyan,
surface: AppColors.darkSurface,
error: AppColors.errorRed,
),
cardTheme: CardThemeData(
color: AppColors.darkCard,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: const BorderSide(color: AppColors.darkCardBorder),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.appleBlue,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 15),
textStyle: AppTypography.titleMedium.copyWith(color: Colors.white),
),
),
);
}
static ThemeData get lightTheme {
return ThemeData(
brightness: Brightness.light,
scaffoldBackgroundColor: AppColors.lightBackground,
fontFamily: AppTypography.primaryFont,
fontFamilyFallback: AppTypography.fallbackFonts,
colorScheme: const ColorScheme.light(
primary: AppColors.appleBlue,
secondary: AppColors.saqelCyan,
surface: AppColors.lightSurface,
error: AppColors.errorRed,
),
cardTheme: CardThemeData(
color: AppColors.lightCard,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: const BorderSide(color: AppColors.lightCardBorder),
),
),
);
}
}
@@ -0,0 +1,83 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
/// Enterprise Logging Utility for Saqel Platform
/// Active in Debug mode (kDebugMode), completely silent in Production
class AppLogger {
AppLogger._();
static void log(String message, {String tag = 'SAQEL'}) {
if (kDebugMode) {
debugPrint('[$tag] $message');
}
}
static void request({
required String method,
required Uri uri,
Map<String, String>? headers,
dynamic body,
}) {
if (!kDebugMode) return;
final buffer = StringBuffer();
buffer.writeln('\n🌐 ─── [HTTP REQUEST] ──────────────────────────────────────────');
buffer.writeln('➡️ Method: $method');
buffer.writeln('🔗 URL: $uri');
if (headers != null && headers.isNotEmpty) {
final safeHeaders = Map<String, String>.from(headers);
if (safeHeaders.containsKey('Authorization')) {
final auth = safeHeaders['Authorization']!;
safeHeaders['Authorization'] = auth.length > 20 ? '${auth.substring(0, 15)}...' : 'Bearer [REDACTED]';
}
buffer.writeln('📋 Headers: ${jsonEncode(safeHeaders)}');
}
if (body != null) {
buffer.writeln('📦 Body: ${body is String ? body : jsonEncode(body)}');
}
buffer.writeln('───────────────────────────────────────────────────────────────');
debugPrint(buffer.toString());
}
static void response({
required String method,
required Uri uri,
required int statusCode,
dynamic responseBody,
Duration? duration,
}) {
if (!kDebugMode) return;
final isSuccess = statusCode >= 200 && statusCode < 300;
final emoji = isSuccess ? '✅' : '⚠️';
final buffer = StringBuffer();
buffer.writeln('\n$emoji ─── [HTTP RESPONSE $statusCode] ────────────────────────────────');
buffer.writeln('⬅️ Method: $method');
buffer.writeln('🔗 URL: $uri');
if (duration != null) {
buffer.writeln('⏱️ Time: ${duration.inMilliseconds}ms');
}
if (responseBody != null) {
buffer.writeln('📦 Data: ${responseBody is String ? responseBody : jsonEncode(responseBody)}');
}
buffer.writeln('───────────────────────────────────────────────────────────────');
debugPrint(buffer.toString());
}
static void error(String message, {dynamic error, StackTrace? stackTrace, String tag = 'ERROR'}) {
if (!kDebugMode) return;
final buffer = StringBuffer();
buffer.writeln('\n🚨 ─── [ERROR: $tag] ──────────────────────────────────────────');
buffer.writeln('❌ Message: $message');
if (error != null) {
buffer.writeln('⚠️ Error: $error');
}
if (stackTrace != null) {
buffer.writeln('📍 Trace: \n$stackTrace');
}
buffer.writeln('───────────────────────────────────────────────────────────────');
debugPrint(buffer.toString());
}
}
@@ -0,0 +1,165 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_theme.dart';
/// Ultra-Luxury Apple-Grade SnackBar / Toast Notification System
class SaqelToast {
SaqelToast._();
/// Show Success Toast (Emerald / Green)
static void showSuccess(
BuildContext context,
String message, {
String? title,
Duration duration = const Duration(seconds: 3),
}) {
_show(
context: context,
message: message,
title: title,
icon: CupertinoIcons.checkmark_circle_fill,
iconColor: AppColors.teacherEmerald,
borderColor: AppColors.teacherEmerald.withAlpha(90),
backgroundColor: const Color(0xE60D1A14),
duration: duration,
);
}
/// Show Error Toast (Crimson / Red)
static void showError(
BuildContext context,
String message, {
String? title,
Duration duration = const Duration(seconds: 4),
}) {
_show(
context: context,
message: message,
title: title,
icon: CupertinoIcons.exclamationmark_circle_fill,
iconColor: AppColors.errorRed,
borderColor: AppColors.errorRed.withAlpha(90),
backgroundColor: const Color(0xE61A0D0D),
duration: duration,
);
}
/// Show Info Toast (Apple Blue / Cyan)
static void showInfo(
BuildContext context,
String message, {
String? title,
Duration duration = const Duration(seconds: 3),
}) {
_show(
context: context,
message: message,
title: title,
icon: CupertinoIcons.info_circle_fill,
iconColor: AppColors.appleBlue,
borderColor: AppColors.appleBlue.withAlpha(90),
backgroundColor: const Color(0xE60D1424),
duration: duration,
);
}
/// Show WhatsApp / Nabeh Toast (WhatsApp Green)
static void showWhatsApp(
BuildContext context,
String message, {
String? title,
Duration duration = const Duration(seconds: 3),
}) {
_show(
context: context,
message: message,
title: title,
icon: CupertinoIcons.chat_bubble_fill,
iconColor: AppColors.whatsappGreen,
borderColor: AppColors.whatsappGreen.withAlpha(100),
backgroundColor: const Color(0xE6091A12),
duration: duration,
);
}
static void _show({
required BuildContext context,
required String message,
String? title,
required IconData icon,
required Color iconColor,
required Color borderColor,
required Color backgroundColor,
required Duration duration,
}) {
final messenger = ScaffoldMessenger.of(context);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
duration: duration,
elevation: 0,
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.only(bottom: 24, left: 16, right: 16),
padding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
content: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: borderColor, width: 1.2),
boxShadow: const [
BoxShadow(
color: Color(0x99000000),
blurRadius: 25,
offset: Offset(0, 10),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: iconColor.withAlpha(35),
shape: BoxShape.circle,
),
child: Icon(icon, color: iconColor, size: 20),
),
const SizedBox(width: 14),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null && title.isNotEmpty) ...[
Text(
title,
style: AppTypography.titleMedium.copyWith(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
const SizedBox(height: 2),
],
Text(
message,
style: AppTypography.bodyMedium.copyWith(
fontSize: 12.5,
color: const Color(0xFFE2E8F0),
height: 1.4,
),
),
],
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,104 @@
/// Real Lesson Model from /api/student/lessons
class LessonModel {
final int id;
final int courseId;
final String title;
final int sequenceOrder;
final String storageType;
final String? videoUuid;
final String? hlsUrl;
final String? thumbnailUrl;
final String? aiVideoUrl;
final int durationSeconds;
final bool isFreePreview;
final String courseTitle;
final bool isAiVersion;
final int? checkpointCount;
LessonModel({
required this.id,
required this.courseId,
required this.title,
required this.sequenceOrder,
required this.storageType,
this.videoUuid,
this.hlsUrl,
this.thumbnailUrl,
this.aiVideoUrl,
required this.durationSeconds,
required this.isFreePreview,
required this.courseTitle,
required this.isAiVersion,
this.checkpointCount,
});
factory LessonModel.fromJson(Map<String, dynamic> json) {
return LessonModel(
id: json['id'] is int ? json['id'] : int.tryParse(json['id']?.toString() ?? '0') ?? 0,
courseId: json['course_id'] is int ? json['course_id'] : int.tryParse(json['course_id']?.toString() ?? '0') ?? 0,
title: json['title']?.toString() ?? 'درس تفاعلي',
sequenceOrder: json['sequence_order'] is int ? json['sequence_order'] : int.tryParse(json['sequence_order']?.toString() ?? '1') ?? 1,
storageType: json['storage_type']?.toString() ?? 'local',
videoUuid: json['video_uuid']?.toString(),
hlsUrl: json['hls_url']?.toString(),
thumbnailUrl: json['thumbnail_url']?.toString(),
aiVideoUrl: json['ai_video_url']?.toString(),
durationSeconds: json['duration_seconds'] is int ? json['duration_seconds'] : int.tryParse(json['duration_seconds']?.toString() ?? '0') ?? 0,
isFreePreview: json['is_free_preview'] == 1 || json['is_free_preview'] == true,
courseTitle: json['course_title']?.toString() ?? 'المنهاج المعتمد',
isAiVersion: json['is_ai_version'] == 1 || json['is_ai_version'] == true,
checkpointCount: json['checkpoint_count'] is int ? json['checkpoint_count'] : int.tryParse(json['checkpoint_count']?.toString() ?? '0'),
);
}
String get formattedDuration {
if (durationSeconds <= 0) return 'درس تفاعلي';
final minutes = durationSeconds ~/ 60;
final seconds = durationSeconds % 60;
return '$minutes دقيقة ${seconds > 0 ? '$seconds ثانية' : ''}';
}
}
/// Real Child Profile & Academic Progress Model for Guardian
class GuardianChildModel {
final int id;
final String uuid;
final String name;
final String? nationalId;
final String? gradeLevel;
final String? stream;
final String? schoolName;
final double readinessScore;
final int examsPassed;
final int examsTotal;
GuardianChildModel({
required this.id,
required this.uuid,
required this.name,
this.nationalId,
this.gradeLevel,
this.stream,
this.schoolName,
this.readinessScore = 0.0,
this.examsPassed = 0,
this.examsTotal = 0,
});
factory GuardianChildModel.fromJson(Map<String, dynamic> json) {
return GuardianChildModel(
id: json['id'] is int ? json['id'] : int.tryParse(json['id']?.toString() ?? '0') ?? 0,
uuid: json['uuid']?.toString() ?? '',
name: json['full_name']?.toString() ?? json['name']?.toString() ?? 'الطالب',
nationalId: json['national_id']?.toString(),
gradeLevel: json['grade_level']?.toString(),
stream: json['stream']?.toString(),
schoolName: json['school_name']?.toString() ?? 'مدرسة معتمدة',
readinessScore: json['tawjihi_readiness_score'] != null
? double.tryParse(json['tawjihi_readiness_score'].toString()) ?? 0.0
: (json['readiness_score'] != null ? double.tryParse(json['readiness_score'].toString()) ?? 0.0 : 0.0),
examsPassed: json['exams_passed_count'] is int ? json['exams_passed_count'] : int.tryParse(json['exams_passed_count']?.toString() ?? '0') ?? 0,
examsTotal: json['exams_total_count'] is int ? json['exams_total_count'] : int.tryParse(json['exams_total_count']?.toString() ?? '0') ?? 0,
);
}
}
@@ -0,0 +1,67 @@
/// Real User Profile Model (Student / Guardian / Teacher)
class UserModel {
final int id;
final String uuid;
final String name;
final String phone;
final String role;
final String? nationalId;
final String? gradeLevel;
final String? stream;
final double? readinessScore;
final int? schoolId;
final bool isCompleted;
UserModel({
required this.id,
required this.uuid,
required this.name,
required this.phone,
required this.role,
this.nationalId,
this.gradeLevel,
this.stream,
this.readinessScore,
this.schoolId,
this.isCompleted = true,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] is int ? json['id'] : int.tryParse(json['id']?.toString() ?? '0') ?? 0,
uuid: json['uuid']?.toString() ?? '',
name: json['name']?.toString() ?? json['full_name']?.toString() ?? 'مستخدم صَقِل',
phone: json['phone']?.toString() ?? '',
role: json['role']?.toString() ?? 'student',
nationalId: json['national_id']?.toString(),
gradeLevel: json['grade_level']?.toString(),
stream: json['stream']?.toString(),
readinessScore: json['readiness_score'] != null
? double.tryParse(json['readiness_score'].toString())
: null,
schoolId: json['school_id'] != null
? int.tryParse(json['school_id'].toString())
: null,
isCompleted: json['is_completed'] ?? true,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'uuid': uuid,
'name': name,
'phone': phone,
'role': role,
'national_id': nationalId,
'grade_level': gradeLevel,
'stream': stream,
'readiness_score': readinessScore,
'school_id': schoolId,
'is_completed': isCompleted,
};
}
bool get isStudent => role == 'student';
bool get isGuardian => role == 'guardian';
}
@@ -0,0 +1,178 @@
import '../../core/config/app_config.dart';
import '../../core/network/api_client.dart';
import '../../core/services/storage_service.dart';
import '../models/user_model.dart';
import '../models/lesson_model.dart';
/// Authentication Repository (OTP, National ID, me, logout)
class AuthRepository {
final ApiClient _api;
final StorageService _storage;
AuthRepository({ApiClient? api, StorageService? storage})
: _api = api ?? ApiClient(),
_storage = storage ?? StorageService();
Future<Map<String, dynamic>> requestOtp(String phoneNumber, {String role = 'student'}) async {
final res = await _api.post(
AppConfig.otpRequestEndpoint,
body: {
'phone_number': phoneNumber,
'role': role,
},
requiresAuth: false,
);
return res is Map<String, dynamic> ? res : {};
}
Future<Map<String, dynamic>> verifyOtp(String phoneNumber, String otp, {String role = 'student'}) async {
final res = await _api.post(
AppConfig.otpVerifyEndpoint,
body: {
'phone_number': phoneNumber,
'otp': otp,
'role': role,
},
requiresAuth: false,
);
if (res is Map<String, dynamic>) {
final data = res['data'];
if (data != null && data is Map) {
if (data['token'] != null) {
final token = data['token'].toString();
await _storage.saveToken(token);
await _storage.saveActiveRole(role);
if (data['user'] != null) {
final user = UserModel.fromJson(Map<String, dynamic>.from(data['user']));
await _storage.saveUser(user);
}
} else if (data['identity_token'] != null) {
await _storage.saveIdentityToken(data['identity_token'].toString());
}
}
return res;
}
return {};
}
Future<Map<String, dynamic>> verifyNationalId(String identityToken, String nationalId) async {
final res = await _api.post(
AppConfig.loginNationalIdEndpoint,
body: {
'identity_token': identityToken,
'national_id': nationalId,
},
requiresAuth: false,
);
if (res is Map<String, dynamic> && res['data'] != null) {
final data = res['data'];
if (data['token'] != null) {
final token = data['token'].toString();
await _storage.saveToken(token);
await _storage.saveActiveRole('student');
if (data['user'] != null) {
final user = UserModel.fromJson(Map<String, dynamic>.from(data['user']));
await _storage.saveUser(user);
}
}
return res;
}
throw ApiException('فشل التحقق من الرقم الوطني');
}
Future<UserModel> setupStudentProfile({
required String identityToken,
required String fullName,
required String nationalId,
String gradeLevel = 'tawjihi_2008',
String stream = 'scientific',
}) async {
final res = await _api.post(
AppConfig.studentProfileSetupEndpoint,
body: {
'identity_token': identityToken,
'full_name': fullName,
'national_id': nationalId,
'grade_level': gradeLevel,
'stream': stream,
},
requiresAuth: false,
);
if (res is Map<String, dynamic> && res['data'] != null) {
final data = res['data'];
if (data['token'] != null) {
final token = data['token'].toString();
await _storage.saveToken(token);
await _storage.saveActiveRole('student');
}
if (data['user'] != null) {
final user = UserModel.fromJson(Map<String, dynamic>.from(data['user']));
await _storage.saveUser(user);
return user;
}
}
throw ApiException('فشل استكمال فتح الملف الأكاديمي');
}
Future<UserModel?> getMe() async {
try {
final res = await _api.get(AppConfig.meEndpoint);
if (res is Map && res['data'] != null) {
final user = UserModel.fromJson(Map<String, dynamic>.from(res['data']));
await _storage.saveUser(user);
return user;
}
} catch (_) {
return null;
}
return null;
}
Future<void> logout() async {
try {
await _api.post(AppConfig.logoutEndpoint);
} catch (_) {}
await _storage.clearSession();
}
}
/// Student Repository (Lessons, Curriculum, Progress)
class StudentRepository {
final ApiClient _api;
StudentRepository({ApiClient? api}) : _api = api ?? ApiClient();
Future<List<LessonModel>> getLessons({String? gradeLevel, String? stream}) async {
final queryParams = <String, dynamic>{};
if (gradeLevel != null) queryParams['grade_level'] = gradeLevel;
if (stream != null) queryParams['stream'] = stream;
final res = await _api.get(AppConfig.studentLessonsEndpoint, queryParams: queryParams);
if (res is Map && res['data'] is List) {
return (res['data'] as List)
.map((item) => LessonModel.fromJson(Map<String, dynamic>.from(item)))
.toList();
}
return [];
}
}
/// Guardian Repository (Dashboard, Linked Children, Real Progress)
class GuardianRepository {
final ApiClient _api;
GuardianRepository({ApiClient? api}) : _api = api ?? ApiClient();
Future<List<GuardianChildModel>> getDashboardChildren() async {
final res = await _api.get(AppConfig.guardianDashboardEndpoint);
if (res is Map && res['data'] is List) {
return (res['data'] as List)
.map((item) => GuardianChildModel.fromJson(Map<String, dynamic>.from(item)))
.toList();
}
return [];
}
}
@@ -0,0 +1,187 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/services/storage_service.dart';
import '../../core/utils/app_logger.dart';
import '../../data/models/user_model.dart';
import '../../data/repositories/app_repositories.dart';
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthOtpSent extends AuthState {
final String phoneNumber;
final String role;
AuthOtpSent({required this.phoneNumber, required this.role});
}
class AuthNeedsNationalId extends AuthState {
final String identityToken;
final String phoneNumber;
AuthNeedsNationalId({required this.identityToken, required this.phoneNumber});
}
class AuthNeedsOnboarding extends AuthState {
final String identityToken;
final String nationalId;
AuthNeedsOnboarding({required this.identityToken, required this.nationalId});
}
class Authenticated extends AuthState {
final UserModel user;
final String activeRole;
Authenticated({required this.user, required this.activeRole});
}
class Unauthenticated extends AuthState {}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
class AuthCubit extends Cubit<AuthState> {
final AuthRepository _authRepo;
final StorageService _storage;
AuthCubit({AuthRepository? authRepo, StorageService? storage})
: _authRepo = authRepo ?? AuthRepository(),
_storage = storage ?? StorageService(),
super(AuthInitial());
Future<void> checkSession() async {
AppLogger.log('Checking local session...', tag: 'AUTH_CUBIT');
emit(AuthLoading());
try {
final token = await _storage.getToken();
if (token == null || token.isEmpty) {
AppLogger.log('No token found -> Unauthenticated', tag: 'AUTH_CUBIT');
emit(Unauthenticated());
return;
}
AppLogger.log('Token found -> Validating with /api/auth/me...', tag: 'AUTH_CUBIT');
final user = await _authRepo.getMe();
if (user != null) {
final role = await _storage.getActiveRole() ?? user.role;
AppLogger.log('User authenticated: ${user.name} (Role: $role)', tag: 'AUTH_CUBIT');
emit(Authenticated(user: user, activeRole: role));
} else {
AppLogger.log('Failed to fetch user -> Unauthenticated', tag: 'AUTH_CUBIT');
emit(Unauthenticated());
}
} catch (e) {
AppLogger.log('Session check error: $e', tag: 'AUTH_CUBIT');
emit(Unauthenticated());
}
}
Future<void> sendOtp(String phoneNumber, {String role = 'student'}) async {
AppLogger.log('Requesting OTP for $phoneNumber (Role: $role)...', tag: 'AUTH_CUBIT');
emit(AuthLoading());
try {
final cleanPhone = phoneNumber.trim();
final res = await _authRepo.requestOtp(cleanPhone, role: role);
if (res['status'] == 'success') {
AppLogger.log('OTP sent successfully to $cleanPhone', tag: 'AUTH_CUBIT');
emit(AuthOtpSent(phoneNumber: cleanPhone, role: role));
} else {
final msg = res['message']?.toString() ?? 'فشل إرسال رمز التحقق';
AppLogger.log('OTP request rejected: $msg', tag: 'AUTH_CUBIT');
emit(AuthError(msg));
}
} catch (e) {
AppLogger.error('OTP Send Exception', error: e, tag: 'AUTH_CUBIT');
emit(AuthError(e.toString()));
}
}
Future<void> verifyOtp(String phoneNumber, String otp, {String role = 'student'}) async {
AppLogger.log('Verifying OTP for $phoneNumber...', tag: 'AUTH_CUBIT');
emit(AuthLoading());
try {
final res = await _authRepo.verifyOtp(phoneNumber, otp.trim(), role: role);
final data = res['data'];
if (data != null && data['token'] != null) {
final user = UserModel.fromJson(Map<String, dynamic>.from(data['user']));
AppLogger.log('OTP verified -> Authenticated: ${user.name}', tag: 'AUTH_CUBIT');
emit(Authenticated(user: user, activeRole: role));
} else if (data != null && data['identity_token'] != null) {
AppLogger.log('OTP verified -> Requires National ID', tag: 'AUTH_CUBIT');
emit(AuthNeedsNationalId(
identityToken: data['identity_token'].toString(),
phoneNumber: phoneNumber,
));
} else {
final msg = res['message']?.toString() ?? 'رمز التحقق غير صحيح';
AppLogger.log('OTP Verification Failed: $msg', tag: 'AUTH_CUBIT');
emit(AuthError(msg));
}
} catch (e) {
AppLogger.error('OTP Verify Exception', error: e, tag: 'AUTH_CUBIT');
emit(AuthError(e.toString()));
}
}
Future<void> verifyNationalId(String identityToken, String nationalId) async {
AppLogger.log('Verifying National ID: $nationalId...', tag: 'AUTH_CUBIT');
emit(AuthLoading());
try {
final res = await _authRepo.verifyNationalId(identityToken, nationalId.trim());
final data = res['data'];
if (data != null && data['requires_onboarding'] == true) {
AppLogger.log('National ID not registered -> Requires Onboarding', tag: 'AUTH_CUBIT');
emit(AuthNeedsOnboarding(
identityToken: identityToken,
nationalId: nationalId.trim(),
));
} else if (data != null && data['user'] != null) {
final user = UserModel.fromJson(Map<String, dynamic>.from(data['user']));
AppLogger.log('National ID verified -> Student authenticated: ${user.name}', tag: 'AUTH_CUBIT');
emit(Authenticated(user: user, activeRole: 'student'));
} else {
emit(AuthError(res['message']?.toString() ?? 'فشل التحقق من الرقم الوطني'));
}
} catch (e) {
AppLogger.error('National ID Exception', error: e, tag: 'AUTH_CUBIT');
emit(AuthError(e.toString()));
}
}
Future<void> setupProfile({
required String identityToken,
required String fullName,
required String nationalId,
String gradeLevel = 'tawjihi_2008',
String stream = 'scientific',
}) async {
AppLogger.log('Setting up profile for $fullName ($nationalId)...', tag: 'AUTH_CUBIT');
emit(AuthLoading());
try {
final user = await _authRepo.setupStudentProfile(
identityToken: identityToken,
fullName: fullName.trim(),
nationalId: nationalId.trim(),
gradeLevel: gradeLevel,
stream: stream,
);
AppLogger.log('Profile setup complete -> Student authenticated: ${user.name}', tag: 'AUTH_CUBIT');
emit(Authenticated(user: user, activeRole: 'student'));
} catch (e) {
AppLogger.error('Setup Profile Exception', error: e, tag: 'AUTH_CUBIT');
emit(AuthError(e.toString()));
}
}
Future<void> switchRole(String newRole) async {
final currentState = state;
if (currentState is Authenticated) {
AppLogger.log('Switching role from ${currentState.activeRole} to $newRole', tag: 'AUTH_CUBIT');
await _storage.saveActiveRole(newRole);
emit(Authenticated(user: currentState.user, activeRole: newRole));
}
}
Future<void> logout() async {
AppLogger.log('Logging out user...', tag: 'AUTH_CUBIT');
emit(AuthLoading());
await _authRepo.logout();
emit(Unauthenticated());
}
}
@@ -0,0 +1,94 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/utils/app_logger.dart';
import '../../data/models/lesson_model.dart';
import '../../data/repositories/app_repositories.dart';
// --- STUDENT CUBIT ---
abstract class StudentState {}
class StudentInitial extends StudentState {}
class StudentLoading extends StudentState {}
class StudentLoaded extends StudentState {
final List<LessonModel> lessons;
final double readinessScore;
StudentLoaded({required this.lessons, required this.readinessScore});
}
class StudentEmpty extends StudentState {}
class StudentError extends StudentState {
final String message;
StudentError(this.message);
}
class StudentCubit extends Cubit<StudentState> {
final StudentRepository _studentRepo;
StudentCubit({StudentRepository? studentRepo})
: _studentRepo = studentRepo ?? StudentRepository(),
super(StudentInitial());
Future<void> fetchLessons({String? gradeLevel, String? stream, double initialReadiness = 0.0}) async {
AppLogger.log('Fetching lessons (Grade: $gradeLevel, Stream: $stream)...', tag: 'STUDENT_CUBIT');
emit(StudentLoading());
try {
final lessons = await _studentRepo.getLessons(gradeLevel: gradeLevel, stream: stream);
AppLogger.log('Fetched ${lessons.length} lessons from API', tag: 'STUDENT_CUBIT');
if (lessons.isEmpty) {
emit(StudentEmpty());
} else {
emit(StudentLoaded(lessons: lessons, readinessScore: initialReadiness));
}
} catch (e) {
AppLogger.error('Fetch lessons failed', error: e, tag: 'STUDENT_CUBIT');
emit(StudentError(e.toString()));
}
}
}
// --- GUARDIAN CUBIT ---
abstract class GuardianState {}
class GuardianInitial extends GuardianState {}
class GuardianLoading extends GuardianState {}
class GuardianLoaded extends GuardianState {
final List<GuardianChildModel> children;
final int selectedChildIndex;
GuardianLoaded({required this.children, this.selectedChildIndex = 0});
}
class GuardianEmpty extends GuardianState {}
class GuardianError extends GuardianState {
final String message;
GuardianError(this.message);
}
class GuardianCubit extends Cubit<GuardianState> {
final GuardianRepository _guardianRepo;
GuardianCubit({GuardianRepository? guardianRepo})
: _guardianRepo = guardianRepo ?? GuardianRepository(),
super(GuardianInitial());
Future<void> fetchDashboard() async {
AppLogger.log('Fetching guardian dashboard children from API...', tag: 'GUARDIAN_CUBIT');
emit(GuardianLoading());
try {
final children = await _guardianRepo.getDashboardChildren();
AppLogger.log('Fetched ${children.length} linked children from API', tag: 'GUARDIAN_CUBIT');
if (children.isEmpty) {
emit(GuardianEmpty());
} else {
emit(GuardianLoaded(children: children, selectedChildIndex: 0));
}
} catch (e) {
AppLogger.error('Fetch guardian dashboard failed', error: e, tag: 'GUARDIAN_CUBIT');
emit(GuardianError(e.toString()));
}
}
void selectChild(int index) {
final currentState = state;
if (currentState is GuardianLoaded && index >= 0 && index < currentState.children.length) {
AppLogger.log('Selected child: ${currentState.children[index].name}', tag: 'GUARDIAN_CUBIT');
emit(GuardianLoaded(children: currentState.children, selectedChildIndex: index));
}
}
}
+84
View File
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'core/config/app_config.dart';
import 'core/localization/app_strings.dart';
import 'core/theme/app_theme.dart';
import 'logic/cubits/auth_cubit.dart';
import 'logic/cubits/dashboard_cubits.dart';
import 'presentation/screens/auth/auth_screen.dart';
import 'presentation/screens/home/unified_home_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const SaqelAppRoot());
}
class SaqelAppRoot extends StatefulWidget {
const SaqelAppRoot({super.key});
@override
State<SaqelAppRoot> createState() => _SaqelAppRootState();
}
class _SaqelAppRootState extends State<SaqelAppRoot> {
final ThemeMode _themeMode = ThemeMode.dark;
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(
create: (context) => AuthCubit()..checkSession(),
),
BlocProvider<StudentCubit>(
create: (context) => StudentCubit(),
),
BlocProvider<GuardianCubit>(
create: (context) => GuardianCubit(),
),
],
child: MaterialApp(
title: AppConfig.appNameAr,
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: _themeMode,
builder: (context, child) {
// RTL / LTR Directionality Control
return Directionality(
textDirection: AppStrings.isArabic ? TextDirection.rtl : TextDirection.ltr,
child: child!,
);
},
home: const AuthGate(),
),
);
}
}
/// Global Auth Gate: Auto-Directs to Unified Dashboard if Logged In, or AuthScreen
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
return UnifiedHomeScreen(
user: state.user,
activeRole: state.activeRole,
);
} else if (state is AuthLoading) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
// Default to AuthScreen for unauthenticated or OTP states
return const AuthScreen();
},
);
}
}
@@ -0,0 +1,527 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/config/app_config.dart';
import '../../../core/localization/app_strings.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/utils/saqel_toast.dart';
import '../../../logic/cubits/auth_cubit.dart';
import '../../widgets/luxury_widgets.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _otpController = TextEditingController();
final TextEditingController _nationalIdController = TextEditingController();
final TextEditingController _fullNameController = TextEditingController();
String _selectedRole = 'student';
final String _selectedGrade = 'tawjihi_2008';
String _selectedStream = 'scientific';
@override
void dispose() {
_phoneController.dispose();
_otpController.dispose();
_nationalIdController.dispose();
_fullNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment(0.8, -0.6),
radius: 1.3,
colors: [
Color(0xFF0A2540),
AppColors.darkBackground,
],
),
),
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthError) {
SaqelToast.showError(context, state.message, title: 'تنبيه المصادقة');
} else if (state is AuthOtpSent) {
SaqelToast.showWhatsApp(
context,
'تم إرسال كود التحقق بنجاح إلى الرقم ${state.phoneNumber}',
title: 'رسالة واتساب من نبيه 📲',
);
} else if (state is AuthNeedsOnboarding) {
SaqelToast.showInfo(
context,
'يرجى استكمال الاسم والفرع لفتح ملفك الأكاديمي',
title: 'طالب جديد في صَقِل 🎓',
);
} else if (state is Authenticated) {
SaqelToast.showSuccess(
context,
'أهلاً بك يا ${state.user.name}',
title: 'تم تسجيل الدخول بنجاح ✨',
);
}
},
builder: (context, state) {
final isLoading = state is AuthLoading;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Server Connection Health Badge
Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: AppColors.saqelCyan,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
AppConfig.baseUrl.replaceAll('https://', '').replaceAll('http://', ''),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.textSecondaryDark,
),
),
],
),
),
),
const SizedBox(height: 24),
if (state is AuthNeedsOnboarding)
_buildOnboardingStep(context, state, isLoading)
else if (state is AuthNeedsNationalId)
_buildNationalIdStep(context, state, isLoading)
else if (state is AuthOtpSent)
_buildOtpStep(context, state, isLoading)
else
_buildPhoneStep(context, isLoading),
],
);
},
),
),
),
),
),
),
);
}
/// Step 1: Phone Input & Role Selector
Widget _buildPhoneStep(BuildContext context, bool isLoading) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 76,
height: 76,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.appleBlue, AppColors.saqelCyan],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(22),
boxShadow: const [
BoxShadow(
color: Color(0x4D00F5D4),
blurRadius: 25,
offset: Offset(0, 10),
),
],
),
child: const Center(
child: Text(
'ص',
style: TextStyle(
fontSize: 38,
fontWeight: FontWeight.w900,
color: Colors.black,
),
),
),
),
),
const SizedBox(height: 24),
Text(
AppStrings.enterPhoneTitle,
textAlign: TextAlign.center,
style: AppTypography.displayLarge.copyWith(color: Colors.white),
),
const SizedBox(height: 8),
Text(
AppStrings.enterPhoneSubtitle,
textAlign: TextAlign.center,
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark),
),
const SizedBox(height: 28),
// Role Segmented Pill
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: isLoading ? null : () => setState(() => _selectedRole = 'student'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: _selectedRole == 'student' ? AppColors.appleBlue : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
AppStrings.studentRole,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: _selectedRole == 'student' ? Colors.white : AppColors.textSecondaryDark,
),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: isLoading ? null : () => setState(() => _selectedRole = 'guardian'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: _selectedRole == 'guardian' ? AppColors.guardianAmber : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
AppStrings.guardianRole,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: _selectedRole == 'guardian' ? Colors.black : AppColors.textSecondaryDark,
),
),
),
),
),
),
],
),
),
const SizedBox(height: 20),
LuxuryTextField(
controller: _phoneController,
label: AppStrings.phoneNumber,
hint: '0791234567',
keyboardType: TextInputType.phone,
enabled: !isLoading,
prefixIcon: const Icon(CupertinoIcons.phone_fill, color: AppColors.saqelCyan, size: 20),
),
const SizedBox(height: 24),
LuxuryButton(
text: AppStrings.requestOtp,
isLoading: isLoading,
onPressed: () {
final phone = _phoneController.text.trim();
if (phone.isEmpty) {
SaqelToast.showError(context, 'يرجى إدخال رقم الهاتف المسجل في منصة صَقِل');
return;
}
context.read<AuthCubit>().sendOtp(phone, role: _selectedRole);
},
),
],
);
}
/// Step 2: OTP Verification
Widget _buildOtpStep(BuildContext context, AuthOtpSent state, bool isLoading) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.whatsappGreen.withAlpha(35),
shape: BoxShape.circle,
),
child: const Icon(CupertinoIcons.chat_bubble_2_fill, color: AppColors.whatsappGreen, size: 30),
),
),
const SizedBox(height: 20),
Text(
AppStrings.enterOtpTitle,
textAlign: TextAlign.center,
style: AppTypography.headlineMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 8),
Text(
'${AppStrings.enterOtpSubtitle} (${state.phoneNumber})',
textAlign: TextAlign.center,
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark),
),
const SizedBox(height: 24),
LuxuryTextField(
controller: _otpController,
label: 'رمز التحقق (6 أرقام)',
hint: '123456',
keyboardType: TextInputType.number,
enabled: !isLoading,
prefixIcon: const Icon(CupertinoIcons.lock_shield_fill, color: AppColors.appleBlue, size: 20),
),
const SizedBox(height: 24),
LuxuryButton(
text: AppStrings.verifyOtp,
isLoading: isLoading,
onPressed: () {
final otp = _otpController.text.trim();
if (otp.isEmpty) {
SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المستلم عبر الواتساب');
return;
}
context.read<AuthCubit>().verifyOtp(state.phoneNumber, otp, role: state.role);
},
),
const SizedBox(height: 14),
TextButton(
onPressed: isLoading ? null : () => context.read<AuthCubit>().checkSession(),
child: const Text(
'تغيير رقم الهاتف ↺',
style: TextStyle(
color: AppColors.textSecondaryDark,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
),
],
);
}
/// Step 3: National ID Verification (For Students)
Widget _buildNationalIdStep(BuildContext context, AuthNeedsNationalId state, bool isLoading) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(35),
shape: BoxShape.circle,
),
child: const Icon(CupertinoIcons.person_crop_square_fill, color: AppColors.saqelCyan, size: 30),
),
),
const SizedBox(height: 20),
Text(
AppStrings.enterNationalIdTitle,
textAlign: TextAlign.center,
style: AppTypography.headlineMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 8),
Text(
AppStrings.enterNationalIdSubtitle,
textAlign: TextAlign.center,
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark),
),
const SizedBox(height: 24),
LuxuryTextField(
controller: _nationalIdController,
label: AppStrings.nationalId,
hint: '2008XXXXXX',
keyboardType: TextInputType.number,
enabled: !isLoading,
prefixIcon: const Icon(CupertinoIcons.number, color: AppColors.saqelCyan, size: 20),
),
const SizedBox(height: 24),
LuxuryButton(
text: AppStrings.continueBtn,
isLoading: isLoading,
onPressed: () {
final nationalId = _nationalIdController.text.trim();
if (nationalId.isEmpty) {
SaqelToast.showError(context, 'يرجى إدخال الرقم الوطني الخاص بالطالب');
return;
}
context.read<AuthCubit>().verifyNationalId(state.identityToken, nationalId);
},
),
],
);
}
/// Step 4: Onboarding (For New Students not registered in school roster)
Widget _buildOnboardingStep(BuildContext context, AuthNeedsOnboarding state, bool isLoading) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.appleBlue.withAlpha(35),
shape: BoxShape.circle,
),
child: const Icon(CupertinoIcons.sparkles, color: AppColors.appleBlue, size: 30),
),
),
const SizedBox(height: 20),
Text(
'استكمال بيانات الطالب 🎓',
textAlign: TextAlign.center,
style: AppTypography.headlineMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 8),
Text(
'يرجى إدخال اسمك الكامل واختيار الفرع الأكاديمي لضبط المنهاج',
textAlign: TextAlign.center,
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark),
),
const SizedBox(height: 24),
LuxuryTextField(
controller: _fullNameController,
label: 'الاسم الكامل للطالب',
hint: 'حمزة أحمد الغويريين',
enabled: !isLoading,
prefixIcon: const Icon(CupertinoIcons.person_fill, color: AppColors.appleBlue, size: 20),
),
const SizedBox(height: 18),
// Stream Selector (العلمي / الأدبي)
Text(
'الفرع الأكاديمي',
style: AppTypography.titleMedium.copyWith(color: AppColors.textSecondaryDark, fontSize: 13),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: isLoading ? null : () => setState(() => _selectedStream = 'scientific'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _selectedStream == 'scientific' ? AppColors.saqelCyan.withAlpha(30) : AppColors.darkSurface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _selectedStream == 'scientific' ? AppColors.saqelCyan : AppColors.darkCardBorder,
),
),
child: Center(
child: Text(
'الفرع العلمي 🔬',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: _selectedStream == 'scientific' ? AppColors.saqelCyan : Colors.white,
),
),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: GestureDetector(
onTap: isLoading ? null : () => setState(() => _selectedStream = 'literary'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _selectedStream == 'literary' ? AppColors.guardianAmber.withAlpha(30) : AppColors.darkSurface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _selectedStream == 'literary' ? AppColors.guardianAmber : AppColors.darkCardBorder,
),
),
child: Center(
child: Text(
'الفرع الأدبي 📚',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: _selectedStream == 'literary' ? AppColors.guardianAmber : Colors.white,
),
),
),
),
),
),
],
),
const SizedBox(height: 24),
LuxuryButton(
text: 'فتح الملف والدخول للمنصة 🚀',
isLoading: isLoading,
onPressed: () {
final name = _fullNameController.text.trim();
if (name.isEmpty) {
SaqelToast.showError(context, 'يرجى إدخال الاسم الكامل للطالب');
return;
}
context.read<AuthCubit>().setupProfile(
identityToken: state.identityToken,
fullName: name,
nationalId: state.nationalId,
gradeLevel: _selectedGrade,
stream: _selectedStream,
);
},
),
],
);
}
}
@@ -0,0 +1,559 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/localization/app_strings.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/utils/saqel_toast.dart';
import '../../../data/models/user_model.dart';
import '../../../data/models/lesson_model.dart';
import '../../../logic/cubits/auth_cubit.dart';
import '../../../logic/cubits/dashboard_cubits.dart';
import '../../widgets/luxury_widgets.dart';
class UnifiedHomeScreen extends StatefulWidget {
final UserModel user;
final String activeRole;
const UnifiedHomeScreen({
super.key,
required this.user,
required this.activeRole,
});
@override
State<UnifiedHomeScreen> createState() => _UnifiedHomeScreenState();
}
class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
late String _currentRole;
@override
void initState() {
super.initState();
_currentRole = widget.activeRole;
_refreshData();
}
void _refreshData() {
if (_currentRole == 'student') {
context.read<StudentCubit>().fetchLessons(
gradeLevel: widget.user.gradeLevel,
stream: widget.user.stream,
initialReadiness: widget.user.readinessScore ?? 0.0,
);
} else {
context.read<GuardianCubit>().fetchDashboard();
}
}
void _switchRole(String newRole) {
if (_currentRole == newRole) return;
setState(() => _currentRole = newRole);
context.read<AuthCubit>().switchRole(newRole);
_refreshData();
}
@override
Widget build(BuildContext context) {
final isStudent = _currentRole == 'student';
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: RadialGradient(
center: isStudent ? const Alignment(0.8, -0.6) : const Alignment(-0.8, -0.6),
radius: 1.3,
colors: isStudent
? const [Color(0xFF0A2540), AppColors.darkBackground]
: const [Color(0xFF332008), AppColors.darkBackground],
),
),
child: SafeArea(
child: Column(
children: [
_buildTopBar(isStudent),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isStudent ? _buildStudentTab() : _buildGuardianTab(),
),
),
],
),
),
),
);
}
/// Top App Bar with Persona Switcher & Logout
Widget _buildTopBar(bool isStudent) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: const BoxDecoration(
color: Color(0x990D111A),
border: Border(
bottom: BorderSide(color: AppColors.darkCardBorder),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isStudent
? const [AppColors.appleBlue, AppColors.saqelCyan]
: const [AppColors.guardianAmber, Color(0xFFFBBF24)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
'ص',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: Colors.black),
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.user.name,
style: AppTypography.titleMedium.copyWith(color: Colors.white),
),
Text(
isStudent ? AppStrings.studentPortal : AppStrings.guardianPortal,
style: AppTypography.labelSmall.copyWith(color: AppColors.textMutedDark),
),
],
),
const Spacer(),
// Persona Toggle
Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Row(
children: [
_buildRolePill('الطالب', isStudent, () => _switchRole('student')),
_buildRolePill('ولي الأمر', !isStudent, () => _switchRole('guardian')),
],
),
),
const SizedBox(width: 8),
// Logout Icon
IconButton(
icon: const Icon(CupertinoIcons.power, color: AppColors.errorRed, size: 20),
onPressed: () => context.read<AuthCubit>().logout(),
tooltip: AppStrings.logout,
),
],
),
);
}
Widget _buildRolePill(String title, bool isSelected, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isSelected ? const Color(0x33FFFFFF) : Colors.transparent,
borderRadius: BorderRadius.circular(14),
),
child: Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
color: isSelected ? Colors.white : AppColors.textMutedDark,
),
),
),
);
}
/// 🎓 Real Student Tab (Data from API)
Widget _buildStudentTab() {
return BlocBuilder<StudentCubit, StudentState>(
builder: (context, state) {
if (state is StudentLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.saqelCyan),
);
} else if (state is StudentError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(CupertinoIcons.exclamationmark_triangle, color: AppColors.errorRed, size: 40),
const SizedBox(height: 12),
Text(state.message, style: const TextStyle(color: Colors.white)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _refreshData,
child: Text(AppStrings.retry),
),
],
),
);
} else if (state is StudentEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(CupertinoIcons.book, color: AppColors.textMutedDark, size: 48),
const SizedBox(height: 12),
Text(AppStrings.noLessonsFound, style: const TextStyle(color: AppColors.textSecondaryDark)),
],
),
);
} else if (state is StudentLoaded) {
return RefreshIndicator(
onRefresh: () async => _refreshData(),
child: ListView(
padding: const EdgeInsets.all(20),
children: [
// Real Readiness Score Card
LuxuryCard(
borderColor: AppColors.saqelCyan.withAlpha(50),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(30),
borderRadius: BorderRadius.circular(6),
),
child: Text(
widget.user.gradeLevel ?? 'توجيهي 2008',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppColors.saqelCyan,
),
),
),
const SizedBox(height: 8),
Text(
AppStrings.readinessScore,
style: AppTypography.headlineMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 4),
Text(
'تقييم حقيقي مبني على الكويزات والفحوصات السقراطية',
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark, fontSize: 12),
),
],
),
),
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: AppColors.saqelCyan, width: 3),
),
child: Center(
child: Text(
'${state.readinessScore.toStringAsFixed(0)}%',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.white),
),
),
),
],
),
),
const SizedBox(height: 20),
// Real Lessons List from API
Text(
AppStrings.tawjihiCurriculum,
style: AppTypography.titleMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 12),
...state.lessons.map((lesson) => _buildRealLessonItem(lesson)),
],
),
);
}
return const SizedBox.shrink();
},
);
}
Widget _buildRealLessonItem(LessonModel lesson) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: LuxuryCard(
padding: const EdgeInsets.all(16),
onTap: () {
SaqelToast.showInfo(
context,
'تشغيل الدرس: ${lesson.title}',
title: lesson.courseTitle,
);
},
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppColors.appleBlue.withAlpha(30),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(CupertinoIcons.play_fill, color: AppColors.appleBlue, size: 20),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
lesson.courseTitle,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppColors.saqelCyan,
),
),
if (lesson.isAiVersion) ...[
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
color: AppColors.adminViolet.withAlpha(40),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'ذكاء اصطناعي ✨',
style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: Color(0xFFA78BFA)),
),
),
],
],
),
const SizedBox(height: 4),
Text(
lesson.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white),
),
const SizedBox(height: 2),
Text(
lesson.formattedDuration,
style: const TextStyle(fontSize: 11, color: AppColors.textMutedDark),
),
],
),
),
],
),
),
);
}
/// 👨‍👧‍👦 Real Guardian Tab (Data from API)
Widget _buildGuardianTab() {
return BlocBuilder<GuardianCubit, GuardianState>(
builder: (context, state) {
if (state is GuardianLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.guardianAmber),
);
} else if (state is GuardianError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(CupertinoIcons.exclamationmark_triangle, color: AppColors.errorRed, size: 40),
const SizedBox(height: 12),
Text(state.message, style: const TextStyle(color: Colors.white)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _refreshData,
child: Text(AppStrings.retry),
),
],
),
);
} else if (state is GuardianEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(CupertinoIcons.person_2, color: AppColors.textMutedDark, size: 48),
const SizedBox(height: 12),
Text(AppStrings.noChildrenLinked, style: const TextStyle(color: AppColors.textSecondaryDark)),
],
),
);
} else if (state is GuardianLoaded) {
final selectedChild = state.children[state.selectedChildIndex];
return RefreshIndicator(
onRefresh: () async => _refreshData(),
child: ListView(
padding: const EdgeInsets.all(20),
children: [
Text(
AppStrings.linkedChildren,
style: AppTypography.titleMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 10),
// Multi-Child Switcher
Row(
children: List.generate(state.children.length, (index) {
final isSelected = index == state.selectedChildIndex;
final child = state.children[index];
return Expanded(
child: GestureDetector(
onTap: () => context.read<GuardianCubit>().selectChild(index),
child: Container(
margin: EdgeInsets.only(left: index == 0 ? 8 : 0),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? AppColors.guardianAmber.withAlpha(35) : AppColors.darkCard,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isSelected ? AppColors.guardianAmber : AppColors.darkCardBorder,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
child.name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: isSelected ? const Color(0xFFFBBF24) : Colors.white,
),
),
const SizedBox(height: 2),
Text(
child.gradeLevel ?? 'طالب مسجل',
style: const TextStyle(fontSize: 10.5, color: AppColors.textMutedDark),
),
],
),
),
),
);
}),
),
const SizedBox(height: 20),
// Selected Child Performance Bento
LuxuryCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'تقرير الطالب: ${selectedChild.name}',
style: AppTypography.titleMedium.copyWith(color: Colors.white),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.teacherEmerald.withAlpha(30),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'جاهزية ${selectedChild.readinessScore.toStringAsFixed(1)}%',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: AppColors.teacherEmerald,
),
),
),
],
),
const SizedBox(height: 6),
Text(
'المدرسة: ${selectedChild.schoolName ?? 'مدرسة الثقافة العسكرية'}',
style: const TextStyle(fontSize: 12, color: AppColors.textMutedDark),
),
const SizedBox(height: 16),
const Divider(color: AppColors.darkCardBorder),
const SizedBox(height: 10),
_buildGuardianStat('الامتحانات المنجزة بنجاح', '${selectedChild.examsPassed} من ${selectedChild.examsTotal}'),
_buildGuardianStat('الرقم الوطني', selectedChild.nationalId ?? 'مسجل بالهاتف'),
],
),
),
const SizedBox(height: 16),
// Real WhatsApp Reports Integration Banner
LuxuryCard(
backgroundColor: const Color(0xFF075E54).withAlpha(35),
borderColor: AppColors.whatsappGreen.withAlpha(80),
child: const Row(
children: [
Icon(CupertinoIcons.chat_bubble_fill, color: AppColors.whatsappGreen, size: 24),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'تقارير بوابة نبيه للواتساب',
style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w700, color: Colors.white),
),
SizedBox(height: 2),
Text(
'تصلك إشعارات الأداء والغياب آلياً عبر رقم الواتساب المسجل',
style: TextStyle(fontSize: 11, color: AppColors.textSecondaryDark),
),
],
),
),
],
),
),
],
),
);
}
return const SizedBox.shrink();
},
);
}
Widget _buildGuardianStat(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Text(label, style: const TextStyle(fontSize: 12, color: AppColors.textSecondaryDark)),
const Spacer(),
Text(value, style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700, color: Colors.white)),
],
),
);
}
}
@@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_theme.dart';
/// Reusable Apple-Grade Frosted Glass Card
class LuxuryCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
final VoidCallback? onTap;
final Color? borderColor;
final Color? backgroundColor;
const LuxuryCard({
super.key,
required this.child,
this.padding = const EdgeInsets.all(20),
this.onTap,
this.borderColor,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
final cardWidget = Container(
padding: padding,
decoration: BoxDecoration(
color: backgroundColor ?? AppColors.darkCard,
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: borderColor ?? AppColors.darkCardBorder,
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x66000000),
blurRadius: 25,
offset: Offset(0, 10),
),
],
),
child: child,
);
if (onTap != null) {
return GestureDetector(
onTap: onTap,
child: cardWidget,
);
}
return cardWidget;
}
}
/// Reusable Apple-Grade Primary Button
class LuxuryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final Color? backgroundColor;
final Color? textColor;
final IconData? icon;
const LuxuryButton({
super.key,
required this.text,
required this.onPressed,
this.isLoading = false,
this.backgroundColor,
this.textColor,
this.icon,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? AppColors.appleBlue,
foregroundColor: textColor ?? Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 0,
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: 8),
],
Text(
text,
style: AppTypography.titleMedium.copyWith(
color: textColor ?? Colors.white,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
/// Reusable Apple-Grade Text Field
class LuxuryTextField extends StatelessWidget {
final TextEditingController controller;
final String label;
final String hint;
final TextInputType keyboardType;
final Widget? prefixIcon;
final bool enabled;
const LuxuryTextField({
super.key,
required this.controller,
required this.label,
required this.hint,
this.keyboardType = TextInputType.text,
this.prefixIcon,
this.enabled = true,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: AppTypography.titleMedium.copyWith(
color: AppColors.textSecondaryDark,
fontSize: 13,
),
),
const SizedBox(height: 8),
TextField(
controller: controller,
keyboardType: keyboardType,
enabled: enabled,
style: AppTypography.bodyMedium.copyWith(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
),
decoration: InputDecoration(
hintText: hint,
hintStyle: AppTypography.bodyMedium.copyWith(
color: AppColors.textMutedDark,
),
prefixIcon: prefixIcon,
filled: true,
fillColor: AppColors.darkSurface,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: AppColors.darkCardBorder),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: AppColors.darkCardBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: AppColors.appleBlue, width: 1.5),
),
),
),
],
);
}
}