Commit 55151ba2 authored by DatHV's avatar DatHV
Browse files

update history point, manager

parent f714cdcc
import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import 'package:mypoint_flutter_app/screen/game/models/game_bundle_item_model.dart'; import 'package:mypoint_flutter_app/screen/game/models/game_bundle_item_model.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../../configs/constants.dart'; import '../../configs/constants.dart';
class GameTabViewModel extends RestfulApiViewModel { class GameTabViewModel extends RestfulApiViewModel {
......
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import 'models/transaction_summary_by_date_model.dart';
class MonthlyPointsChart extends StatelessWidget {
const MonthlyPointsChart({
super.key,
required this.items,
required this.date,
this.onPrevMonth,
this.onNextMonth,
this.onChangeDate,
});
final List<DaySummaryChartModel> items;
final DateTime date;
final VoidCallback? onPrevMonth;
final VoidCallback? onNextMonth;
final VoidCallback? onChangeDate;
final EdgeInsets cardPadding = const EdgeInsets.all(16);
@override
Widget build(BuildContext context) {
final parsed = _parseToDayMap(items, date);
final daysInMonth = DateUtils.getDaysInMonth(date.year, date.month);
final total = parsed.values.fold<double>(0, (p, e) => p + e);
final maxVal = (parsed.values.isEmpty ? 0 : parsed.values.reduce((a, b) => a > b ? a : b)).abs();
final yMax = _niceMax(maxVal.toDouble());
final yStep = _niceStep(yMax);
final stats = _statsByDay(items, date);
final barGroups = List.generate(daysInMonth, (i) {
final day = i + 1;
final v = parsed[day] ?? 0.0;
final color = v >= 0 ? const Color(0xFFFE515A) : const Color(0xFF21C777);
return BarChartGroupData(
x: day,
barRods: [BarChartRodData(toY: v, width: 8, borderRadius: BorderRadius.circular(2), color: color)],
);
});
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 12, offset: Offset(0, 3))],
),
padding: cardPadding,
margin: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 4),
const Center(child: Text('Thống kê tích điểm', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600))),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_formatInt(total),
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.w700,
color: Color(0xFF21C777),
height: 1.0,
),
),
const SizedBox(width: 4),
Image.asset('assets/images/ic_point.png', width: 20, height: 20),
],
),
const SizedBox(height: 24),
SizedBox(
height: 160,
child: BarChart(
BarChartData(
minY: 0,
maxY: yMax,
barGroups: barGroups,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: yStep,
getDrawingHorizontalLine: (v) => FlLine(color: Colors.black12, strokeWidth: 1),
),
extraLinesData: ExtraLinesData(
extraLinesOnTop: true,
horizontalLines: [
HorizontalLine(y: 0, color: Colors.black12, strokeWidth: 1),
HorizontalLine(y: yMax, color: Colors.black12, strokeWidth: 1),
],
),
titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
reservedSize: 28,
showTitles: true,
interval: yStep,
getTitlesWidget:
(value, meta) => Text(
value.toInt().toString(),
style: const TextStyle(fontSize: 10, color: Colors.black54),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 4, // hiển thị 1,5,9,...; muốn 1,5,10,15... đổi thành 5
getTitlesWidget: (value, meta) {
final d = value.toInt();
if (d < 1 || d > daysInMonth) return const SizedBox.shrink();
// chỉ hiện 1,5,10,15,20,25,31 để đỡ rối
var marks = {1, 5, 10, 15, 20, 25, daysInMonth};
if (!marks.contains(d)) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Text('$d', style: const TextStyle(fontSize: 10, color: Colors.black54)),
);
},
),
),
),
borderData: FlBorderData(show: false),
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: true,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: Colors.black87,
fitInsideHorizontally: true,
fitInsideVertically: true,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final day = group.x.toInt();
final stat = stats[day];
final r = stat?.reward ?? 0;
return BarTooltipItem(
textAlign: TextAlign.center,
'Ngày $day/${date.month}\n'
'Tích điểm: ${_formatInt(r)}',
const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500),
);
},
),
),
),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
visualDensity: VisualDensity.compact,
onPressed: onPrevMonth,
icon: Icon(Icons.chevron_left, color: Colors.blue[900]),
),
GestureDetector(
onTap: onChangeDate,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Text(
'Tháng ${date.month}/${date.year}',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15, color: Colors.blue[900]),
),
),
),
IconButton(
visualDensity: VisualDensity.compact,
onPressed: onNextMonth,
icon: Icon(Icons.chevron_right, color: Colors.blue[900]),
),
],
),
],
),
);
}
/// Map<day, value> cho tháng đang chọn
Map<int, double> _parseToDayMap(List<DaySummaryChartModel> list, DateTime month) {
final map = <int, double>{};
final m = month.month;
final y = month.year;
for (final e in list) {
final dt = _parseYmd(e.summaryDate);
if (dt == null || dt.month != m || dt.year != y) continue;
final reward = _toDouble(e.rewardDayTotal);
final redeem = _toDouble(e.redeemDayTotal);
final adjust = _toDouble(e.adjustDayTotal);
final val = reward - redeem + adjust;
map[dt.day] = (map[dt.day] ?? 0) + val; // gộp nếu trùng ngày
}
return map;
}
static final _fmtYmd = DateFormat('yyyy-MM-dd');
DateTime? _parseYmd(String? s) {
if (s == null || s.isEmpty) return null;
try {
return _fmtYmd.parseStrict(s);
} catch (_) {
return DateTime.tryParse(s);
}
}
double _toDouble(String? v) {
if (v == null) return 0;
final s = v.replaceAll(RegExp(r'[,\s_]'), '');
return double.tryParse(s) ?? 0;
}
static String _formatInt(double v) => v.toStringAsFixed(0);
/// Làm tròn max Y cho đẹp (bước 4/5)
double _niceMax(double maxVal) {
if (maxVal <= 0) return 10;
// làm tròn lên tới bội số 4 hoặc 5 gần nhất
final candidates = [4, 5, 10];
for (final step in candidates) {
final up = ((maxVal / step).ceil()) * step;
if (up >= maxVal && up / step <= 6) return up.toDouble(); // tối đa 6 vạch cho gọn
}
return ((maxVal / 10).ceil()) * 10.0;
}
double _niceStep(double yMax) {
if (yMax <= 10) return 2; // 0,2,4,6,8,10
if (yMax <= 20) return 4; // 0,4,8,12,16,20
if (yMax <= 50) return 10;
return 20;
}
Map<int, _DayStat> _statsByDay(List<DaySummaryChartModel> list, DateTime month) {
final map = <int, _DayStat>{};
for (final e in list) {
final dt = _parseYmd(e.summaryDate);
if (dt == null || dt.month != month.month || dt.year != month.year) continue;
final rwd = _toDouble(e.rewardDayTotal);
final rdm = _toDouble(e.redeemDayTotal);
final adj = _toDouble(e.adjustDayTotal);
final cur = map[dt.day];
map[dt.day] =
cur == null ? _DayStat(rwd, rdm, adj) : _DayStat(cur.reward + rwd, cur.redeem + rdm, cur.adjust + adj);
}
return map;
}
}
class _DayStat {
_DayStat(this.reward, this.redeem, this.adjust);
final double reward, redeem, adjust;
double get net => reward - redeem + adjust;
}
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:month_picker_dialog/month_picker_dialog.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/resources/base_color.dart';
import 'package:mypoint_flutter_app/widgets/custom_toast_message.dart';
import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_navigation_bar.dart';
import '../../extensions/date_format.dart';
import 'history_point_chart.dart';
import 'history_point_viewmodel.dart';
import 'models/cash_history_model.dart';
import 'models/transaction_history_model.dart';
class HistoryPointScreen extends StatefulWidget {
const HistoryPointScreen({super.key});
@override
State<HistoryPointScreen> createState() => _HistoryPointScreenState();
}
class _HistoryPointScreenState extends State<HistoryPointScreen> {
late final HistoryPointViewModel _viewModel = Get.put(HistoryPointViewModel());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomNavigationBar(title: 'Lịch sử điểm'),
body: Obx(() {
final transactionHistories = _viewModel.historyPoint.value?.historyTransaction ?? [];
final cashHistories = _viewModel.historyPoint.value?.historyCash ?? [];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 8),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [_buildTab('Tích điểm', 0), _buildTab('Tiêu điểm', 1), _buildTab('Cash', 2)],
),
),
),
Expanded(
child: RefreshIndicator(
onRefresh: () => _viewModel.freshData(),
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
if (_viewModel.selectedTabIndex.value == 0)
SliverToBoxAdapter(
child: MonthlyPointsChart(
items: _viewModel.transactionSummary.value?.days ?? [],
date: _viewModel.selectedDate,
onPrevMonth: () => _viewModel.changeDate(true),
onNextMonth: () => _viewModel.changeDate(false),
onChangeDate: _showDatePicker,
),
),
if (_viewModel.selectedTabIndex.value != 0)
_buildHeaderDate(),
if (_viewModel.selectedTabIndex.value != 2)
_buildTransactionHistoryList(transactionHistories),
if (_viewModel.selectedTabIndex.value == 2)
_buildCashHistoryList(cashHistories),
SliverToBoxAdapter(child: SizedBox(height: 32)),
],
),
),
),
],
);
}),
);
}
SliverToBoxAdapter _buildHeaderDate() {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
visualDensity: VisualDensity.compact,
onPressed: () => _viewModel.changeDate(true),
icon: Icon(Icons.chevron_left, color: Colors.blue[900]),
),
GestureDetector(
onTap: _showDatePicker,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Text(
'Tháng ${_viewModel.selectedDate.month}/${_viewModel.selectedDate.year}',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15, color: Colors.blue[900]),
),
),
),
IconButton(
visualDensity: VisualDensity.compact,
onPressed: () => _viewModel.changeDate(false),
icon: Icon(Icons.chevron_right, color: Colors.blue[900]),
),
],
),
),
);
}
_showDatePicker() {
showMonthPicker(context: context, initialDate: _viewModel.selectedDate, lastDate: DateTime.now()).then((date) {
if (date == null) return;
_viewModel.selectedDate = date;
_viewModel.freshData();
});
}
Widget _buildTransactionHistoryList(List<TransactionHistoryModel> transactionHistories) {
if (transactionHistories.isEmpty) {
return SliverFillRemaining(hasScrollBody: false, child: Center(child: EmptyWidget()));
} else {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return _buildTransactionHistoryItem(transactionHistories[index]);
}, childCount: transactionHistories.length),
);
}
}
Widget _buildCashHistoryList(List<CashHistoryModel> cashHistories) {
if (cashHistories.isEmpty) {
return SliverFillRemaining(hasScrollBody: false, child: Center(child: EmptyWidget()));
} else {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return _buildCashHistoryItem(cashHistories[index]);
}, childCount: cashHistories.length),
);
}
}
Widget _buildCashHistoryItem(CashHistoryModel item) {
final dateText = item.transactionDatetime ?? '';
return InkWell(
onTap: () => {},
child: Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
dateText,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
),
),
Text(
(item.total?.toInt() ?? 0).money(CurrencyUnit.noneSpace),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(width: 4),
Image.asset('assets/images/ic_point.png', width: 18, height: 18),
],
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Text(
item.cashTitle.orIfBlank('Giao dịch'),
style: const TextStyle(fontSize: 14, color: Colors.black54),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Text(
item.status.title,
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: item.status.color),
),
],
),
const SizedBox(height: 8),
const Divider(height: 1, color: Colors.black12),
],
),
),
);
}
Widget _buildTransactionHistoryItem(TransactionHistoryModel item) {
final title = item.transactionTagDescription.orIfBlank('Giao dịch');
final brand = item.brandName.orIfBlank("MyPoint");
final redeemTotal = item.redeemTotal?.toInt() ?? 0;
final rewardTotal = item.rewardTotal?.toInt() ?? 0;
final adjustTotal = item.adjustTotal?.toInt() ?? 0;
final value = rewardTotal - redeemTotal + adjustTotal;
final valueColor = value >= 0 ? const Color(0xFF21C777) : const Color(0xFFFE515A);
final valueText = '${value > 0 ? '+' : (value < 0 ? '-' : '')}${value.toInt()}';
final dateText = item.transactionDatetime?.toDate()?.toFormattedString(format: DateFormat.viFull);
final transactionId = item.transactionSequenceId.orIfBlank('');
return InkWell(
onTap: () => {},
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.blue[900]),
),
const SizedBox(height: 6),
Row(
children: [
Row(
children: [
SizedBox(
width: 24,
height: 24,
child: ClipOval(
child: Image.network(
item.brandLogo ?? '',
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => Image.asset('assets/images/ic_logo.png'),
),
),
),
const SizedBox(width: 8),
Text(brand, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.blue[900])),
],
),
const Spacer(),
Row(
children: [
Text(valueText, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: valueColor)),
const SizedBox(width: 4),
Image.asset('assets/images/ic_point.png', width: 24, height: 24),
],
),
],
),
const SizedBox(height: 6),
Text(dateText ?? '', style: const TextStyle(fontSize: 13, color: Colors.black87)),
const SizedBox(height: 8),
if (transactionId.isNotEmpty)
Container(
margin: const EdgeInsets.only(bottom: 8),
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.max, // ⬅️ fill ngang
crossAxisAlignment: CrossAxisAlignment.center,
children: [
InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () {
Clipboard.setData(ClipboardData(text: transactionId));
showToastMessage('Đã sao chép mã giao dịch');
},
child: const Padding(
padding: EdgeInsets.all(4),
child: Icon(Icons.copy, size: 16, color: Color(0xFFFF3D00)),
),
),
const SizedBox(width: 4),
Expanded(
child: SelectableText(transactionId, style: const TextStyle(fontSize: 13, color: Colors.black87)),
),
],
),
),
const Divider(height: 1, color: Colors.black12),
],
),
),
);
}
Widget _buildTab(String title, int index) {
final width = MediaQuery.of(context).size.width-3;
return GestureDetector(
onTap: () {
if (_viewModel.selectedTabIndex.value == index) return;
_viewModel.selectedTabIndex.value = index;
_viewModel.freshData();
},
child: Obx(() {
final selected = _viewModel.selectedTabIndex.value == index;
return SizedBox(
width: width / 3,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: selected ? Colors.red : Colors.black54,
),
),
const SizedBox(height: 4),
// if (selected) Container(height: 2, width: 60, color: Colors.red),
AnimatedContainer(
duration: const Duration(milliseconds: 180),
height: 2,
width: selected ? 60 : 0,
color: selected ? Colors.red : Colors.transparent,
),
],
),
);
}),
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../networking/restful_api_viewmodel.dart';
import 'models/history_point_models.dart';
import 'models/transaction_summary_by_date_model.dart';
class HistoryPointViewModel extends RestfulApiViewModel {
var historyPoint = Rxn<ListHistoryResponseModel>();
var transactionSummary = Rxn<TransactionSummaryByDateModel>();
final RxInt selectedTabIndex = 0.obs;
DateTime selectedDate = DateTime.now();
@override
onInit() {
super.onInit();
freshData();
}
changeDate(bool prevMonth) {
selectedDate = DateTime(selectedDate.year, selectedDate.month + (prevMonth ? -1 : 1), 1);
freshData();
}
Future<void> freshData() async {
showLoading();
try {
await Future.wait<void>([
_getTransactionGetSummaryByDate(),
_getTransactionSummaryByDateModel(),
], eagerError: false);
} finally {
hideLoading();
}
}
Future<void> _getTransactionGetSummaryByDate() async {
final body = {
'month': selectedDate.month,
'year': selectedDate.year,
'lang': 'vi',
};
final res = await client.transactionGetSummaryByDate(body);
transactionSummary.value = res.data;
}
Future<void> _getTransactionSummaryByDateModel() async {
historyPoint.value = null;
final body = {
'transaction_happened_in_year': selectedDate.year,
'transaction_happened_in_month': selectedDate.month,
'transaction_types': selectedTabIndex.value == 1 ? 'RD,AD': (selectedTabIndex.value == 0 ? 'RW,AD' : ''),
'limit': 1000,
'start': 0,
'lang': 'vi',
};
final res = await client.transactionHistoryGetList(body);
historyPoint.value = res.data;
}
}
\ No newline at end of file
import 'dart:ui';
class CashHistoryModel {
final String? cashTitle;
final String? orderStatus;
final String? total;
final String? transactionDatetime;
CashOrderStatus get status => CashOrderStatus.fromRaw(orderStatus ?? '0');
CashHistoryModel({
this.cashTitle,
this.orderStatus,
this.total,
this.transactionDatetime,
});
factory CashHistoryModel.fromJson(Map<String, dynamic> json) {
return CashHistoryModel(
cashTitle: json['cash_title'] as String?,
orderStatus: json['order_status'] as String?,
total: json['total'] as String?,
transactionDatetime: json['transaction_datetime'] as String?,
);
}
Map<String, dynamic> toJson() => {
'cash_title': cashTitle,
'order_status': orderStatus,
'total': total,
'transaction_datetime': transactionDatetime,
};
}
enum CashOrderStatus {
processing, success, rejected;
static CashOrderStatus fromRaw(String raw, {CashOrderStatus fallback = CashOrderStatus.processing}) {
final i = int.tryParse(raw.trim());
return (i != null && i >= 0 && i < CashOrderStatus.values.length)
? CashOrderStatus.values[i]
: fallback;
}
String get title => switch (this) {
CashOrderStatus.processing => 'Đang chờ',
CashOrderStatus.success => 'Thành công',
CashOrderStatus.rejected => 'Đã hủy',
};
Color get color => switch (this) {
CashOrderStatus.processing => const Color(0xFFFF7527), // h_FF7527
CashOrderStatus.success => const Color(0xFF04AF5D), // h_04AF5D
CashOrderStatus.rejected => const Color(0xFFD42230), // h_D42230
};
}
import 'dart:ui';
import 'cash_history_model.dart';
import 'package:mypoint_flutter_app/screen/history_point/models/transaction_history_model.dart';
class ListHistoryResponseModel {
final List<TransactionHistoryModel>? historyTransaction;
final List<CashHistoryModel>? historyCash;
ListHistoryResponseModel({this.historyTransaction, this.historyCash});
factory ListHistoryResponseModel.fromJson(Map<String, dynamic> json) {
return ListHistoryResponseModel(
historyTransaction: (json['history'] as List<dynamic>?)
?.map((e) => TransactionHistoryModel.fromJson(e as Map<String, dynamic>))
.toList(),
historyCash: (json['history_cash'] as List<dynamic>?)
?.map((e) => CashHistoryModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() => {
'history': historyTransaction?.map((e) => e.toJson()).toList(),
'history_cash': historyCash?.map((e) => e.toJson()).toList(),
};
}
enum HistoryPointStatus { processing, success, rejected }
extension CashStatusX on HistoryPointStatus {
String get title {
switch (this) {
case HistoryPointStatus.processing:
return 'Đang chờ';
case HistoryPointStatus.success:
return 'Thành công';
case HistoryPointStatus.rejected:
return 'Đã hủy';
}
}
Color get color {
switch (this) {
case HistoryPointStatus.processing:
return _hexColor('#FF7527');
case HistoryPointStatus.success:
return _hexColor('#04AF5D');
case HistoryPointStatus.rejected:
return _hexColor('#D42230');
}
}
Color _hexColor(String hex) {
var c = hex.replaceAll('#', '').trim();
if (c.length == 6) c = 'FF$c';
final v = int.tryParse(c, radix: 16) ?? 0xFF000000;
return Color(v);
}
}
import '../../../configs/callbacks.dart';
class TransactionHistoryModel {
final String? poolCode;
final String? transactionTag;
final String? transactionTagDescription;
final String? brandName;
final String? invoiceNumber;
final String? currencyCode;
final String? redeemTotal;
final String? transactionType;
final String? brandId;
final String? brandCode;
final String? poolId;
final String? transactionDatetime;
final String? transactionSequenceId;
final String? brandLogo;
final String? rewardTotal;
final String? adjustTotal;
const TransactionHistoryModel({
this.poolCode,
this.transactionTag,
this.transactionTagDescription,
this.brandName,
this.invoiceNumber,
this.currencyCode,
this.redeemTotal,
this.transactionType,
this.brandId,
this.brandCode,
this.poolId,
this.transactionDatetime,
this.transactionSequenceId,
this.brandLogo,
this.rewardTotal,
this.adjustTotal,
});
factory TransactionHistoryModel.fromJson(Json json) {
return TransactionHistoryModel(
poolCode: json['pool_code'] as String?,
transactionTag: json['transaction_tag'] as String?,
transactionTagDescription: json['transaction_tag_description'] as String?,
brandName: json['brand_name'] as String?,
invoiceNumber: json['invoice_number'] as String?,
currencyCode: json['currency_code'] as String?,
redeemTotal: json['redeem_total'] as String?,
transactionType: json['transaction_type'] as String?,
brandId: json['brand_id'] as String?,
brandCode: json['brand_code'] as String?,
poolId: json['pool_id'] as String?,
transactionDatetime: json['transaction_datetime'] as String?,
transactionSequenceId: json['transaction_sequence_id'] as String?,
brandLogo: json['brand_logo'] as String?,
rewardTotal: json['reward_total'] as String?,
adjustTotal: json['adjust_total'] as String?,
);
}
Json toJson() => {
'pool_code': poolCode,
'transaction_tag': transactionTag,
'transaction_tag_description': transactionTagDescription,
'brand_name': brandName,
'invoice_number': invoiceNumber,
'currency_code': currencyCode,
'redeem_total': redeemTotal,
'transaction_type': transactionType,
'brand_id': brandId,
'brand_code': brandCode,
'pool_id': poolId,
'transaction_datetime': transactionDatetime,
'transaction_sequence_id': transactionSequenceId,
'brand_logo': brandLogo,
'reward_total': rewardTotal,
'adjust_total': adjustTotal,
};
}
import '../../../configs/callbacks.dart';
class TransactionSummaryByDateModel {
final MonthSummaryChartModel month;
final List<DaySummaryChartModel> days;
const TransactionSummaryByDateModel({
required this.month,
required this.days,
});
factory TransactionSummaryByDateModel.fromJson(Json json) {
return TransactionSummaryByDateModel(
month: MonthSummaryChartModel.fromJson((json['month'] as Json?) ?? const {}),
days: ((json['days'] as List?) ?? const [])
.map((e) => DaySummaryChartModel.fromJson(e as Json))
.toList(),
);
}
Map<String, dynamic> toJson() => {
'month': month.toJson(),
'days': days.map((e) => e.toJson()).toList(),
};
}
class DaySummaryChartModel {
final String? summaryDate;
final String? adjustDayTotal;
final String? redeemDayTotal;
final String? rewardDayTotal;
const DaySummaryChartModel({
this.summaryDate,
this.adjustDayTotal,
this.redeemDayTotal,
this.rewardDayTotal,
});
factory DaySummaryChartModel.fromJson(Json json) {
return DaySummaryChartModel(
summaryDate: json['summary_date'] as String?,
adjustDayTotal: json['adjust_day_total'] as String?,
redeemDayTotal: json['redeem_day_total'] as String?,
rewardDayTotal: json['reward_day_total'] as String?,
);
}
Map<String, dynamic> toJson() => {
'summary_date': summaryDate,
'adjust_day_total': adjustDayTotal,
'redeem_day_total': redeemDayTotal,
'reward_day_total': rewardDayTotal,
};
}
class MonthSummaryChartModel {
final String? adjustMonthTotal;
final String? redeemMonthTotal;
final String? rewardMonthTotal;
const MonthSummaryChartModel({
this.adjustMonthTotal,
this.redeemMonthTotal,
this.rewardMonthTotal,
});
factory MonthSummaryChartModel.fromJson(Json json) {
return MonthSummaryChartModel(
adjustMonthTotal: json['adjust_month_total'] as String?,
redeemMonthTotal: json['redeem_month_total'] as String?,
rewardMonthTotal: json['reward_month_total'] as String?,
);
}
Map<String, dynamic> toJson() => {
'adjust_month_total': adjustMonthTotal,
'redeem_month_total': redeemMonthTotal,
'reward_month_total': rewardMonthTotal,
};
}
\ No newline at end of file
import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/extensions/collection_extension.dart'; import 'package:mypoint_flutter_app/extensions/collection_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import 'models/history_point_cashback_model.dart'; import 'models/history_point_cashback_model.dart';
class HistoryPointCashBackViewModel extends RestfulApiViewModel { class HistoryPointCashBackViewModel extends RestfulApiViewModel {
......
...@@ -148,16 +148,14 @@ class HomeGreetingHeader extends StatelessWidget { ...@@ -148,16 +148,14 @@ class HomeGreetingHeader extends StatelessWidget {
} }
_onPointTap() { _onPointTap() {
print("_onPointTap"); Get.toNamed(historyPointScreen);
} }
_onMyVoucherTap() { _onMyVoucherTap() {
Get.toNamed(myVoucherListScreen); Get.toNamed(myVoucherListScreen);
print("_onMyVoucherTap");
} }
_onRankTap() { _onRankTap() {
Get.toNamed(membershipScreen); Get.toNamed(membershipScreen);
print("_onRankTap");
} }
} }
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:mypoint_flutter_app/screen/home/custom_widget/scrollable_header.dart';
import '../../../shared/router_gage.dart';
import '../../voucher/sub_widget/voucher_section_title.dart';
import '../home_tab_viewmodel.dart';
import '../models/achievement_model.dart';
import '../models/main_service_model.dart';
import 'achievement_carousel_widget.dart';
import 'banner_carousel_widget.dart';
import 'main_service_grid_widget.dart';
class HomeScreenWithHeader extends StatefulWidget {
const HomeScreenWithHeader({super.key});
@override
State<HomeScreenWithHeader> createState() => _HomeScreenWithHeaderState();
}
class _HomeScreenWithHeaderState extends State<HomeScreenWithHeader> {
String backgroundImage = 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800';
final HomeTabViewModel _viewModel = Get.put(HomeTabViewModel());
bool _showHover = true;
List<MainServiceModel> _services = [];
List<AchievementModel> _achievements = [];
@override
void initState() {
super.initState();
loadMainServicesFromAsset().then((list) {
setState(() => _services = list);
});
loadMainAchievementsFromAsset().then((list) {
setState(() => _achievements = list);
});
}
// Sample data
final String userName = 'Khánh';
final int coinCount = 100;
final int messageCount = 8;
final String userRank = 'Hạng Đồng';
void _changeBackground() {
final List<String> backgrounds = [
'https://images.unsplash.com/photo-1557683316-973673baf926?w=800',
'https://images.unsplash.com/photo-1579952363873-27d3bfad9c0d?w=800',
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800',
'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800',
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800',
];
setState(() {
backgroundImage = backgrounds[(backgrounds.indexOf(backgroundImage) + 1) % backgrounds.length];
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false, // Cho phép content hiển thị dưới status bar
child: CustomScrollView(
physics: BouncingScrollPhysics(), // Hiệu ứng bounce khi scroll
slivers: [
// Scrollable Header
ScrollableHeader(
backgroundImageUrl: backgroundImage,
userName: userName,
coinCount: coinCount,
messageCount: messageCount,
userRank: userRank,
onSearchTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Search tapped')));
},
onNotificationTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Notification tapped')));
},
onCoinTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Coin: $coinCount')));
},
onMessageTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Messages: $messageCount')));
},
onRankTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Rank: $userRank')));
},
),
// Content area
SliverToBoxAdapter(
child: Container(
color: Colors.white,
child: Column(
children: [
BannerCarousel(banners: [],),
if (_services.isNotEmpty)
MainServiceGrid(
services: _services,
onTap: (item) {
print("Tapped: ${item.serviceName}");
},
),
if (_achievements.isNotEmpty)
HeaderSectionTitle(
title: 'Sự kiện MyPoint',
onViewAll: () {
Get.toNamed(vouchersScreen, arguments: {"isHotProduct": true});
},
),
if (_achievements.isNotEmpty)
AchievementCarousel(
items: _achievements,
onTap: (item) {
// xử lý khi nhấn vào card
},
),
// Sample content
...List.generate(20, (index) => _buildContentCard(index)),
// Bottom padding
SizedBox(height: 20),
],
),
),
),
],
),
),
);
}
Widget _buildContentCard(int index) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: Offset(0, 2))],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(color: Colors.red[400]!.withOpacity(0.1), borderRadius: BorderRadius.circular(8)),
child: Icon(Icons.card_giftcard, color: Colors.red[400]),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Nội dung ${index + 1}',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
),
SizedBox(height: 4),
Text(
'Mô tả chi tiết cho nội dung số ${index + 1}',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16),
],
),
);
}
Future<List<MainServiceModel>> loadMainServicesFromAsset() async {
final jsonStr = await rootBundle.loadString('assets/data/main_services.json');
final json = jsonDecode(jsonStr);
final List list = json['data'];
return list.map((e) => MainServiceModel.fromJson(e)).toList();
}
Future<List<AchievementModel>> loadMainAchievementsFromAsset() async {
final jsonStr = await rootBundle.loadString('assets/data/main_achievements.json');
final json = jsonDecode(jsonStr);
final List list = json['data'];
return list.map((e) => AchievementModel.fromJson(e)).toList();
}
}
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart'; import 'package:mypoint_flutter_app/widgets/image_loader.dart';
......
import 'package:flutter/material.dart';
class ScrollableHeader extends StatelessWidget {
final String backgroundImageUrl;
final String userName;
final int coinCount;
final int messageCount;
final String userRank;
final VoidCallback? onSearchTap;
final VoidCallback? onNotificationTap;
final VoidCallback? onCoinTap;
final VoidCallback? onMessageTap;
final VoidCallback? onRankTap;
const ScrollableHeader({
super.key,
required this.backgroundImageUrl,
required this.userName,
this.coinCount = 0,
this.messageCount = 0,
this.userRank = 'Hạng Đồng',
this.onSearchTap,
this.onNotificationTap,
this.onCoinTap,
this.onMessageTap,
this.onRankTap,
});
@override
Widget build(BuildContext context) {
return SliverAppBar(
expandedHeight: 200.0,
floating: false,
pinned: false,
elevation: 0,
backgroundColor: Colors.transparent,
flexibleSpace: FlexibleSpaceBar(
background: Stack(
children: [
// Background Image
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: backgroundImageUrl.startsWith('http')
? NetworkImage(backgroundImageUrl)
: AssetImage(backgroundImageUrl) as ImageProvider,
fit: BoxFit.cover,
),
),
),
// Gradient overlay
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.3),
Colors.transparent,
],
),
),
),
// MyPoint Logo
Positioned(
top: 60,
left: 20,
child: Row(
children: [
Text(
'mypoint',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Decorative circles
Positioned(
top: 40,
right: 50,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.1),
),
),
),
Positioned(
top: 80,
right: 20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.15),
),
),
),
],
),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(80),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Greeting and actions row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Xin chào $userName!',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Row(
children: [
GestureDetector(
onTap: onSearchTap,
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Icon(
Icons.search,
color: Colors.grey[600],
size: 20,
),
),
),
SizedBox(width: 12),
GestureDetector(
onTap: onNotificationTap,
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
shape: BoxShape.circle,
),
child: Stack(
children: [
Icon(
Icons.notifications_outlined,
color: Colors.grey[600],
size: 20,
),
if (messageCount > 0)
Positioned(
right: 0,
top: 0,
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: BoxConstraints(
minWidth: 12,
minHeight: 12,
),
child: Text(
messageCount > 99 ? '99+' : messageCount.toString(),
style: TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
),
],
),
],
),
SizedBox(height: 16),
// Stats row
Row(
children: [
_buildStatItem(
icon: Icons.monetization_on_outlined,
value: coinCount.toString(),
iconColor: Colors.orange,
onTap: onCoinTap,
),
SizedBox(width: 12),
_buildStatItem(
icon: Icons.mail_outline,
value: messageCount.toString(),
iconColor: Colors.blue,
onTap: onMessageTap,
),
SizedBox(width: 12),
_buildStatItem(
icon: Icons.person_outline,
value: userRank,
iconColor: Colors.green,
onTap: onRankTap,
),
],
),
],
),
),
),
),
);
}
Widget _buildStatItem({
required IconData icon,
required String value,
required Color iconColor,
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: iconColor,
size: 16,
),
),
SizedBox(width: 8),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
],
),
),
);
}
}
\ No newline at end of file
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../../preference/point/header_home_model.dart'; import '../../preference/point/header_home_model.dart';
import 'models/notification_unread_model.dart'; import 'models/notification_unread_model.dart';
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_miniapp/game_miniapp.dart'; import 'package:game_miniapp/game_miniapp.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart'; import 'package:mypoint_flutter_app/screen/home/custom_widget/header_home_widget.dart';
import 'package:mypoint_flutter_app/screen/home/custom_widget/header_home.dart';
import 'package:mypoint_flutter_app/screen/home/custom_widget/product_grid_widget.dart'; import 'package:mypoint_flutter_app/screen/home/custom_widget/product_grid_widget.dart';
import 'package:mypoint_flutter_app/screen/home/pipi_detail_screen.dart'; import 'package:mypoint_flutter_app/screen/pipi/pipi_detail_screen.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart'; import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../../directional/directional_action_type.dart'; import '../../directional/directional_action_type.dart';
import '../../preference/point/header_home_model.dart';
import '../popup_manager/popup_manager_model.dart';
import '../popup_manager/popup_manager_screen.dart';
import '../popup_manager/popup_manager_viewmodel.dart';
import '../popup_manager/popup_runner_helper.dart'; import '../popup_manager/popup_runner_helper.dart';
import '../voucher/sub_widget/voucher_section_title.dart';
import 'custom_widget/achievement_carousel_widget.dart'; import 'custom_widget/achievement_carousel_widget.dart';
import 'custom_widget/affiliate_brand_grid_widget.dart'; import 'custom_widget/affiliate_brand_grid_widget.dart';
import 'custom_widget/banner_carousel_widget.dart'; import 'custom_widget/banner_carousel_widget.dart';
import 'custom_widget/brand_grid_widget.dart'; import 'custom_widget/brand_grid_widget.dart';
import 'custom_widget/flash_sale_carousel_widget.dart'; import 'custom_widget/flash_sale_carousel_widget.dart';
import 'custom_widget/hover_view.dart'; import 'custom_widget/hover_view_widget.dart';
import 'custom_widget/main_service_grid_widget.dart'; import 'custom_widget/main_service_grid_widget.dart';
import 'custom_widget/my_product_carousel_widget.dart'; import 'custom_widget/my_product_carousel_widget.dart';
import 'custom_widget/news_carousel_widget.dart'; import 'custom_widget/news_carousel_widget.dart';
...@@ -81,9 +76,6 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit { ...@@ -81,9 +76,6 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
services: _viewModel.services, services: _viewModel.services,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.topButton), sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.topButton),
onTap: (item) { onTap: (item) {
print(
"item serviceName serviceName ${item.serviceName} ${item.clickActionType} ${item.clickActionParam}",
);
item.directionalScreen?.begin(); item.directionalScreen?.begin();
}, },
), ),
...@@ -105,9 +97,12 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit { ...@@ -105,9 +97,12 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
break; break;
case HeaderSectionType.product: case HeaderSectionType.product:
if (_viewModel.products.isNotEmpty) { if (_viewModel.products.isNotEmpty) {
List<ProductModel> products = _viewModel.products;
final length = products.length;
products = (length.isOdd) ? products.sublist(0, length - 1) : products;
sections.add( sections.add(
ProductGrid( ProductGrid(
products: _viewModel.products, products: products,
sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.product), sectionConfig: _viewModel.getMainSectionConfigModel(HeaderSectionType.product),
onTap: (product) { onTap: (product) {
Get.toNamed(voucherDetailScreen, arguments: product.id); Get.toNamed(voucherDetailScreen, arguments: product.id);
......
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../affiliate/model/affiliate_brand_model.dart'; import '../affiliate/model/affiliate_brand_model.dart';
import '../faqs/faqs_model.dart'; import '../faqs/faqs_model.dart';
import '../voucher/models/product_model.dart'; import '../voucher/models/product_model.dart';
...@@ -45,10 +45,11 @@ class HomeTabViewModel extends RestfulApiViewModel { ...@@ -45,10 +45,11 @@ class HomeTabViewModel extends RestfulApiViewModel {
try { try {
final response = await client.getSectionLayoutHome(); final response = await client.getSectionLayoutHome();
sectionLayouts.value = response.data ?? []; sectionLayouts.value = response.data ?? [];
hideLoading();
} catch (error) { } catch (error) {
sectionLayouts.value = await _loadSectionLayoutHomeFromCache(); sectionLayouts.value = await _loadSectionLayoutHomeFromCache();
} finally {
hideLoading(); hideLoading();
} finally {
if (sectionLayouts.value.isEmpty) { if (sectionLayouts.value.isEmpty) {
sectionLayouts.value = await _loadSectionLayoutHomeFromCache(); sectionLayouts.value = await _loadSectionLayoutHomeFromCache();
} }
......
import 'package:flutter/foundation.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart'; import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart'; import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import '../../voucher/models/my_product_status_type.dart'; import '../../voucher/models/my_product_status_type.dart';
class MyProductModel { class MyProductModel {
...@@ -25,8 +25,9 @@ class MyProductModel { ...@@ -25,8 +25,9 @@ class MyProductModel {
}); });
factory MyProductModel.fromJson(Map<String, dynamic> json) { factory MyProductModel.fromJson(Map<String, dynamic> json) {
try {
return MyProductModel( return MyProductModel(
id: (json['id'] as num).toInt(), id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?, title: json['title'] as String?,
logo: json['logo'] as String?, logo: json['logo'] as String?,
brandName: json['brand_name'] as String?, brandName: json['brand_name'] as String?,
...@@ -35,6 +36,12 @@ class MyProductModel { ...@@ -35,6 +36,12 @@ class MyProductModel {
updatedAt: json['updated_at'] as String?, updatedAt: json['updated_at'] as String?,
rawStatus: json['status'] as int?, rawStatus: json['status'] as int?,
); );
} catch (e) {
if (kDebugMode) {
print('Failed to parse MyProductModel: $e');
}
rethrow;
}
} }
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
...@@ -54,8 +61,15 @@ class MyProductModel { ...@@ -54,8 +61,15 @@ class MyProductModel {
} }
DateTime? get expireDate { DateTime? get expireDate {
if (expireTime == null) return null; if (expireTime == null || expireTime!.isEmpty) return null;
try {
return DateTime.tryParse(expireTime!); return DateTime.tryParse(expireTime!);
} catch (e) {
if (kDebugMode) {
print('Failed to parse expireTime: $expireTime - $e');
}
return null;
}
} }
String get expire { String get expire {
...@@ -65,8 +79,15 @@ class MyProductModel { ...@@ -65,8 +79,15 @@ class MyProductModel {
String get deadline { String get deadline {
if (expireDate == null) return ''; if (expireDate == null) return '';
try {
final formatted = _formatDate(expireDate!); final formatted = _formatDate(expireDate!);
return 'HSD: $formatted'; return 'HSD: $formatted';
} catch (e) {
if (kDebugMode) {
print('Failed to format deadline: $e');
}
return 'HSD: Không xác định';
}
} }
String _formatDate(DateTime date) { String _formatDate(DateTime date) {
......
import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../../configs/constants.dart'; import '../../configs/constants.dart';
import 'models/interested_categories_model.dart'; import 'models/interested_categories_model.dart';
......
...@@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; ...@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/invite_friend_campaign/popup_invite_friend_code.dart'; import 'package:mypoint_flutter_app/screen/invite_friend_campaign/popup_invite_friend_code.dart';
import 'package:mypoint_flutter_app/widgets/custom_toast_message.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../base/base_screen.dart'; import '../../base/base_screen.dart';
...@@ -29,9 +30,9 @@ class _InviteFriendCampaignScreenState extends BaseState<InviteFriendCampaignScr ...@@ -29,9 +30,9 @@ class _InviteFriendCampaignScreenState extends BaseState<InviteFriendCampaignScr
void initState() { void initState() {
super.initState(); super.initState();
fetchContacts(); fetchContacts();
viewModel.onShowAlertError = (message) { viewModel.onShowAlertError = (message, onBack) {
if (message.isNotEmpty) { if (message.isNotEmpty) {
showAlertError(content: message); showAlertError(content: message, onConfirmed: onBack ? () => Get.back() : null);
} }
}; };
viewModel.phoneInviteFriendResponse = (sms, phone) { viewModel.phoneInviteFriendResponse = (sms, phone) {
...@@ -58,7 +59,7 @@ class _InviteFriendCampaignScreenState extends BaseState<InviteFriendCampaignScr ...@@ -58,7 +59,7 @@ class _InviteFriendCampaignScreenState extends BaseState<InviteFriendCampaignScr
return CustomNavigationBar(title: title); return CustomNavigationBar(title: title);
}), }),
), ),
backgroundColor: BaseColor.primary100, backgroundColor: Color(0xFFFDE8EA),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Obx(() { child: Obx(() {
return Column( return Column(
...@@ -216,9 +217,7 @@ class _InviteFriendCampaignScreenState extends BaseState<InviteFriendCampaignScr ...@@ -216,9 +217,7 @@ class _InviteFriendCampaignScreenState extends BaseState<InviteFriendCampaignScr
Clipboard.setData( Clipboard.setData(
ClipboardData(text: viewModel.inviteFriendDetail.value?.inviteCodeDefault ?? ''), ClipboardData(text: viewModel.inviteFriendDetail.value?.inviteCodeDefault ?? ''),
); );
ScaffoldMessenger.of( showToastMessage('Đã sao chép');
context,
).showSnackBar(const SnackBar(content: Text('Đã sao chép'), duration: Duration(seconds: 1)));
}, },
child: SizedBox( child: SizedBox(
width: 40, width: 40,
......
import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/configs/constants.dart'; import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import 'models/invite_friend_campaign_model.dart'; import 'models/invite_friend_campaign_model.dart';
class InviteFriendCampaignViewModel extends RestfulApiViewModel { class InviteFriendCampaignViewModel extends RestfulApiViewModel {
var inviteFriendDetail = Rxn<InviteFriendDetailModel>(); var inviteFriendDetail = Rxn<InviteFriendDetailModel>();
var campaignDetail = Rxn<CampaignInviteFriendDetail>(); var campaignDetail = Rxn<CampaignInviteFriendDetail>();
void Function(String message, bool onBack)? onShowAlertError;
void Function(String message)? onShowAlertError;
void Function(String, String)? phoneInviteFriendResponse; void Function(String, String)? phoneInviteFriendResponse;
loadData() async { loadData() {
showLoading(); _getInviteFriendDetail();
await _getInviteFriendDetail(); _getCampaignInviteFriendDetail();
await _getCampaignInviteFriendDetail();
hideLoading();
} }
Future<void> phoneInviteFriend(String phone) async { Future<void> phoneInviteFriend(String phone) async {
...@@ -27,57 +24,34 @@ class InviteFriendCampaignViewModel extends RestfulApiViewModel { ...@@ -27,57 +24,34 @@ class InviteFriendCampaignViewModel extends RestfulApiViewModel {
if (response.isSuccess && sms.isNotEmpty) { if (response.isSuccess && sms.isNotEmpty) {
phoneInviteFriendResponse?.call(sms, phone); phoneInviteFriendResponse?.call(sms, phone);
} else { } else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError); onShowAlertError?.call(response.errorMessage ?? Constants.commonError, false);
} }
} catch (error) { } catch (error) {
hideLoading(); hideLoading();
onShowAlertError?.call("Error fetching product detail: $error"); onShowAlertError?.call(Constants.commonError, false);
} }
} }
Future<void> _getInviteFriendDetail() async { Future<void> _getInviteFriendDetail() async {
showLoading();
try { try {
final response = await client.getCampaignInviteFriend(); final response = await client.getCampaignInviteFriend();
hideLoading();
inviteFriendDetail.value = response.data; inviteFriendDetail.value = response.data;
if (!response.isSuccess) {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError, true);
}
} catch (error) { } catch (error) {
onShowAlertError?.call("Error fetching product detail: $error"); onShowAlertError?.call(Constants.commonError, true);
} finally {
hideLoading();
} }
} }
Future<void> _getCampaignInviteFriendDetail() async { Future<void> _getCampaignInviteFriendDetail() async {
try { try {
// final response = await client.getDetailCampaignInviteFriend(); final response = await client.getDetailCampaignInviteFriend();
final item1 = CampaignInviteFriendItemModel( campaignDetail.value = response.data;
name: "Mời bạn đăng ký, cả đôi cù nđôi cùngMời bạn đăng ký, nhận gMời bạn đăng ký, nh đôi cùngMời bạn đăng ký, nhận ận quà", } catch (_) {}
description: "Chỉ cần giới thiệMời bạn đăng ký, u MyPoint, cả bạn lẫn bạn Mời bạn đăng ký, bè đều sẽ nhận",
avatarUrl:
"https://api.sandbox.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/5223A5C210D155B8AB447D32888562CD/1732855098",
);
final item2 = CampaignInviteFriendItemModel(
name: "Mời bạn đăng ký, cMời bạn đăng ký,ả đôi cùng nhậMời bạn đăng ký,n quà",
description: "Chỉ cần giới thiệu MyPoint, cảMời bạn đăng ký, bạn lẫn bạn bè Mời bạn đăng ký,đều sẽ nhận",
avatarUrl:
"https://api.sandbox.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/5223A5C210D155B8AB447D32888562CD/1732855098",
);
final item3 = CampaignInviteFriendItemModel(
name: "Mời bạn đăng ký, cả Mời bạn đăng ký, Mời bạn đăng ký, đôi cùng nhận quà",
description: "Chỉ cần giới thiệMời bạn đăng ký, u MyPoint, cả bMời bạn đăng ký, ạn lẫn bạn bè Mời bạn đăng ký,đều sẽMời bạn đăng ký, nhận",
avatarUrl:
"https://api.sandbox.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/5223A5C210D155B8AB447D32888562CD/1732855098",
);
final item4 = CampaignInviteFriendItemModel(
name: "Mời bạn đăng ký, cả Mời bạn đăng ký, đôi cùng nhận quà",
description: "Chỉ cần giới thiệu MyPoiMời bạn đăng ký, nt, cả bạn lẫn bạn bè đMời bạn đăng ký, ều sẽ nhận",
avatarUrl:
"https://api.sandbox.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F603E24F2D46C44ADCEE955FF57A53CE/1732854874",
);
final value = CampaignInviteFriendDetail(
title: "Chương trình khuyến mãi mời bạn bè",
campaigns: [item1, item2, item3, item4],
);
campaignDetail.value = value;
} catch (error) {
onShowAlertError?.call("Error fetching product detail: $error");
}
} }
} }
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment