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,
),
),
],
),
),
],
),
),
),
);
}
}