Implement verified curriculum, video review, watch sessions, ledger safeguards, and remove demo data

This commit is contained in:
Hamza-Ayed
2026-09-09 17:04:08 +03:00
parent 0dc5df7b5e
commit 5af684a892
49 changed files with 1809 additions and 3005 deletions
@@ -188,9 +188,9 @@ class TeacherProfileModel {
required this.schoolName,
required this.gradesTaught,
required this.bio,
this.rating = 4.9,
this.studentsCount = 42,
this.lessonsCount = 28,
this.rating = 0,
this.studentsCount = 0,
this.lessonsCount = 0,
});
factory TeacherProfileModel.fromJson(Map<String, dynamic> json) {
@@ -28,6 +28,8 @@ class TeacherMonetizationState {
final List<TeacherPayoutRequestModel> payoutRequests;
final bool isSubmittingPayout;
final bool isLoading;
final bool financialUnavailable;
final String financialMessage;
const TeacherMonetizationState({
required this.institutionalStudents,
@@ -44,6 +46,8 @@ class TeacherMonetizationState {
required this.payoutRequests,
required this.isSubmittingPayout,
this.isLoading = false,
this.financialUnavailable = false,
this.financialMessage = '',
});
TeacherMonetizationState copyWith({
@@ -61,6 +65,8 @@ class TeacherMonetizationState {
List<TeacherPayoutRequestModel>? payoutRequests,
bool? isSubmittingPayout,
bool? isLoading,
bool? financialUnavailable,
String? financialMessage,
}) {
return TeacherMonetizationState(
institutionalStudents:
@@ -78,6 +84,8 @@ class TeacherMonetizationState {
payoutRequests: payoutRequests ?? this.payoutRequests,
isSubmittingPayout: isSubmittingPayout ?? this.isSubmittingPayout,
isLoading: isLoading ?? this.isLoading,
financialUnavailable: financialUnavailable ?? this.financialUnavailable,
financialMessage: financialMessage ?? this.financialMessage,
);
}
}
@@ -116,16 +124,21 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
final mkt =
audience['marketplace_students'] as Map<String, dynamic>? ?? {};
final wallet = data['wallet'] as Map<String, dynamic>? ?? {};
final unavailable = data['financial_status'] == 'unavailable';
final instCount = (inst['count'] as num?)?.toDouble() ?? 0.0;
final paidCount = (mkt['count'] as num?)?.toDouble() ?? 0.0;
final gross =
(data['gross_revenue_jod'] as num?)?.toDouble() ?? (paidCount * 20.0);
final tShare = gross * 0.55;
final dShare = gross * 0.15;
final pShare = gross * 0.30;
final split = data['revenue_split'] as Map<String, dynamic>? ?? {};
// Never infer a wallet from subscriber counts, course prices, or old
// percentages. The backend returns null until a provider-backed ledger
// and eligible watch-minute policy are live.
final gross = (data['gross_revenue_jod'] as num?)?.toDouble() ?? 0.0;
final tShare = (split['teacher_amount_jod'] as num?)?.toDouble() ?? 0.0;
final dShare =
(split['directorate_amount_jod'] as num?)?.toDouble() ?? 0.0;
final pShare = (split['platform_amount_jod'] as num?)?.toDouble() ?? 0.0;
final avail =
(wallet['available_balance_jod'] as num?)?.toDouble() ?? tShare;
(wallet['available_balance_jod'] as num?)?.toDouble() ?? 0.0;
final withdrawn =
(wallet['total_withdrawn_jod'] as num?)?.toDouble() ?? 0.0;
final alias = wallet['cliq_payout_alias']?.toString() ?? '';
@@ -154,6 +167,8 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
cliqAlias: alias,
courses: courses,
payoutRequests: payouts,
financialUnavailable: unavailable,
financialMessage: data['financial_message']?.toString() ?? '',
isLoading: false,
));
} catch (_) {
@@ -162,15 +177,8 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
}
void updateSubscribers(double subscribers) {
final gross = subscribers * state.pricePerCourse;
emit(state.copyWith(
studentSubscribers: subscribers,
grossRevenue: gross,
teacherShare: gross * 0.55,
directorateShare: gross * 0.15,
platformShare: gross * 0.30,
availableBalance: gross * 0.55,
));
// Presentation must not fabricate revenue from a manually changed count.
emit(state.copyWith(studentSubscribers: subscribers));
}
Future<void> submitCliqPayout({
@@ -186,14 +194,8 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
final updatedList =
List<TeacherPayoutRequestModel>.from(state.payoutRequests)
..insert(0, req);
final newAvail =
(state.availableBalance - amountJod).clamp(0.0, double.infinity);
final newWithdrawn = state.totalWithdrawn + amountJod;
emit(state.copyWith(
payoutRequests: updatedList,
availableBalance: newAvail,
totalWithdrawn: newWithdrawn,
isSubmittingPayout: false,
));
} catch (_) {
@@ -286,6 +286,40 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
Widget build(BuildContext context) {
return BlocBuilder<TeacherMonetizationCubit, TeacherMonetizationState>(
builder: (context, state) {
if (state.financialUnavailable) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: TeacherTheme.surfaceCard,
borderRadius: BorderRadius.circular(18),
border: Border.all(color: TeacherTheme.surfaceBorder),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(CupertinoIcons.lock_circle_fill,
color: TeacherTheme.royalGold, size: 34),
const SizedBox(height: 12),
const Text('الأرباح والسحب غير متاحين حالياً',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w800)),
const SizedBox(height: 8),
Text(state.financialMessage,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF94A3B8), fontSize: 13)),
],
),
),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(