Commit efb4662c authored by DatHV's avatar DatHV
Browse files

update campaign 7day

parent 4c376d38
......@@ -179,13 +179,13 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState {
final menuItems = [
{'icon': Icons.monetization_on_outlined, 'title': 'Săn điểm', 'type': 'APP_SCREEN_POINT_HUNTING'},
{'icon': Icons.check_box_outlined, 'title': 'Check-in nhận quà', 'type': 'DAILY_CHECKIN'},
{'icon': Icons.emoji_events_outlined, 'title': 'Bảng xếp hạng', 'type': ''},
{'icon': Icons.emoji_events_outlined, 'title': 'Bảng xếp hạng', 'type': 'APP_SCREEN_LIST_PAYMENT_OF_ELECTRIC'},
{'icon': Icons.gif_box_outlined, 'title': 'Ưu đãi của tôi', 'type': 'APP_SCREEN_MY_PURCHASE_ITEMS'},
{'icon': Icons.receipt_long_outlined, 'title': 'Lịch sử giao dịch', 'sectionDivider': true, 'type': ''},
{'icon': Icons.history_outlined, 'title': 'Lịch sử điểm', 'type': ''},
{'icon': Icons.receipt_long_outlined, 'title': 'Lịch sử giao dịch', 'sectionDivider': true, 'type': 'APP_SCREEN_TRANSACTION_HISTORIES'},
{'icon': Icons.history_outlined, 'title': 'Lịch sử điểm', 'type': 'APP_SCREEN_SURVERY_APP'},
{'icon': Icons.history_outlined, 'title': 'Lịch sử hoàn điểm', 'type': 'APP_SCREEN_REFUND_HISTORY'},
{'icon': Icons.account_balance_wallet_outlined, 'title': 'Quản lý tài khoản/thẻ', 'type': ''},
{'icon': Icons.favorite_border, 'title': 'Yêu thích', 'type': ''},
{'icon': Icons.account_balance_wallet_outlined, 'title': 'Quản lý tài khoản/thẻ', 'type': 'APP_SCREEN_ELECTRIC_BILL'},
{'icon': Icons.favorite_border, 'title': 'Yêu thích', 'type': 'APP_SCREEN_CATEGORY_TAB_FAVORITE'},
{'icon': Icons.shopping_bag_outlined, 'title': 'Đơn mua', 'sectionDivider': true, 'type': 'APP_SCREEN_ORDER_MENU'},
{'icon': Icons.info_outline, 'title': 'Giới thiệu MyPoint', 'sectionDivider': true, 'type': 'VIEW_WEBSITE_PAGE'},
{'icon': Icons.headset_mic_outlined, 'title': 'Hỗ trợ', 'type': 'APP_SCREEN_CUSTOMER_FEEDBACK'},
......
import 'package:flutter/material.dart';
import '../../configs/callbacks.dart';
import '../../widgets/back_button.dart';
class QuizCampaignHeader extends StatelessWidget {
final int currentIndex;
final int total;
final VoidCallback? onBackPressed;
const QuizCampaignHeader({
super.key,
required this.currentIndex,
required this.total,
this.onBackPressed,
});
@override
Widget build(BuildContext context) {
final topSpace = MediaQuery.of(context).padding.top;
return Stack(
children: [
Container(
padding: EdgeInsets.only(top: topSpace, left: 16, right: 16),
color: const Color(0xFFFFF1F3),
child: Column(
children: [
Row(
children: [
CustomBackButton(
onPressed: onBackPressed ?? () => Navigator.of(context).pop(),
),
const SizedBox(width: 12),
Expanded(child: _buildProgressBar(currentIndex, total)),
const SizedBox(width: 24),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: const Text(
'Khảo sát\nnhận thưởng',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: Color(0xFF3B0E0E),
),
),
),
const SizedBox(width: 12),
Image.asset(
'assets/images/ic_header_quiz_survey.png',
height: 180,
fit: BoxFit.contain,
),
],
),
],
),
),
],
);
}
Widget _buildProgressBar(int current, int total) {
return Row(
children: List.generate(
total,
(index) => Expanded(
child: Container(
height: 6,
margin: EdgeInsets.only(left: index == 0 ? 0 : 8),
decoration: BoxDecoration(
color: index <= current ? const Color(0xFFEC4A53) : Colors.white,
borderRadius: BorderRadius.circular(3),
),
),
),
),
);
}
}
import '../../widgets/alert/popup_data_model.dart';
enum SurveyQuestionType {
textarea,
radio,
checkbox;
String get textDes {
switch (this) {
case SurveyQuestionType.textarea:
return 'Nhập câu trả lời dạng text';
case SurveyQuestionType.radio:
return 'Chọn 1 đáp án duy nhất';
case SurveyQuestionType.checkbox:
return 'Chọn nhiều đáp án';
}
}
}
SurveyQuestionType? _parseType(String? raw) {
if (raw == null) return null;
return SurveyQuestionType.values.firstWhere(
(e) => e.name == raw,
orElse: () => SurveyQuestionType.textarea,
);
}
class QuizCampaignSubmitResponseModel {
final PopupDataModel? popup;
QuizCampaignSubmitResponseModel({this.popup});
factory QuizCampaignSubmitResponseModel.fromJson(Map<String, dynamic> json) {
return QuizCampaignSubmitResponseModel(
popup: json['popup'] != null ? PopupDataModel.fromJson(json['popup']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'popup': popup?.toJson(),
};
}
}
class SurveyAnswerCampaignModel {
int? id;
String? value;
String? placeholder;
String? type;
bool? isSelected;
bool? required;
SurveyQuestionType? get qType {
return _parseType(type);
}
SurveyAnswerCampaignModel();
factory SurveyAnswerCampaignModel.fromJson(Map<String, dynamic> json) {
return SurveyAnswerCampaignModel()
..id = json['id']
..value = json['value']
..placeholder = json['placeholder']
..type = json['type']
..isSelected = json['isSelected']
..required = json['required'];
}
Map<String, dynamic> toJson() => {
'id': id,
'value': value,
'placeholder': placeholder,
'type': type,
'isSelected': isSelected,
'required': required,
};
}
class SurveyQuestionCampaignModel {
int id;
bool? required;
String? type;
String? text;
List<SurveyAnswerCampaignModel>? choices;
String? textDesIndex;
String? inputText;
SurveyQuestionCampaignModel({
required this.id,
this.required,
this.type,
this.text,
this.choices,
this.textDesIndex,
this.inputText,
});
bool get answered {
if (qType == SurveyQuestionType.textarea) {
return inputText?.isNotEmpty ?? false;
} else if (qType == SurveyQuestionType.radio || qType == SurveyQuestionType.checkbox) {
return choices?.any((e) => e.isSelected == true) ?? false;
}
return false;
}
SurveyQuestionType? get qType => _parseType(type);
Map<String, dynamic> get submitParamQuestion {
final List<dynamic> answers = [];
if (qType == SurveyQuestionType.textarea) {
answers.add({'text': inputText ?? ''});
} else {
for (var item in choices ?? []) {
answers.add({'text': item.value ?? ''});
}
}
return {
'question_id': id,
'answers': answers,
};
}
factory SurveyQuestionCampaignModel.fromJson(Map<String, dynamic> json) {
return SurveyQuestionCampaignModel(
id: json['id'],
required: json['required'],
type: json['type'],
text: json['text'],
choices: (json['choices'] as List?)
?.map((e) => SurveyAnswerCampaignModel.fromJson(e))
.toList(),
inputText: json['input_text'],
);
}
}
class SurveyCampaignInfoModel {
int id;
String? name;
List<SurveyQuestionCampaignModel>? questions;
SurveyCampaignInfoModel({
required this.id,
this.name,
this.questions,
});
void makeQuestionsTextDesIndex() {
final total = questions?.length ?? 0;
for (var i = 0; i < total; i++) {
final q = questions![i];
final des = 'Câu ${i + 1}/$total: ${q.qType?.textDes ?? ''}';
q.textDesIndex = des;
if (q.qType == SurveyQuestionType.textarea && (q.choices?.isEmpty ?? true)) {
q.choices = [SurveyAnswerCampaignModel()];
}
}
}
Map<String, dynamic> get submitParam {
return {
'quiz_id': id,
'answers': questions?.map((e) => e.submitParamQuestion).toList() ?? [],
};
}
factory SurveyCampaignInfoModel.fromJson(Map<String, dynamic> json) {
return SurveyCampaignInfoModel(
id: json['id'],
name: json['name'],
questions: (json['questions'] as List?)
?.map((e) => SurveyQuestionCampaignModel.fromJson(e))
.toList(),
);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/collection_extension.dart';
import 'package:mypoint_flutter_app/screen/quiz_campaign/quiz_campaign_header.dart';
import 'package:mypoint_flutter_app/screen/quiz_campaign/quiz_campaign_model.dart';
import 'package:mypoint_flutter_app/screen/quiz_campaign/quiz_campaign_viewmodel.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../../widgets/custom_empty_widget.dart';
class SurveyQuestionScreen extends BaseScreen {
const SurveyQuestionScreen({super.key});
@override
State<SurveyQuestionScreen> createState() => _SurveyQuestionScreenState();
}
class _SurveyQuestionScreenState extends BaseState<SurveyQuestionScreen> with BasicState {
late final QuizCampaignViewModel _viewModel;
int currentIndex = 0;
FocusNode? _textFocusNode;
@override
void initState() {
super.initState();
_textFocusNode = FocusNode();
String? quizId;
final args = Get.arguments;
if (args is Map) {
quizId = args['quizId'];
}
if (quizId == null && quizId == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
return;
}
_viewModel = Get.put(QuizCampaignViewModel(quizId: quizId));
_viewModel.onShowAlertError = (message, {shouldQuitScreen = false}) {
if (message.isNotEmpty) {
showAlertError(content: message,
onConfirmed: shouldQuitScreen ? _onQuitScreen : null,
);
}
};
_viewModel.quizCampaignSubmitResponse = (popup) {
showPopup(data: popup);
};
_viewModel.getQuizCampaignDetail();
}
_onQuitScreen() {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
}
@override
void dispose() {
_textFocusNode?.dispose();
super.dispose();
}
@override
Widget createBody() {
final textController = TextEditingController();
final bottomSpace = MediaQuery.of(context).padding.bottom;
return Scaffold(
body: Obx(() {
final questions = _viewModel.surveyData.value?.questions ?? [];
final currentQuestion = questions.safe(currentIndex);
final isLastQuestion = currentIndex == questions.length - 1;
final qType = currentQuestion?.qType;
if (qType == SurveyQuestionType.textarea) {
textController.text = currentQuestion?.inputText ?? '';
}
if (currentQuestion == null) {
return const EmptyWidget();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuizCampaignHeader(
currentIndex: currentIndex,
total: questions.length,
onBackPressed: _showAlertConfirmQuitSurvey,
),
Expanded(
child: QuizCampaignContainer(
child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Câu ${currentIndex + 1}/${questions.length}: ${qType?.textDes ?? ''}',
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.black54),
),
const SizedBox(height: 8),
Text(
currentQuestion.text ?? '',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
if (qType == SurveyQuestionType.textarea)
TextField(
focusNode: _textFocusNode,
controller: textController,
maxLines: 10,
decoration: InputDecoration(
hintText: 'Nhập câu trả lời của bạn',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFFEC4A53), width: 1.5),
),
),
onChanged: (value) {
currentQuestion?.inputText = value;
},
),
...List.generate(currentQuestion.choices?.length ?? 0, (index) {
final choice = currentQuestion.choices![index];
final isSelected = choice.isSelected == true;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: InkWell(
onTap: () {
setState(() {
if (qType == SurveyQuestionType.radio) {
for (final c in currentQuestion.choices!) {
c.isSelected = false;
}
choice.isSelected = true;
} else if (qType == SurveyQuestionType.checkbox) {
choice.isSelected = !(choice.isSelected ?? false);
}
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: isSelected ? BaseColor.primary400 : Colors.grey.shade300,
width: 1.5,
),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
if (qType == SurveyQuestionType.radio)
Radio<String>(
activeColor: BaseColor.primary400,
value: choice.value ?? '',
groupValue:
currentQuestion.choices!.firstWhereOrNull((e) => e.isSelected == true)?.value,
onChanged: (_) {},
),
if (qType == SurveyQuestionType.checkbox)
Checkbox(value: isSelected, onChanged: (_) {}, activeColor: BaseColor.primary400),
const SizedBox(width: 8),
Expanded(child: Text(choice.value ?? '', style: const TextStyle(fontSize: 16))),
],
),
),
),
);
}),
],
),
),
),
),
Padding(
padding: EdgeInsets.only(left: 20, right: 20, bottom: bottomSpace),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
OutlinedButton(
onPressed: currentIndex > 0 ? _prev : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: BaseColor.primary400,
),
child: Text('Trước', style: TextStyle(fontSize: 16)),
),
ElevatedButton(
onPressed: (currentQuestion.required == true && !currentQuestion.answered) ? null : _next,
style: ElevatedButton.styleFrom(
backgroundColor: isLastQuestion ? Colors.redAccent : Colors.white, // Màu nền
foregroundColor: isLastQuestion ? Colors.white : BaseColor.primary400, // Màu chữ/icon
),
child: Text(isLastQuestion ? 'Hoàn thành' : 'Tiếp', style: TextStyle(fontSize: 16)),
),
],
),
),
],
);
}),
backgroundColor: const Color(0xFFFFF1F3),
);
}
_next() {
if (currentIndex < (_viewModel.surveyData.value?.questions?.length ?? 0) - 1) {
setState(() => currentIndex++);
} else {
_textFocusNode?.unfocus();
Future.delayed(Duration(milliseconds: 100), () {
_showAlertConfirmSubmit();
});
}
}
void _prev() {
if (currentIndex > 0) {
setState(() => currentIndex--);
}
}
_showAlertConfirmSubmit() {
final dataAlert = DataAlertModel(
title: "Xác nhận",
description: "Bạn chắc chắn muốn nộp khảo sát? Sau khi gửi, bạn sẽ không thể thay đổi câu trả lời.",
localHeaderImage: "assets/images/ic_pipi_05.png",
buttons: [
AlertButton(
text: "Đồng ý",
onPressed: () {
Get.back();
_viewModel.quizCampaignSubmit();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(text: "Huỷ", onPressed: () => Get.back(), bgColor: Colors.white, textColor: BaseColor.second500),
],
);
showAlert(data: dataAlert);
}
_showAlertConfirmQuitSurvey() {
final dataAlert = DataAlertModel(
description: "Có vẻ bạn chưa hoàn thành nhiệm vụ rồi. Tiếp tục nhé!!",
localHeaderImage: "assets/images/ic_pipi_03.png",
buttons: [
AlertButton(
text: "Thực hiện",
onPressed: () {
Get.back();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
),
AlertButton(
text: "Thoát",
onPressed: () {
Get.back();
Get.back();
},
bgColor: Colors.white,
textColor: BaseColor.second500,
),
],
);
showAlert(data: dataAlert);
}
}
extension FirstWhereOrNullExtension<E> on Iterable<E> {
E? firstWhereOrNull(bool Function(E) test) {
for (var element in this) {
if (test(element)) return element;
}
return null;
}
}
class QuizCampaignContainer extends StatelessWidget {
final Widget child;
const QuizCampaignContainer({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(left: 16, right: 16, bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
// border: Border.all(color: Colors.redAccent.shade100, width: 1),
),
child: child,
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/screen/quiz_campaign/quiz_campaign_model.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../widgets/alert/popup_data_model.dart';
class QuizCampaignViewModel extends RestfulApiViewModel {
var surveyData = Rxn<SurveyCampaignInfoModel>();
String quizId;
void Function(String message, {bool shouldQuitScreen})? onShowAlertError;
void Function(PopupDataModel data)? quizCampaignSubmitResponse;
QuizCampaignViewModel({required this.quizId});
Future<void> getQuizCampaignDetail() async {
showLoading();
try {
final response = await client.getCampaignQuizSurvey(quizId);
hideLoading();
surveyData.value = response.data;
if (surveyData.value == null) {
onShowAlertError?.call(Constants.commonError, shouldQuitScreen: true);
}
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
}
}
Future<void> quizCampaignSubmit() async {
showLoading();
final body = surveyData.value?.submitParam ?? {};
try {
final response = await client.quizSubmitCampaign(quizId, body);
hideLoading();
final popup = response.data?.popup;
if (popup != null) {
quizCampaignSubmitResponse?.call(popup);
} else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
}
}
}
\ No newline at end of file
......@@ -54,7 +54,6 @@ class SplashScreenViewModel extends RestfulApiViewModel {
});
}
}
class EmptyCodable {
EmptyCodable.fromJson(dynamic json);
......
import 'dart:io';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:http/http.dart' as http;
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../widgets/custom_navigation_bar.dart';
import 'dart:typed_data' as typed_data;
class TrafficServiceCertificateScreen extends BaseScreen {
final String urlView;
final String urlDownload;
final String licensePlate;
const TrafficServiceCertificateScreen({
super.key,
required this.urlView,
required this.urlDownload,
required this.licensePlate,
});
@override
State<TrafficServiceCertificateScreen> createState() => _TrafficServiceCertificateScreenState();
}
class _TrafficServiceCertificateScreenState extends BaseState<TrafficServiceCertificateScreen> with BasicState {
late final WebViewController _controller;
@override
void initState() {
super.initState();
showLoading();
_controller = WebViewController()
..loadRequest(Uri.parse(widget.urlView))
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) async {
hideLoading();
},
onWebResourceError: (error) {
hideLoading();
},
),
);
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(title: "Giấy chứng nhận cứu hộ VNTRA"),
body: WebViewWidget(controller: _controller),
bottomNavigationBar: _buildBottomButtonEditMode(),
);
}
Widget _buildBottomButtonEditMode() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 4))],
),
child: SafeArea(
top: false,
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _savePdfToFiles,
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary500,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text(
'Lưu file',
style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
),
);
}
// Future<void> _savePdfToFiles() async {
// final url = widget.urlDownload;
// final licensePlate = widget.licensePlate;
// try {
// final response = await http.get(Uri.parse(url));
// if (response.statusCode == 200) {
// typed_data.Uint8List bytes = response.bodyBytes;
// final fileName = 'MyPoint-Cer-$licensePlate.pdf';
//
// await FileSaver.instance.saveFile(
// name: fileName,
// bytes: bytes,
// ext: 'pdf',
// mimeType: MimeType.pdf,
// );
// } else {
// print("Tải file thất bại");
// }
// } catch (e) {
// print("Lỗi: $e");
// }
// }
Future<void> _savePdfToFiles() async {
final url = widget.urlDownload;
final licensePlate = widget.licensePlate;
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
typed_data.Uint8List bytes = response.bodyBytes;
final dir = await getTemporaryDirectory();
final filePath = '${dir.path}/MyPoint-Cer-$licensePlate.pdf';
final file = File(filePath);
await file.writeAsBytes(bytes);
await Share.shareXFiles([XFile(filePath)], text: 'Giấy chứng nhận cứu hộ');
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tải file thất bại')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Lỗi: $e')),
);
}
}
Future<void> _savePdf() async {
final url = widget.urlDownload;
final licensePlate = widget.licensePlate;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Đang tải PDF...')),
);
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final dir = await getApplicationDocumentsDirectory();
String baseName = 'MyPoint-Cer-$licensePlate.pdf';
String path = '${dir.path}/$baseName';
int count = 0;
while (File(path).existsSync()) {
path = '${dir.path}/MyPoint-Cer-$licensePlate-${count++}.pdf';
}
final file = File(path);
await file.writeAsBytes(response.bodyBytes);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Lưu file thành công:\n${file.path}')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tải file thất bại')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Lỗi: $e')),
);
}
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/traffic_service/traffic_service_certificate_screen.dart';
import 'package:mypoint_flutter_app/screen/traffic_service/traffic_service_model.dart';
import 'package:mypoint_flutter_app/screen/traffic_service/traffic_service_viewmodel.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../resouce/base_color.dart';
import '../../widgets/custom_navigation_bar.dart';
class TrafficServiceDetailScreen extends StatefulWidget {
const TrafficServiceDetailScreen({super.key});
@override
State<TrafficServiceDetailScreen> createState() => _TrafficServiceDetailScreenState();
}
class _TrafficServiceDetailScreenState extends State<TrafficServiceDetailScreen> {
final TrafficServiceViewModel _viewModel = Get.put(TrafficServiceViewModel());
@override
void initState() {
super.initState();
int? serviceId;
TrafficServiceDetailModel? data;
final args = Get.arguments;
if (args is Map) {
serviceId = args['serviceId'];
data = args['data'];
}
if (serviceId == null && data == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.back();
});
return;
}
_viewModel.trafficServiceDetail.value = data;
if (serviceId != null) {
_viewModel.getTrafficServiceDetail(serviceId.toString());
}
// _viewModel.onShowAlertError = (message) {
// if (message.isNotEmpty) {
// showAlertError(content: message);
// }
// };
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomNavigationBar(title: "Thông tin chi tiết"),
body: Obx(() {
final model = _viewModel.trafficServiceDetail.value;
if (model == null) {
return const Center(child: EmptyWidget());
}
return Column(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle("Thông tin xe"),
_rowItem("Biển số xe", model.licensePlate ?? ''),
const Divider(color: Colors.black12),
_sectionTitle("Thông tin chủ xe"),
_rowItem("Họ và tên", model.ownerName ?? ''),
_rowItem("Số điện thoại", model.phoneNumber ?? ''),
const Divider(color: Colors.black12),
_sectionTitle("Thông tin gói dịch vụ"),
_rowItem("Tên gói", model.packageName ?? ''),
_rowItem("Hiệu lực gói dịch vụ", model.dateDes),
GestureDetector(
onTap: () async {
final Uri phoneUri = Uri(scheme: 'tel', path: model.hotline);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
}
},
child: _rowItem("SĐT cứu hộ", model.hotline ?? '', color: Colors.red, icon: Icons.phone),
),
const Divider(color: Colors.black12),
],
),
),
],
);
}),
bottomNavigationBar: _buildBottomButtonEditMode(),
);
}
Widget _sectionTitle(String title) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
);
}
Widget _rowItem(String label, String value, {Color? color, IconData? icon}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Expanded(child: Text(label, style: const TextStyle(color: Colors.black54, fontSize: 14))),
if (icon != null) Icon(icon, color: color ?? Colors.black),
const SizedBox(width: 4),
Text(value, style: TextStyle(color: color ?? Colors.black87, fontSize: 14)),
],
),
);
}
Widget _buildBottomButtonEditMode() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 4))],
),
child: SafeArea(
top: false,
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () {
Get.to(TrafficServiceCertificateScreen(
urlView: _viewModel.trafficServiceDetail.value?.urlView ?? '',
urlDownload: _viewModel.trafficServiceDetail.value?.urlDownload ?? '',
licensePlate: _viewModel.trafficServiceDetail.value?.licensePlate ?? '',
));
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary500,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text(
'Xem giấy chứng nhận',
style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
),
);
}
}
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
enum SortFilter { asc, desc }
class HeaderFilterOrderModel {
String title;
SortFilter? sort;
String? expired;
double rateWidth;
String suffixChecking;
bool? selected;
HeaderFilterOrderModel({
required this.title,
this.sort,
this.expired,
this.rateWidth = 1.0,
required this.suffixChecking,
this.selected,
});
Map<String, dynamic> get params {
if (sort != null) {
return {'order_checkup': sort!.name};
}
return {'expired': expired ?? ''};
}
String get eventNameTracking {
if (sort == null) {
return 'soskdt_${suffixChecking}';
}
return 'soskdt_${suffixChecking}_${sort!.name}';
}
String get eventNameTrackingDvCar => 'dvcar_${suffixChecking}';
}
class TrafficServiceResponseModel {
int? total;
List<TrafficServiceDetailModel>? products;
TrafficServiceResponseModel({this.total, this.products});
factory TrafficServiceResponseModel.fromJson(Map<String, dynamic> json) {
return TrafficServiceResponseModel(
total: json['total'],
products: (json['products'] as List<dynamic>?)
?.map((e) => TrafficServiceDetailModel.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
'total': total,
'products': products?.map((e) => e.toJson()).toList(),
};
}
class TrafficServiceDetailModel {
int? itemId;
String? licensePlate;
String? ownerName;
String? phoneNumber;
String? packageName;
String? hotline;
String? startTime;
String? endTime;
String? updatedAt;
ButtonConfigModel? buyMoreNote;
ActiveTextConfig? active;
String? urlDownload;
String? urlView;
List<ProductMediaItem>? media;
TrafficServiceDetailModel({
this.itemId,
this.licensePlate,
this.ownerName,
this.phoneNumber,
this.packageName,
this.hotline,
this.startTime,
this.endTime,
this.updatedAt,
this.buyMoreNote,
this.active,
this.urlDownload,
this.urlView,
this.media,
});
factory TrafficServiceDetailModel.fromJson(Map<String, dynamic> json) {
return TrafficServiceDetailModel(
itemId: json['item_id'],
licensePlate: json['license_plate'],
ownerName: json['owner_name'],
phoneNumber: json['phone_number'],
packageName: json['package_name'],
hotline: json['hotline'],
startTime: json['start_time'],
endTime: json['end_time'],
updatedAt: json['updated_at'],
buyMoreNote: json['buy_more_note'] != null
? ButtonConfigModel.fromJson(json['buy_more_note'])
: null,
active: json['active'] != null
? ActiveTextConfig.fromJson(json['active'])
: null,
urlDownload: json['url_download'],
urlView: json['url_view'],
media: (json['media'] as List<dynamic>?)
?.map((e) => ProductMediaItem.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() => {
'item_id': itemId,
'license_plate': licensePlate,
'owner_name': ownerName,
'phone_number': phoneNumber,
'package_name': packageName,
'hotline': hotline,
'start_time': startTime,
'end_time': endTime,
'updated_at': updatedAt,
'buy_more_note': buyMoreNote?.toJson(),
'active': active?.toJson(),
'url_download': urlDownload,
'url_view': urlView,
'media': media?.map((e) => e.toJson()).toList(),
};
String get dateDes {
final start = (startTime ?? '').toDate()?.toFormattedString() ?? '';
final end = (endTime ?? '').toDate()?.toFormattedString() ?? '';
return '$start - $end';
}
}
class ActiveTextConfig {
String? text;
String? textColor;
String? bgColor;
ActiveTextConfig({this.text, this.textColor, this.bgColor});
factory ActiveTextConfig.fromJson(Map<String, dynamic> json) {
return ActiveTextConfig(
text: json['text'],
textColor: json['text_color'],
bgColor: json['bg_color'],
);
}
Map<String, dynamic> toJson() => {
'text': text,
'text_color': textColor,
'bg_color': bgColor,
};
}
class ProductMediaItem {
String? name;
String? url;
String? rawType;
ProductMediaItem({this.name, this.url, this.rawType});
factory ProductMediaItem.fromJson(Map<String, dynamic> json) {
return ProductMediaItem(
name: json['name'],
url: json['url'],
rawType: json['type'],
);
}
Map<String, dynamic> toJson() => {
'name': name,
'url': url,
'type': rawType,
};
String? get type => rawType;
}
class ButtonConfigModel {
String? text;
String? action;
ButtonConfigModel({this.text, this.action});
factory ButtonConfigModel.fromJson(Map<String, dynamic> json) {
return ButtonConfigModel(
text: json['text'],
action: json['action'],
);
}
Map<String, dynamic> toJson() => {
'text': text,
'action': action,
};
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/resouce/base_color.dart';
import 'package:mypoint_flutter_app/screen/traffic_service/traffic_service_detail_screen.dart';
import 'package:mypoint_flutter_app/screen/traffic_service/traffic_service_viewmodel.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../extensions/date_format.dart';
import '../../shared/router_gage.dart';
import '../../widgets/custom_navigation_bar.dart';
class TrafficServiceScreen extends StatefulWidget {
const TrafficServiceScreen({super.key});
@override
State<TrafficServiceScreen> createState() => _TrafficServiceScreenState();
}
class _TrafficServiceScreenState extends State<TrafficServiceScreen> {
final TrafficServiceViewModel _viewModel = Get.put(TrafficServiceViewModel());
@override
void initState() {
super.initState();
_viewModel.getTrafficServiceData();
}
@override
Widget build(BuildContext context) {
final tags = _viewModel.headerFilterOrder;
return Scaffold(
appBar: CustomNavigationBar(title: "Dịch vụ giao thông"),
body: Column(
children: [
const SizedBox(height: 8),
Obx(()
=> Row(
mainAxisAlignment: MainAxisAlignment.start,
children: List.generate(tags.length, (index) {
final isSelected = index == _viewModel.selectedIndex.value;
return GestureDetector(
onTap: () {
if (_viewModel.selectedIndex.value == index) return;
setState(() {
_viewModel.selectedIndex.value = index;
_viewModel.getTrafficServiceData();
});
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: isSelected ? BaseColor.primary500 : Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Text(
tags[index].title,
style: TextStyle(color: isSelected ? BaseColor.primary500 : Colors.black87),
),
),
);
}),
),
),
const Divider(height: 14, color: Colors.black12),
const SizedBox(height: 8),
Expanded(
child: Obx(() {
final products = _viewModel.trafficData.value?.products ?? [];
return products.isEmpty
? Center(child: EmptyWidget())
: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final item = products[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
onTap: () {
print('Tapped on item: ${item.licensePlate}');
Get.toNamed(trafficServiceDetailScreen, arguments: {'serviceId': item.itemId});
},
leading: SizedBox(
width: 60, // <= giới hạn rõ
height: 60,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: loadNetworkImage(
url: item.media?.firstOrNull?.url ?? '',
fit: BoxFit.cover,
placeholderAsset: 'assets/images/bg_default_11.png',
),
),
),
title: Text(item.licensePlate ?? '', style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
item.packageName ?? '',
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
(item.endTime ?? '').toDate()?.toFormattedString() ?? '',
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'Cập nhật lúc ${(item.updatedAt ?? '').toDate()?.toFormattedString(format: DateFormat.ddMMyyyyhhmm) ?? ''}',
style: const TextStyle(fontSize: 11, color: Colors.black54),
),
],
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(4),
),
child: Text(
item.active?.text ?? '',
style: const TextStyle(fontSize: 12, color: Colors.green, fontWeight: FontWeight.w500),
),
),
),
),
);
},
);
}),
),
],
),
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/screen/traffic_service/traffic_service_model.dart';
import '../../base/restful_api_viewmodel.dart';
class TrafficServiceViewModel extends RestfulApiViewModel {
var trafficData = Rxn<TrafficServiceResponseModel>();
void Function(String message)? onShowAlertError;
var trafficServiceDetail = Rxn<TrafficServiceDetailModel>();
RxInt selectedIndex = 0.obs;
List<HeaderFilterOrderModel> get headerFilterOrder {
return [
HeaderFilterOrderModel(
title: 'Tất cả',
suffixChecking: 'tatca',
selected: true,
),
HeaderFilterOrderModel(
title: 'Hiệu lực',
suffixChecking: 'hieuluc',
),
HeaderFilterOrderModel(
title: 'Không hiệu lực',
expired: "true",
suffixChecking: 'khonghieuluc',
),
];
}
Future<void> getTrafficServiceData() async {
var body = headerFilterOrder[selectedIndex.value].params;
body['page'] = 1;
body['size'] = 10000;
showLoading();
try {
final response = await client.getProductVnTraSold(body);
hideLoading();
if (response.isSuccess) {
trafficData.value = response.data;
} else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call("Error fetching product detail: $error");
}
}
Future<void> getTrafficServiceDetail(String id) async {
showLoading();
try {
final response = await client.getDetailMyPackageVnTra(id);
hideLoading();
if (response.isSuccess) {
trafficServiceDetail.value = response.data;
} else {
onShowAlertError?.call(response.errorMessage ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call("Error fetching product detail: $error");
}
}
}
\ No newline at end of file
class TransactionCategoryModel {
int? id;
String? code;
String? name;
bool _isSelected = false;
bool get isSelected => _isSelected;
set isSelected(bool value) => _isSelected = value;
TransactionCategoryModel({
this.id,
this.code,
this.name,
bool? isSelected,
}) : _isSelected = isSelected ?? false;
factory TransactionCategoryModel.fromJson(Map<String, dynamic> json) {
return TransactionCategoryModel(
id: json['id'] as int?,
code: json['code'] as String?,
name: json['name'] as String?,
isSelected: json['_isSelected'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'code': code,
'name': name,
'_isSelected': _isSelected,
};
}
}
......@@ -121,9 +121,28 @@ class _TransactionHistoryDetailScreenState extends BaseState<TransactionHistoryD
const SizedBox(height: 8),
const Text("Thanh toán mua ưu đãi", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(
data.payCash?.isNotEmpty == true ? '${data.payCash}' : data.payPoint ?? '0đ',
style: const TextStyle(fontSize: 20, color: Colors.red, fontWeight: FontWeight.bold),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if ((data.payCash ?? '').isNotEmpty)
Text(
'${data.payCash}',
style: const TextStyle(fontSize: 20, color: Colors.red, fontWeight: FontWeight.bold),
),
if ((data.payCash ?? '').isNotEmpty && (data.payPoint ?? '').isNotEmpty) const SizedBox(width: 24),
if ((data.payPoint ?? '').isNotEmpty)
Row(
children: [
Image.asset('assets/images/ic_point.png', width: 24, height: 24),
const SizedBox(width: 4),
Text(
data.payPoint ?? '0',
style: const TextStyle(fontSize: 20, color: Colors.red, fontWeight: FontWeight.bold),
),
],
),
],
),
],
),
......@@ -232,15 +251,17 @@ class _TransactionHistoryDetailScreenState extends BaseState<TransactionHistoryD
minimum: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (transaction.titleRedButton != null)
ElevatedButton(
onPressed: () {
Get.until((route) => Get.currentRoute == mainScreen);
// Navigator.of(context).pop();
final finish = transaction.directionScreenRedButton?.begin();
if (finish != true) {
Get.until((route) => Get.currentRoute == mainScreen);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary600,
......
import 'package:json_annotation/json_annotation.dart';
import 'package:mypoint_flutter_app/screen/transaction/history/transaction_history_emun.dart';
import '../../../directional/directional_action_type.dart';
import '../../../directional/directional_screen.dart';
import '../../voucher/models/product_type.dart';
......@@ -91,27 +92,36 @@ class TransactionHistoryModel {
// String? get iconSupport =>
// statusT == TransactionStatusOrder.failed ? 'ic_support_transaction' : null;
// DirectionalScreen? get directionScreenRedButton {
// switch (statusT) {
// case TransactionStatusOrder.success:
// switch (productType) {
// case ProductType.voucher:
// return DirectionalScreen(clickActionType: 'productOwnVoucher', clickActionParam: itemId);
// case ProductType.topupMobile:
// return DirectionalScreen(clickActionType: 'mobileTopup', clickActionParam: itemId);
// case ProductType.typeCard:
// return DirectionalScreen(clickActionType: 'familyMedonDetailCard', clickActionParam: itemId);
// case ProductType.vnTra:
// return DirectionalScreen(clickActionType: 'detailTrafficService', clickActionParam: itemId);
// default:
// return null;
// }
// case TransactionStatusOrder.failed:
// return DirectionalScreen(clickActionType: 'customerSupport');
// default:
// return DirectionalScreen(clickActionType: 'home');
// }
// }
DirectionalScreen? get directionScreenRedButton {
switch (statusT) {
case TransactionStatusOrder.success:
switch (productType) {
case ProductType.voucher:
return DirectionalScreen.buildByName(
name: DirectionalScreenName.productOwnVoucher,
clickActionParam: itemId,
);
case ProductType.topupMobile:
return DirectionalScreen.buildByName(name: DirectionalScreenName.mobileTopup,);
case ProductType.typeCard:
return DirectionalScreen.buildByName(
name: DirectionalScreenName.familyMedonDetailCard,
clickActionParam: itemId,
);
case ProductType.vnTra:
return DirectionalScreen.buildByName(
name: DirectionalScreenName.detailTrafficService,
clickActionParam: itemId,
);
default:
return null;
}
case TransactionStatusOrder.failed:
return DirectionalScreen.buildByName(name: DirectionalScreenName.customerSupport,);
default:
return null;
}
}
}
class ProductInfoModel {
......
import 'package:mypoint_flutter_app/screen/transaction/history/transaction_history_model.dart';
class TransactionHistoryResponse {
List<TransactionHistoryModel>? items;
TransactionHistorySummaryModel? summary;
TransactionHistoryResponse({this.items, this.summary});
factory TransactionHistoryResponse.fromJson(Map<String, dynamic> json) {
return TransactionHistoryResponse(
items: (json['items'] as List<dynamic>?)
?.map((e) => TransactionHistoryModel.fromJson(e))
.toList(),
summary: json['summary'] != null
? TransactionHistorySummaryModel.fromJson(json['summary'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'items': items?.map((e) => e.toJson()).toList(),
'summary': summary?.toJson(),
};
}
}
class TransactionHistorySummaryModel {
String? total;
String? totalPayCash;
String? totalPayPoint;
int? totalOrder;
TransactionHistorySummaryModel({
this.total,
this.totalPayCash,
this.totalPayPoint,
this.totalOrder,
});
factory TransactionHistorySummaryModel.fromJson(Map<String, dynamic> json) {
return TransactionHistorySummaryModel(
total: json['total'],
totalPayCash: json['total_pay_cash'],
totalPayPoint: json['total_pay_point'],
totalOrder: json['total_order'],
);
}
Map<String, dynamic> toJson() {
return {
'total': total,
'total_pay_cash': totalPayCash,
'total_pay_point': totalPayPoint,
'total_order': totalOrder,
};
}
}
......@@ -95,8 +95,6 @@ class TransactionDetailViewModel extends RestfulApiViewModel {
showAlertBack: true,
callback: (result) {
if (result == PaymentProcess.success) {
print("PaymentProcess.success");
print(data?.id ?? "");
Get.offNamed(
transactionHistoryDetailScreen,
arguments: {"orderId": data?.id ?? "", "canBack": true},
......
import 'package:flutter/material.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/screen/transaction/transactions_history_viewmodel.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../resouce/base_color.dart';
import '../../shared/router_gage.dart';
import '../../widgets/custom_navigation_bar.dart';
import 'history/transaction_history_model.dart';
import 'history/transaction_history_response_model.dart';
class TransactionHistoryScreen extends StatefulWidget {
const TransactionHistoryScreen({super.key});
@override
State<TransactionHistoryScreen> createState() => _TransactionHistoryScreenState();
}
class _TransactionHistoryScreenState extends State<TransactionHistoryScreen> {
final TransactionsHistoryViewModel _viewModel = Get.put(TransactionsHistoryViewModel());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomNavigationBar(title: 'Lịch sử giao dịch'),
body: Obx(() {
final summary = _viewModel.historyResponse.value?.summary;
final items = _viewModel.historyResponse.value?.items ?? [];
return Column(
children: [
_buildCategoryTabs(),
_buildSummaryBox(summary),
const SizedBox(height: 8),
if (items.isEmpty) Expanded(child: Center(child: EmptyWidget(size: Size(200, 200)))),
Expanded(
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return GestureDetector(
onTap: () {
Get.toNamed(
transactionHistoryDetailScreen,
arguments: {"orderId": item.id ?? ""},
);
},
child: _buildTransactionItem(item));
},
),
),
],
);
}),
);
}
Widget _buildCategoryTabs() {
return SizedBox(
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _viewModel.categories.value.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final category = _viewModel.categories.value[index];
final isSelected = category.code == _viewModel.categorySelected?.code;
return GestureDetector(
onTap: () {
setState(() {
_viewModel.categorySelected = category;
_viewModel.getTransactionHistoryResponse();
});
},
child: Chip(
label: Text(
category.name ?? '',
style: TextStyle(color: isSelected ? BaseColor.primary500 : Colors.black54),
),
backgroundColor: isSelected ? Colors.red.shade50 : Colors.grey.shade100,
),
);
},
),
);
}
Widget _buildSummaryBox(TransactionHistorySummaryModel? summary) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Text(
'Tháng ${_viewModel.date.toFormattedString(format: 'MM/yyyy')}',
style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold),
),
const SizedBox(width: 4),
Text(
'(${_viewModel.historyResponse.value?.items?.length ?? 0} giao dịch)',
style: const TextStyle(color: Colors.black54, fontSize: 14),
),
Spacer(),
GestureDetector(
onTap: () async {
// final picked = await showDatePicker(
// context: context,
// initialDatePickerMode: DatePickerMode.year,
// initialDate: _viewModel.date,
// firstDate: DateTime(1900),
// lastDate: DateTime(2100),
// );
// if (picked != null) {
// setState(() {
// _viewModel.date = picked;
// _viewModel.getTransactionHistoryResponse();
// });
// }
showMonthPicker(context: context, initialDate: _viewModel.date, lastDate: DateTime.now()).then((
date,
) {
if (date != null) {
setState(() {
_viewModel.date = date;
_viewModel.getTransactionHistoryResponse();
});
}
});
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
border: Border.all(color: Colors.black26),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.calendar_today_outlined, size: 16),
const SizedBox(width: 2),
Text("Tháng"),
const SizedBox(width: 2),
Icon(Icons.keyboard_arrow_down, size: 20),
],
),
),
),
],
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: BaseColor.primary200),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(summary?.total ?? '').isEmpty ? "0" : summary?.total ?? '',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Row(
children: [
Text('Giao dịch bằng tiền', style: TextStyle(color: Colors.black87)),
const Spacer(),
Text(summary?.totalPayCash ?? "0", style: const TextStyle(color: Colors.black87, fontSize: 14)),
],
),
Row(
children: [
Text('Giao dịch bằng điểm', style: TextStyle(color: Colors.black87)),
const Spacer(),
Image.asset('assets/images/ic_point.png', width: 16, height: 16),
const SizedBox(width: 2),
Text(summary?.totalPayPoint ?? "0", style: const TextStyle(color: Colors.orange, fontSize: 14)),
],
),
],
),
),
],
);
}
Widget _buildTransactionItem(TransactionHistoryModel item) {
return Column(
children: [
ListTile(
leading: loadNetworkImage(
url: item.logo ?? '',
width: 40,
height: 40,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/ic_membership_voucher.png',
),
title: Text(item.name ?? 'Thanh toán mua ưu đãi'),
subtitle: Text(item.createdAt ?? '', style: const TextStyle(fontSize: 13, color: Colors.black54)),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if ((item.payCash ?? '').isNotEmpty) Text(item.payCash ?? '0đ', style: TextStyle(fontSize: 14)),
if ((item.payPoint ?? '').isNotEmpty)
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset('assets/images/ic_point.png', width: 16, height: 16),
const SizedBox(width: 2),
Text(item.payPoint ?? "0", style: const TextStyle(color: Colors.orange, fontSize: 14)),
],
),
],
),
),
Divider(height: 1, color: Colors.grey.shade100, indent: 20, endIndent: 20),
],
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/extensions/datetime_extensions.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'history/transaction_category_model.dart';
import 'history/transaction_history_response_model.dart';
class TransactionsHistoryViewModel extends RestfulApiViewModel {
var categories = RxList<TransactionCategoryModel>();
var historyResponse = Rxn<TransactionHistoryResponse>();
void Function(String message)? onShowAlertError;
TransactionCategoryModel? categorySelected;
DateTime date = DateTime.now();
bool _isLoading = false;
@override
onInit() {
super.onInit();
_getCategories();
}
Future<void> _getCategories() async {
showLoading();
try {
final response = await client.getTransactionHistoryCategories();
categories.value = response.data ?? [];
categorySelected = categories.isNotEmpty ? categories.first : null;
hideLoading();
getTransactionHistoryResponse();
} catch (error) {
hideLoading();
onShowAlertError?.call("Error fetching product detail: $error");
}
}
Future<void> getTransactionHistoryResponse() async {
if (_isLoading) return;
final body = {
'category_code': categorySelected?.code ?? '',
'date': date.toFormattedString(format: 'yyyy-MM'),
'limit': 10000,
'offset':0,
};
_isLoading = true;
showLoading();
try {
final response = await client.getTransactionHistoryResponse(body);
final data = response.data;
historyResponse.value = data;
_isLoading = false;
hideLoading();
} catch (error) {
_isLoading = false;
hideLoading();
onShowAlertError?.call("Error fetching product detail: $error");
}
}
}
......@@ -113,11 +113,13 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
)
.then((value) {
hideLoading();
if (!value.isSuccess) {
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
if (value.isSuccess && (value.data?.id ?? "").isNotEmpty) {
Get.offNamed(
transactionHistoryDetailScreen,
arguments: {"orderId": value.data?.id ?? "", "canBack": false},
);
} else {
// Success -> go to transaction detail screen
onShowAlertError?.call("Redeem success -> go to transaction detail screen");
onShowAlertError?.call(value.errorMessage ?? Constants.commonError);
}
});
}
......
......@@ -21,8 +21,8 @@ class VoucherActionMenu extends StatelessWidget {
children: const [
_ActionItem(icon: "assets/images/ic_topup.png", label: 'Nạp tiền\ndiện thoại', type: DirectionalScreenName.topup,),
_ActionItem(icon: "assets/images/ic_card_code.png", label: 'Đổi mã\nthẻ nạp', type: DirectionalScreenName.productMobileCard,),
_ActionItem(icon: "assets/images/ic_sim_service.png", label: 'Gói cước\nnhà mạng', type: DirectionalScreenName.carrierPackage,),
_ActionItem(icon: "assets/images/ic_topup_data.png", label: 'Ưu đãi\nData', type: DirectionalScreenName.simService,),
_ActionItem(icon: "assets/images/ic_sim_service.png", label: 'Gói cước\nnhà mạng', type: DirectionalScreenName.simService,),
_ActionItem(icon: "assets/images/ic_topup_data.png", label: 'Ưu đãi\nData', type: DirectionalScreenName.mobileTopupData,),
],
),
);
......@@ -43,10 +43,8 @@ class _ActionItem extends StatelessWidget {
return GestureDetector(
onTap: () {
final param = type == DirectionalScreenName.carrierPackage ? "https://mypoint.uudaigoicuoc.com/" : null;
DirectionalScreen? screen = DirectionalScreen.build(
clickActionType: type.rawValue,
clickActionParam: param,
);
screen?.begin();
},
......
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