feat(apps): الدراور على نمط سيرو — المكافآت والدعوات والمساعدة
جُرد دراور سيرو: 14 مدخلاً للراكب (map_menu_widget) و23 للسائق (drawer_captain). بُني ما له نقطة فعلية على NestJS، والباقي مذكور صراحةً. ## مبنيّ - **المكافآت** (features/rewards): GET /rewards/referrals و /rewards/coupons — كلاهما مُتحقَّق حيّاً. كود الدعوة يُنسخ ويُشارك، وعدّادات دعوتَهم/قيد التأكيد/مدفوعة. يقابل Promos + Invite Driver في سيرو - **شارك التطبيق · قيّم التطبيق · سياسة الخصوصية · تطبيق الطرف الآخر** — أفعال محليّة بلا نقطة خادم (share_plus + url_launcher) - **المساعدة** وُسّعت: أسئلة شائعة قابلة للطيّ · واتساب الدعم · **فحص توافق الجهاز** (نمط سيرو) يفحص ما يُعطّل التطبيق فعلاً: خدمة الموقع وإذنها وإذن الإشعارات — أسباب حقيقية لشكوى «التطبيق لا يعمل» - روابط المتجر والخصوصية والدعم صارت قيم بناء لكل مستأجر في BuildConfig ## غير مبنيّ — ولا نقطة خادم له الشكاوى (R3 في الباكلوج) · الإحصاءات · التحدّيات ولوحة المتصدّرين · مركز الصيانة · جدولي · التأمين الصحي (مؤجَّل بقرار المالك) · الطعام والمواصلات (خارج النطاق، docs/39 §1). بناء واجهة بلا خادم يعني شاشة تكذب على المستخدم. ## سجلّ الإشعارات سيرو يعرض قائمة إشعارات؛ الخادم يوفّر POST /notifications/token فقط بلا نقطة سرد. لم تُبنَ شاشة قائمة على فراغ. flutter analyze نظيف في التطبيقين. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6a33cc7241
commit
ca766a2d4e
@@ -0,0 +1,201 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../core/design/tokens.dart';
|
||||
import '../../../core/design/tripz_colors.dart';
|
||||
import '../../../core/design/typography.dart';
|
||||
import '../../../core/di.dart';
|
||||
import '../../../core/l10n/l10n.dart';
|
||||
import '../../../core/ui/tripz_button.dart';
|
||||
import '../../../core/ui/tripz_card.dart';
|
||||
import '../../../core/ui/tripz_scaffold.dart';
|
||||
import '../cubit/rewards_cubit.dart';
|
||||
|
||||
/// «مكافآتي» — الدعوات والكوبونات في شاشة واحدة (نمط سيرو: Promos + Invite).
|
||||
class RewardsPage extends StatefulWidget {
|
||||
const RewardsPage({super.key});
|
||||
|
||||
@override
|
||||
State<RewardsPage> createState() => _RewardsPageState();
|
||||
}
|
||||
|
||||
class _RewardsPageState extends State<RewardsPage> {
|
||||
late final RewardsCubit _cubit = sl<RewardsCubit>()..load();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return BlocProvider.value(
|
||||
value: _cubit,
|
||||
child: BlocBuilder<RewardsCubit, RewardsState>(
|
||||
builder: (context, state) {
|
||||
final ref = state.referrals;
|
||||
return TripzScaffold(
|
||||
title: l10n.rewardsTitle,
|
||||
status: state.status,
|
||||
error: state.error,
|
||||
onRetry: _cubit.load,
|
||||
child: ListView(
|
||||
children: [
|
||||
if (ref != null) _InviteCard(code: ref.code, summary: ref),
|
||||
const SizedBox(height: Space.lg),
|
||||
Text(l10n.rewardsCoupons, style: context.texts.titleMedium),
|
||||
const SizedBox(height: Space.xs),
|
||||
if (state.coupons.isEmpty)
|
||||
Text(
|
||||
l10n.rewardsCouponsEmpty,
|
||||
style: context.texts.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final coupon in state.coupons) ...[
|
||||
TripzCard(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_offer_outlined,
|
||||
color: context.tripzColors.success,
|
||||
),
|
||||
const SizedBox(width: Space.sm),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
coupon.code,
|
||||
textDirection: TextDirection.ltr,
|
||||
style: numericStyle(context.texts.titleSmall),
|
||||
),
|
||||
if (coupon.description.isNotEmpty)
|
||||
Text(
|
||||
coupon.description,
|
||||
style: context.texts.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Space.xs),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InviteCard extends StatelessWidget {
|
||||
const _InviteCard({required this.code, required this.summary});
|
||||
|
||||
final String code;
|
||||
final dynamic summary;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
padding: Space.page,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.primaryContainer,
|
||||
borderRadius: Radii.card_,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.rewardsInviteTitle,
|
||||
style: context.texts.titleMedium?.copyWith(
|
||||
color: context.colors.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Space.xxs),
|
||||
Text(
|
||||
l10n.rewardsInviteLead,
|
||||
style: context.texts.bodySmall?.copyWith(
|
||||
color: context.colors.onPrimaryContainer.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Space.md),
|
||||
// الكود يُنسخ ويُشارك — الطريقان اللذان يستعملهما الناس فعلاً.
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SelectableText(
|
||||
code,
|
||||
textDirection: TextDirection.ltr,
|
||||
style: numericStyle(context.texts.headlineSmall).copyWith(
|
||||
letterSpacing: 3,
|
||||
color: context.colors.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy_rounded),
|
||||
color: context.colors.onPrimaryContainer,
|
||||
onPressed: () => Clipboard.setData(ClipboardData(text: code)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: Space.sm),
|
||||
TripzButton.primary(
|
||||
label: l10n.menuShare,
|
||||
icon: Icons.share_outlined,
|
||||
onPressed: () =>
|
||||
SharePlus.instance.share(ShareParams(text: l10n.shareMessage(code))),
|
||||
),
|
||||
const SizedBox(height: Space.md),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_Stat(label: l10n.rewardsInvited, value: summary.total),
|
||||
_Stat(label: l10n.rewardsPending, value: summary.pending),
|
||||
_Stat(label: l10n.rewardsPaid, value: summary.paid),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Stat extends StatelessWidget {
|
||||
const _Stat({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final int value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'$value',
|
||||
style: numericStyle(context.texts.titleLarge).copyWith(
|
||||
color: context.colors.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: context.texts.labelSmall?.copyWith(
|
||||
color: context.colors.onPrimaryContainer.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user