Harden published curriculum and student flows
This commit is contained in:
+698
-591
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -116,7 +116,7 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
||||
Icon(CupertinoIcons.checkmark_seal_fill, color: AppColors.saqelCyan, size: 14),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'المنهاج الوزاري الأردني 🇯🇴',
|
||||
'فهرس الدروس المنشورة',
|
||||
style: TextStyle(color: AppColors.saqelCyan, fontSize: 11, fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
@@ -141,7 +141,9 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
||||
const SizedBox(height: 14),
|
||||
LuxuryButton(
|
||||
text: 'إعادة المحاولة',
|
||||
onPressed: () => context.read<CurriculumCubit>().fetchSubjects(),
|
||||
onPressed: () => context.read<CurriculumCubit>().fetchSubjects(
|
||||
gradeLevel: GradeLevelModel.normalizeGradeKey(widget.initialGrade ?? 'grade_10'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -162,7 +164,7 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'المواد المقررة — ${GradeLevelModel.getGradeName(state.selectedGrade)} 📚',
|
||||
'المواد المنشورة — ${GradeLevelModel.getGradeName(state.selectedGrade)} 📚',
|
||||
style: AppTypography.displayLarge.copyWith(color: Colors.white, fontSize: 20),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -188,7 +190,8 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Official Academic Grade Level Banner (Strict Grade-Gate — الصف العاشر فقط)
|
||||
// The catalogue lists reviewed publications; do not claim
|
||||
// ministry certification without a separate source of proof.
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -212,12 +215,12 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${GradeLevelModel.getGradeName(state.selectedGrade)} — المسار الأكاديمي المعتمد',
|
||||
'${GradeLevelModel.getGradeName(state.selectedGrade)} — الدروس المنشورة',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'خطة دراسية موحدة ومطابقة للمنهاج الوزاري الأردني المطور',
|
||||
'تظهر هنا الدروس التي اكتملت مراجعتها ونُشرت على المنصة',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
],
|
||||
@@ -255,15 +258,17 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
||||
child: const Icon(CupertinoIcons.square_grid_2x2, size: 32, color: AppColors.saqelCyan),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'لا توجد مواد مضافة بعد في السيرفر',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16),
|
||||
Text(
|
||||
state.subjects.isEmpty ? 'لا توجد دروس منشورة لهذا الصف بعد' : 'لا توجد مواد تطابق البحث',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'يتم إدراج المواد تلقائياً بمجرد رفع وتحليل كتب المناهج في الخادم',
|
||||
Text(
|
||||
state.subjects.isEmpty
|
||||
? 'يمكنك العودة لاحقاً للاطلاع على الحزم بعد نشرها.'
|
||||
: 'جرّب اسم مادة مختلفاً أو امسح البحث.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
|
||||
style: const TextStyle(color: AppColors.textSecondaryDark, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,20 +7,26 @@ import '../../widgets/luxury_widgets.dart';
|
||||
|
||||
/// Apple Cupertino Sheet for choosing a teacher when multiple published video versions exist for a lesson.
|
||||
/// Conforms to Saqel product rule: "أكثر من حصة متاحة: قائمة المعلمين مرتبة بالتقييم الموثوق، مع عدد التقييمات والتصفح على دفعات."
|
||||
class TeacherSelectionSheet extends StatelessWidget {
|
||||
class TeacherSelectionSheet extends StatefulWidget {
|
||||
final CurriculumLessonItemModel lesson;
|
||||
final List<PublishedLessonVideoModel> videos;
|
||||
final int? nextCursor;
|
||||
final int availableCount;
|
||||
|
||||
const TeacherSelectionSheet({
|
||||
super.key,
|
||||
required this.lesson,
|
||||
required this.videos,
|
||||
required this.nextCursor,
|
||||
required this.availableCount,
|
||||
});
|
||||
|
||||
static Future<PublishedLessonVideoModel?> show(
|
||||
BuildContext context, {
|
||||
required CurriculumLessonItemModel lesson,
|
||||
required List<PublishedLessonVideoModel> videos,
|
||||
required int? nextCursor,
|
||||
required int availableCount,
|
||||
}) {
|
||||
return showModalBottomSheet<PublishedLessonVideoModel>(
|
||||
context: context,
|
||||
@@ -29,20 +35,43 @@ class TeacherSelectionSheet extends StatelessWidget {
|
||||
builder: (sheetContext) => TeacherSelectionSheet(
|
||||
lesson: lesson,
|
||||
videos: videos,
|
||||
nextCursor: nextCursor,
|
||||
availableCount: availableCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Sort server-authoritatively by rating desc, then rating count desc
|
||||
final sortedVideos = List<PublishedLessonVideoModel>.from(videos)
|
||||
..sort((a, b) {
|
||||
final cmp = b.rating.compareTo(a.rating);
|
||||
if (cmp != 0) return cmp;
|
||||
return b.ratingCount.compareTo(a.ratingCount);
|
||||
});
|
||||
State<TeacherSelectionSheet> createState() => _TeacherSelectionSheetState();
|
||||
}
|
||||
|
||||
class _TeacherSelectionSheetState extends State<TeacherSelectionSheet> {
|
||||
late final List<PublishedLessonVideoModel> _videos = List.of(widget.videos);
|
||||
late int? _nextCursor = widget.nextCursor;
|
||||
bool _loading = false;
|
||||
bool _failed = false;
|
||||
|
||||
Future<void> _loadMore() async {
|
||||
final cursor = _nextCursor;
|
||||
final lessonId = widget.lesson.curriculumLessonId;
|
||||
if (_loading || cursor == null || lessonId == null) return;
|
||||
setState(() { _loading = true; _failed = false; });
|
||||
try {
|
||||
final page = await CurriculumRepository().getPublishedLessonVideosPage(lessonId, cursor: cursor, limit: 20);
|
||||
if (!mounted) return;
|
||||
final known = _videos.map((video) => video.videoVersionId).toSet();
|
||||
setState(() {
|
||||
_videos.addAll(page.items.where((video) => known.add(video.videoVersionId)));
|
||||
_nextCursor = page.nextCursor;
|
||||
_loading = false;
|
||||
});
|
||||
} catch (_) {
|
||||
if (mounted) setState(() { _loading = false; _failed = true; });
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.82,
|
||||
@@ -103,7 +132,7 @@ class TeacherSelectionSheet extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
lesson.title,
|
||||
widget.lesson.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
@@ -161,23 +190,21 @@ class TeacherSelectionSheet extends StatelessWidget {
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
shrinkWrap: true,
|
||||
itemCount: sortedVideos.length,
|
||||
itemCount: _videos.length + (_nextCursor != null ? 1 : 0),
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (ctx, idx) {
|
||||
final video = sortedVideos[idx];
|
||||
if (idx == _videos.length) {
|
||||
return Center(child: TextButton(
|
||||
onPressed: _loading ? null : _loadMore,
|
||||
child: Text(_loading ? 'جارٍ تحميل المزيد…' : _failed ? 'تعذر التحميل — أعد المحاولة' : 'عرض المزيد (${_videos.length} من ${widget.availableCount})'),
|
||||
));
|
||||
}
|
||||
final video = _videos[idx];
|
||||
final isNew = video.ratingCount == 0;
|
||||
final initials = video.teacherName.isNotEmpty
|
||||
? video.teacherName.trim().characters.first
|
||||
: 'م';
|
||||
|
||||
final approaches = [
|
||||
'تركيز على خطوات الحل الوزاري والتحليل المفاهيمي',
|
||||
'تبسيط القواعد مع أمثلة حياتية وتطبيقات واقعية',
|
||||
'تدريبات مكثفة ونماذج امتحانات وزارية سابقة',
|
||||
'شرح تفاعلي مرئي مع استنتاج القوانين خطوة بخطوة',
|
||||
];
|
||||
final approach = approaches[idx % approaches.length];
|
||||
|
||||
return LuxuryCard(
|
||||
borderColor: idx == 0
|
||||
? AppColors.saqelCyan.withAlpha(90)
|
||||
@@ -344,8 +371,8 @@ class TeacherSelectionSheet extends StatelessWidget {
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
// Pedagogical approach badge
|
||||
Container(
|
||||
// Only metadata supplied by the published video.
|
||||
if (video.durationSeconds > 0 || video.publishedAt != null) Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
@@ -356,14 +383,17 @@ class TeacherSelectionSheet extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
CupertinoIcons.lightbulb_fill,
|
||||
CupertinoIcons.time,
|
||||
color: AppColors.guardianAmber,
|
||||
size: 12,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
approach,
|
||||
[
|
||||
if (video.durationSeconds > 0) '${(video.durationSeconds / 60).ceil()} دقيقة',
|
||||
if (video.publishedAt != null) 'نُشرت ${video.publishedAt!.year}-${video.publishedAt!.month.toString().padLeft(2, '0')}-${video.publishedAt!.day.toString().padLeft(2, '0')}',
|
||||
].join(' · '),
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondaryDark,
|
||||
fontSize: 11,
|
||||
|
||||
Reference in New Issue
Block a user