136 lines
11 KiB
Dart
136 lines
11 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
|
|
import '../../services/directorate_api_service.dart';
|
|
|
|
/// School dashboard deliberately renders only server-provided records.
|
|
/// Uploads, exam dispatch, parent messaging, and roster imports stay unavailable
|
|
/// here until each action has an auditable production workflow.
|
|
class SchoolPrincipalScreen extends StatefulWidget {
|
|
const SchoolPrincipalScreen({super.key});
|
|
|
|
@override
|
|
State<SchoolPrincipalScreen> createState() => _SchoolPrincipalScreenState();
|
|
}
|
|
|
|
class _SchoolPrincipalScreenState extends State<SchoolPrincipalScreen> {
|
|
Future<Map<String, dynamic>>? _dashboard;
|
|
bool _busy = false;
|
|
String? _operationMessage;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_reload();
|
|
}
|
|
|
|
void _reload() => setState(() => _dashboard = DirectorateApiService.fetchSchoolDashboard());
|
|
|
|
Future<PlatformFile?> _pickVideo() async {
|
|
final picked = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: const ['mp4', 'mov', 'webm'], withData: true);
|
|
return picked?.files.isNotEmpty == true ? picked!.files.single : null;
|
|
}
|
|
|
|
Future<void> _run(String label, Future<String> Function() operation) async {
|
|
setState(() { _busy = true; _operationMessage = null; });
|
|
try {
|
|
final result = await operation();
|
|
if (mounted) setState(() => _operationMessage = result);
|
|
_reload();
|
|
} catch (error) {
|
|
if (mounted) setState(() => _operationMessage = '$label تعذر: $error');
|
|
} finally { if (mounted) setState(() => _busy = false); }
|
|
}
|
|
|
|
Future<void> _recordLesson(int schoolId, List<Map<String, dynamic>> teachers) async {
|
|
if (teachers.isEmpty) { setState(() => _operationMessage = 'لا يوجد معلمون متاحون من الخادم.'); return; }
|
|
final title = TextEditingController();
|
|
var selected = teachers.first;
|
|
final confirmed = await showDialog<bool>(context: context, builder: (dialogContext) => StatefulBuilder(builder: (_, setDialogState) => AlertDialog(
|
|
title: const Text('رفع حصة مرصودة'),
|
|
content: Column(mainAxisSize: MainAxisSize.min, children: [DropdownButton<Map<String, dynamic>>(value: selected, isExpanded: true, items: teachers.map((teacher) => DropdownMenuItem(value: teacher, child: Text(teacher['name']?.toString() ?? 'معلم'))).toList(), onChanged: (value) { if (value != null) setDialogState(() => selected = value); }), TextField(controller: title, decoration: const InputDecoration(labelText: 'عنوان الحصة'))]),
|
|
actions: [TextButton(onPressed: () => Navigator.pop(dialogContext, false), child: const Text('إلغاء')), FilledButton(onPressed: () => Navigator.pop(dialogContext, true), child: const Text('اختيار ملف'))],
|
|
)));
|
|
if (confirmed != true || title.text.trim().isEmpty) return;
|
|
final file = await _pickVideo(); if (file == null) return;
|
|
await _run('رفع الحصة', () async {
|
|
final result = await DirectorateApiService.recordLesson(schoolId: schoolId, teacherId: (selected['id'] as num).toInt(), subject: selected['subject']?.toString() ?? '', gradeLevel: selected['grade']?.toString() ?? 'grade_10', lessonTitle: title.text.trim(), durationMinutes: 0, filePath: file.path, fileBytes: file.bytes, fileName: file.name);
|
|
return 'تم تسجيل الحصة بحالة ${result.status}.';
|
|
});
|
|
}
|
|
|
|
Future<void> _uploadPanorama(int sessionId) async {
|
|
final file = await _pickVideo(); if (file == null) return;
|
|
await _run('رفع العينة', () async { await DirectorateApiService.uploadPanoramicSample(sessionId: sessionId, filePath: file.path, fileBytes: file.bytes, fileName: file.name); return 'تم حفظ العينة وربطها بالجلسة.'; });
|
|
}
|
|
|
|
Future<void> _importRoster(int schoolId) async {
|
|
final picked = await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: const ['json'], withData: true);
|
|
final file = picked?.files.isNotEmpty == true ? picked!.files.single : null;
|
|
if (file?.bytes == null) return;
|
|
final decoded = json.decode(utf8.decode(file!.bytes!));
|
|
if (decoded is! List) throw StateError('ملف الكشف يجب أن يحتوي قائمة JSON من السجلات.');
|
|
final records = decoded.whereType<Map>().map((row) => Map<String, dynamic>.from(row)).toList();
|
|
await _run('استيراد الكشف', () async { final result = await DirectorateApiService.importSchoolRoster(schoolId: schoolId, records: records); return 'استُورد ${result['imported_count'] ?? 0} سجل؛ الفاشل ${result['failed_count'] ?? 0}.'; });
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Scaffold(
|
|
backgroundColor: const Color(0xFF070B12),
|
|
appBar: AppBar(
|
|
backgroundColor: const Color(0xFF0E1626),
|
|
title: const Text('لوحة المدرسة'),
|
|
actions: [IconButton(onPressed: _reload, icon: const Icon(CupertinoIcons.refresh))],
|
|
),
|
|
body: FutureBuilder<Map<String, dynamic>>(
|
|
future: _dashboard,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState != ConnectionState.done) return const Center(child: CupertinoActivityIndicator());
|
|
if (snapshot.hasError) return Center(child: Padding(padding: const EdgeInsets.all(24), child: Text('تعذر تحميل بيانات المدرسة: ${snapshot.error}', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white70))));
|
|
final data = snapshot.data ?? const <String, dynamic>{};
|
|
final school = data['school'] is Map ? Map<String, dynamic>.from(data['school'] as Map) : const <String, dynamic>{};
|
|
final teachers = data['teachers'] is List ? (data['teachers'] as List).whereType<Map>().map((item) => Map<String, dynamic>.from(item)).toList() : const <Map<String, dynamic>>[];
|
|
final exams = data['active_exams'] is List ? (data['active_exams'] as List).whereType<Map>().map((item) => Map<String, dynamic>.from(item)).toList() : const <Map<String, dynamic>>[];
|
|
return RefreshIndicator(onRefresh: () async => _reload(), child: ListView(padding: const EdgeInsets.all(16), children: [
|
|
Text(school['name']?.toString() ?? 'مدرسة غير محددة', style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 4),
|
|
Text('${school['governorate'] ?? 'الموقع غير متاح'} • الطلبة: ${school['student_count'] ?? 'غير متاح'} • المعلمون: ${school['teacher_count'] ?? 'غير متاح'}', style: const TextStyle(color: Colors.white60)),
|
|
const SizedBox(height: 20),
|
|
_operations(school, teachers, exams),
|
|
const SizedBox(height: 20),
|
|
const Text('المعلمون', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
if (teachers.isEmpty) const Text('لا توجد بيانات معلمين متاحة من الخادم.', style: TextStyle(color: Colors.white60)) else ...teachers.map(_teacher),
|
|
const SizedBox(height: 20),
|
|
const Text('الامتحانات الفعالة', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
if (exams.isEmpty) const Text('لا توجد امتحانات فعالة في البيانات المستلمة.', style: TextStyle(color: Colors.white60)) else ...exams.map(_exam),
|
|
]));
|
|
},
|
|
),
|
|
);
|
|
|
|
Widget _notice(String title, String body) => Container(padding: const EdgeInsets.all(14), decoration: BoxDecoration(color: const Color(0xFF4338CA).withOpacity(.2), borderRadius: BorderRadius.circular(12)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(title, style: const TextStyle(color: Color(0xFFC4B5FD), fontWeight: FontWeight.bold)), const SizedBox(height: 5), Text(body, style: const TextStyle(color: Colors.white70, height: 1.4))]));
|
|
Widget _operations(Map<String, dynamic> school, List<Map<String, dynamic>> teachers, List<Map<String, dynamic>> exams) {
|
|
final schoolId = (school['id'] as num?)?.toInt();
|
|
if (schoolId == null) return _notice('الإجراءات غير متاحة', 'لم يعِد الخادم هوية مدرسة صالحة.');
|
|
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
|
_notice('إجراءات خادمية فعلية', 'كل زر يرسل طلباً حقيقياً للخادم. لا توجد موافقات أو نتائج محلية.'),
|
|
if (_operationMessage != null) Padding(padding: const EdgeInsets.only(top: 8), child: Text(_operationMessage!, style: const TextStyle(color: Color(0xFFC4B5FD)))),
|
|
const SizedBox(height: 8),
|
|
Wrap(spacing: 8, runSpacing: 8, children: [
|
|
OutlinedButton(onPressed: _busy ? null : () => _recordLesson(schoolId, teachers), child: const Text('رفع حصة مرصودة')),
|
|
OutlinedButton(onPressed: _busy ? null : () => _run('إرسال التقارير', () async { final result = await DirectorateApiService.dispatchParentReports(schoolId: schoolId); return 'أُرسل ${result['total_dispatched'] ?? 0} تقرير؛ فشل ${result['failed_count'] ?? 0}.'; }), child: const Text('إرسال تقارير الأولياء')),
|
|
OutlinedButton(onPressed: _busy ? null : () => _importRoster(schoolId), child: const Text('استيراد كشف JSON')),
|
|
]),
|
|
if (exams.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 8), child: Wrap(spacing: 8, runSpacing: 8, children: exams.where((exam) => exam['id'] is num).map((exam) => OutlinedButton(onPressed: _busy ? null : () => _run('تفعيل الامتحان', () async { await DirectorateApiService.pushExamToLab(examId: (exam['id'] as num).toInt(), schoolId: schoolId); return 'تم تفعيل الامتحان للمختبر المسجل.'; }), child: Text('تفعيل: ${exam['title'] ?? 'امتحان'}'))).toList())),
|
|
if (exams.any((exam) => exam['session_id'] is num)) Padding(padding: const EdgeInsets.only(top: 8), child: OutlinedButton(onPressed: _busy ? null : () => _uploadPanorama((exams.firstWhere((exam) => exam['session_id'] is num)['session_id'] as num).toInt()), child: const Text('رفع عينة بانورامية'))),
|
|
const SizedBox(height: 20),
|
|
]);
|
|
}
|
|
Widget _teacher(Map<String, dynamic> item) => Card(color: const Color(0xFF0F172A), child: ListTile(title: Text(item['name']?.toString() ?? 'معلم بدون اسم', style: const TextStyle(color: Colors.white)), subtitle: Text('${item['subject'] ?? 'المبحث غير متاح'} • ${item['quota_status'] ?? 'الحالة غير متاحة'}', style: const TextStyle(color: Colors.white60))));
|
|
Widget _exam(Map<String, dynamic> item) => Card(color: const Color(0xFF0F172A), child: ListTile(title: Text(item['title']?.toString() ?? 'امتحان بدون عنوان', style: const TextStyle(color: Colors.white)), subtitle: Text('${item['subject'] ?? 'المبحث غير متاح'} • ${item['status'] ?? 'الحالة غير متاحة'}', style: const TextStyle(color: Colors.white60))));
|
|
}
|