Commit c285d072 authored by DatHV's avatar DatHV
Browse files

update game center

parent 8d264762
...@@ -31,4 +31,5 @@ class APIPaths { ...@@ -31,4 +31,5 @@ class APIPaths {
static const String getProductStores = "/product/api/v2.0/product/stores"; static const String getProductStores = "/product/api/v2.0/product/stores";
static const String productCustomerLikes = "/product/api/v2.0/customer/likes"; static const String productCustomerLikes = "/product/api/v2.0/customer/likes";
static const String productCustomerUnlikes = "/product/api/v2.0/customer/likes/%@"; static const String productCustomerUnlikes = "/product/api/v2.0/customer/likes/%@";
static const String getGames = "/campaign/api/v3.0/games";
} }
\ No newline at end of file
import 'package:json_annotation/json_annotation.dart';
import 'directional_screen.dart';
part 'button_config_model.g.dart';
@JsonSerializable()
class ButtonConfigModel {
final String? text;
final String? color;
@JsonKey(name: "click_action_type")
final String? clickActionType;
@JsonKey(name: "click_action_param")
final String? clickActionParam;
final bool? hiden;
ButtonConfigModel({
this.text,
this.color,
this.clickActionType,
this.clickActionParam,
this.hiden,
});
factory ButtonConfigModel.fromJson(Map<String, dynamic> json) => _$ButtonConfigModelFromJson(json);
Map<String, dynamic> toJson() => _$ButtonConfigModelToJson(this);
DirectionalScreen? get directionScreen {
if (clickActionType != null || clickActionParam != null) {
return DirectionalScreen(
clickActionType: clickActionType,
clickActionParam: clickActionParam,
);
}
return null;
}
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'button_config_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ButtonConfigModel _$ButtonConfigModelFromJson(Map<String, dynamic> json) =>
ButtonConfigModel(
text: json['text'] as String?,
color: json['color'] as String?,
clickActionType: json['click_action_type'] as String?,
clickActionParam: json['click_action_param'] as String?,
hiden: json['hiden'] as bool?,
);
Map<String, dynamic> _$ButtonConfigModelToJson(ButtonConfigModel instance) =>
<String, dynamic>{
'text': instance.text,
'color': instance.color,
'click_action_type': instance.clickActionType,
'click_action_param': instance.clickActionParam,
'hiden': instance.hiden,
};
...@@ -42,4 +42,14 @@ extension StringDateExtension on String { ...@@ -42,4 +42,14 @@ extension StringDateExtension on String {
return null; return null;
} }
} }
DateTime? toDateFormat(String format) {
if (trim().isEmpty) return null;
try {
return intl.DateFormat(format).parseStrict(this);
} catch (e) {
print('❌ Date parse failed for "$this" with format "$format": $e');
return null;
}
}
} }
...@@ -5,6 +5,7 @@ import 'package:mypoint_flutter_app/configs/constants.dart'; ...@@ -5,6 +5,7 @@ import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart'; import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api.dart'; import 'package:mypoint_flutter_app/networking/restful_api.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart'; import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/screen/game/models/game_bundle_response.dart';
import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart'; import 'package:mypoint_flutter_app/screen/voucher/models/product_model.dart';
import '../configs/device_info.dart'; import '../configs/device_info.dart';
import '../model/auth/biometric_register_response_model.dart'; import '../model/auth/biometric_register_response_model.dart';
...@@ -314,4 +315,10 @@ extension RestfullAPIClientAllApi on RestfulAPIClient { ...@@ -314,4 +315,10 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
(data) => EmptyCodable.fromJson(data as Json), (data) => EmptyCodable.fromJson(data as Json),
); );
} }
Future<BaseResponseModel<GameBundleResponse>> getGames() async {
return requestNormal(APIPaths.getGames, Method.GET, {}, (data) {
return GameBundleResponse.fromJson(data as Json);
});
}
} }
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import '../../../widgets/back_button.dart';
import '../models/game_bundle_item_model.dart';
import '../models/game_card_item_model.dart';
class GameCardScreen extends StatefulWidget {
const GameCardScreen({super.key});
@override
State<GameCardScreen> createState() => _GameCardScreenState();
}
class _GameCardScreenState extends State<GameCardScreen> {
late final GameBundleItemModel data;
@override
void initState() {
super.initState();
final args = Get.arguments;
if (args is GameBundleItemModel) {
data = args;
}
}
@override
Widget build(BuildContext context) {
final cards = data.options ?? [];
final screenHeight = MediaQuery.of(context).size.height;
final startTop = screenHeight * 560 / 1920;
return Scaffold(
body: Stack(
children: [
// Background full màn
Container(
decoration: BoxDecoration(
image: data.background != null
? DecorationImage(image: NetworkImage(data.background!), fit: BoxFit.cover)
: null,
color: Colors.green[100],
),
),
// Button Back
SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: CustomBackButton(),
),
),
Positioned(
top: startTop,
left: 16,
right: 16,
bottom: 0,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: cards.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 20,
crossAxisSpacing: 20,
childAspectRatio: 3 / 4,
),
itemBuilder: (context, index) {
final card = cards[index];
return GameCardItem(card: card);
},
),
),
],
),
);
}
}
class GameCardItem extends StatelessWidget {
final GameCardItemModel card;
const GameCardItem({super.key, required this.card});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
print(card.id);
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4, offset: Offset(2, 2))],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child:
card.image != null
? ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(card.image!, fit: BoxFit.cover),
)
: const SizedBox(),
),
const SizedBox(height: 6),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../shared/router_gage.dart';
import '../../widgets/custom_empty_widget.dart';
import '../../widgets/custom_navigation_bar.dart';
import 'game_tab_viewmodel.dart';
class GameTabScreen extends BaseScreen {
const GameTabScreen({super.key});
@override
State<GameTabScreen> createState() => _GameTabScreenState();
}
class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState {
final GameTabViewModel _viewModel = Get.put(GameTabViewModel());
final LayerLink _layerLink = LayerLink();
final GlobalKey _infoKey = GlobalKey();
OverlayEntry? _popupEntry;
bool _isPopupShown = false;
@override
void initState() {
super.initState();
_viewModel.getGames();
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(
title: "Games",
showBackButton: false,
rightButtons: [
CompositedTransformTarget(
link: _layerLink,
child: IconButton(
key: _infoKey,
icon: const Icon(Icons.info_outline, color: Colors.white),
onPressed: _togglePopup,
),
),
],
),
body: Obx(() {
if (_viewModel.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (_viewModel.games.isEmpty) {
return const Center(child: EmptyWidget());
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
_viewModel.turnsNumberText.value,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
itemCount: _viewModel.games.length,
separatorBuilder: (_, __) => const SizedBox(height: 4),
itemBuilder: (context, index) {
final item = _viewModel.games[index];
return GestureDetector(
onTap: () {
Get.toNamed(gameCardScreen, arguments: item);
},
child: AspectRatio(
aspectRatio: 343/132,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
item.icon ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const ColoredBox(
color: Colors.grey,
child: Center(child: Icon(Icons.broken_image)),
),
),
),
),
);
},
),
),
],
);
}),
);
}
void _togglePopup() {
if (_isPopupShown) {
_hidePopup();
} else {
_showPopup();
}
}
void _showPopup() {
final overlay = Overlay.of(context);
final renderBox = _infoKey.currentContext?.findRenderObject() as RenderBox?;
final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final size = renderBox?.size ?? Size.zero;
final widthSize = MediaQuery.of(context).size.width * 0.85;
_popupEntry = OverlayEntry(
builder: (context) => Stack(
children: [
// 👉 Tap ngoài popup để close
Positioned.fill(
child: GestureDetector(
onTap: _hidePopup,
behavior: HitTestBehavior.translucent,
child: Container(color: Colors.transparent),
),
),
Positioned(
top: offset.dy + size.height + 8,
left: widthSize*0.15/0.85/2, // offset.dx - widthSize,
child: Material(
borderRadius: BorderRadius.circular(16),
elevation: 4,
child: Container(
width: widthSize,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: const Text(
'1/ Thể lể trò chơi rất đơn giản, khách hàng sử dụng dịch vụ gói cước viễn thông của chúng tôi sẽ có cơ hội tham gia chơi game, khách hàng nào chơi cũng có thưởng do MyPoint (PayTech) tài trợ 100% phần quà.'
'\n\n2/ Ngoài ra các giải thưởng và luật lệ trong trò chơi không phải do Apple quản lý và tài trợ, điều này đã được thể hiện trong thể lệ và văn bản công bố trò chơi với khách hàng.',
style: TextStyle(fontSize: 14),
),
),
),
),
],
),
);
overlay.insert(_popupEntry!);
_isPopupShown = true;
}
void _hidePopup() {
_popupEntry?.remove();
_popupEntry = null;
_isPopupShown = false;
}
}
\ No newline at end of file
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/screen/game/models/game_bundle_item_model.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
class GameTabViewModel extends RestfulApiViewModel {
final RxList<GameBundleItemModel> games = <GameBundleItemModel>[].obs;
var turnsNumberText = "".obs;
var isLoading = false.obs;
var errorMessage = "".obs;
void getGames() {
isLoading(true);
client.getGames().then((value) {
if (!value.isSuccess) {
errorMessage.value = value.errorMessage ?? Constants.commonError;
} else {
games.value = value.data?.games ?? [];
turnsNumberText.value = value.data?.turnsNumberText ?? "";
}
isLoading(false);
});
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
class GameScreen extends StatelessWidget {
const GameScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Games'));
}
}
\ No newline at end of file
import 'package:json_annotation/json_annotation.dart';
import 'game_card_item_model.dart';
part 'game_bundle_item_model.g.dart';
@JsonSerializable()
class GameBundleItemModel {
final String? id;
final String? name;
final String? icon;
final String? background;
final String? description;
final List<GameCardItemModel>? options;
GameBundleItemModel({
this.id,
this.name,
this.icon,
this.background,
this.description,
this.options,
});
factory GameBundleItemModel.fromJson(Map<String, dynamic> json) => _$GameBundleItemModelFromJson(json);
Map<String, dynamic> toJson() => _$GameBundleItemModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_bundle_item_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GameBundleItemModel _$GameBundleItemModelFromJson(Map<String, dynamic> json) =>
GameBundleItemModel(
id: json['id'] as String?,
name: json['name'] as String?,
icon: json['icon'] as String?,
background: json['background'] as String?,
description: json['description'] as String?,
options:
(json['options'] as List<dynamic>?)
?.map(
(e) => GameCardItemModel.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$GameBundleItemModelToJson(
GameBundleItemModel instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'icon': instance.icon,
'background': instance.background,
'description': instance.description,
'options': instance.options,
};
import 'package:json_annotation/json_annotation.dart';
import 'game_bundle_item_model.dart';
part 'game_bundle_response.g.dart';
@JsonSerializable()
class GameBundleResponse {
@JsonKey(name: 'turns_number_text')
final String? turnsNumberText;
final List<GameBundleItemModel>? games;
GameBundleResponse({
this.turnsNumberText,
this.games,
});
factory GameBundleResponse.fromJson(Map<String, dynamic> json) => _$GameBundleResponseFromJson(json);
Map<String, dynamic> toJson() => _$GameBundleResponseToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_bundle_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GameBundleResponse _$GameBundleResponseFromJson(Map<String, dynamic> json) =>
GameBundleResponse(
turnsNumberText: json['turns_number_text'] as String?,
games:
(json['games'] as List<dynamic>?)
?.map(
(e) => GameBundleItemModel.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$GameBundleResponseToJson(GameBundleResponse instance) =>
<String, dynamic>{
'turns_number_text': instance.turnsNumberText,
'games': instance.games,
};
import 'package:json_annotation/json_annotation.dart';
part 'game_card_item_model.g.dart';
@JsonSerializable()
class GameCardItemModel {
final int? id;
final String? image;
final int? stt;
GameCardItemModel({
this.id,
this.image,
this.stt,
});
factory GameCardItemModel.fromJson(Map<String, dynamic> json) => _$GameCardItemModelFromJson(json);
Map<String, dynamic> toJson() => _$GameCardItemModelToJson(this);
}
\ No newline at end of file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_card_item_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
GameCardItemModel _$GameCardItemModelFromJson(Map<String, dynamic> json) =>
GameCardItemModel(
id: (json['id'] as num?)?.toInt(),
image: json['image'] as String?,
stt: (json['stt'] as num?)?.toInt(),
);
Map<String, dynamic> _$GameCardItemModelToJson(GameCardItemModel instance) =>
<String, dynamic>{
'id': instance.id,
'image': instance.image,
'stt': instance.stt,
};
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../game/games_screen.dart'; import '../../resouce/base_color.dart';
import '../game/game_tab_screen.dart';
import '../home/home_screen.dart'; import '../home/home_screen.dart';
import '../personal/personal_screen.dart'; import '../personal/personal_screen.dart';
import '../shopping/shopping_screen.dart';
import '../support/transaction_history_screen.dart'; import '../support/transaction_history_screen.dart';
import '../voucher/voucher_tab_screen.dart'; import '../voucher/voucher_tab_screen.dart';
...@@ -19,7 +19,7 @@ class _MainTabScreenState extends State<MainTabScreen> { ...@@ -19,7 +19,7 @@ class _MainTabScreenState extends State<MainTabScreen> {
final List<Widget> _pages = const [ final List<Widget> _pages = const [
HomeScreen(), HomeScreen(),
VoucherTabScreen(), VoucherTabScreen(),
GameScreen(), GameTabScreen(),
TransactionHistoryScreen(), TransactionHistoryScreen(),
PersonalScreen(), PersonalScreen(),
]; ];
...@@ -27,22 +27,27 @@ class _MainTabScreenState extends State<MainTabScreen> { ...@@ -27,22 +27,27 @@ class _MainTabScreenState extends State<MainTabScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.transparent, // cho sáng nền tổng thể
extendBody: true,
body: _pages[_currentIndex], body: _pages[_currentIndex],
bottomNavigationBar: Container( bottomNavigationBar: SafeArea(
decoration: const BoxDecoration( minimum: const EdgeInsets.only(left: 12, right: 12, bottom: 12),
color: Colors.red, child: ClipRRect(
), borderRadius: BorderRadius.circular(132),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: Container(
child: SafeArea( height: 72,
child: Row( color: BaseColor.primary500,
mainAxisAlignment: MainAxisAlignment.spaceAround, padding: const EdgeInsets.symmetric(horizontal: 16),
children: [ child: Row(
_buildTabItem(icon: Icons.home, label: 'Trang chủ', index: 0), mainAxisAlignment: MainAxisAlignment.center,
_buildTabItem(icon: Icons.star, label: 'Ưu đãi', index: 1), children: [
_buildTabItem(icon: Icons.videogame_asset, label: 'Game', index: 2), _buildTabItem(icon: Icons.home, label: 'Trang chủ', index: 0),
_buildTabItem(icon: Icons.shopping_cart, label: 'Mua sắm', index: 3), _buildTabItem(icon: Icons.star, label: 'Ưu đãi', index: 1),
_buildTabItem(icon: Icons.person, label: 'Cá nhân', index: 4), _buildTabItem(icon: Icons.videogame_asset, label: 'Game', index: 2),
], _buildTabItem(icon: Icons.shopping_cart, label: 'Mua sắm', index: 3),
_buildTabItem(icon: Icons.person, label: 'Cá nhân', index: 4),
],
),
), ),
), ),
), ),
...@@ -51,18 +56,30 @@ class _MainTabScreenState extends State<MainTabScreen> { ...@@ -51,18 +56,30 @@ class _MainTabScreenState extends State<MainTabScreen> {
Widget _buildTabItem({required IconData icon, required String label, required int index}) { Widget _buildTabItem({required IconData icon, required String label, required int index}) {
final isSelected = _currentIndex == index; final isSelected = _currentIndex == index;
return GestureDetector( return Expanded(
onTap: () => setState(() => _currentIndex = index), child: GestureDetector(
child: Column( onTap: () => setState(() => _currentIndex = index),
mainAxisSize: MainAxisSize.min, child: Container(
children: [ color: Colors.transparent,
Icon(icon, color: Colors.white.withOpacity(isSelected ? 1 : 0.6)), alignment: Alignment.center,
const SizedBox(height: 4), child: Column(
Text(label, style: TextStyle( mainAxisAlignment: MainAxisAlignment.center,
color: Colors.white.withOpacity(isSelected ? 1 : 0.6), children: [
fontSize: 12, Icon(icon,
)), size: 28,
], color: isSelected ? Colors.white : Colors.white70),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.white70,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 12,
),
),
],
),
),
), ),
); );
} }
......
...@@ -3,9 +3,12 @@ import 'package:flutter/material.dart'; ...@@ -3,9 +3,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:mypoint_flutter_app/screen/voucher/detail/store_list_section.dart'; import 'package:mypoint_flutter_app/screen/voucher/detail/store_list_section.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../base/base_screen.dart';
import '../../../base/basic_state.dart';
import '../../../resouce/base_color.dart'; import '../../../resouce/base_color.dart';
import '../../../widgets/back_button.dart'; import '../../../widgets/back_button.dart';
import '../../../widgets/custom_empty_widget.dart'; import '../../../widgets/custom_empty_widget.dart';
import '../../../widgets/custom_point_text_tag.dart';
import '../../../widgets/custom_price_tag.dart'; import '../../../widgets/custom_price_tag.dart';
import '../../../widgets/dashed_line.dart'; import '../../../widgets/dashed_line.dart';
import '../../../widgets/image_loader.dart'; import '../../../widgets/image_loader.dart';
...@@ -16,14 +19,14 @@ import '../models/product_model.dart'; ...@@ -16,14 +19,14 @@ import '../models/product_model.dart';
import 'voucher_detail_viewmodel.dart'; import 'voucher_detail_viewmodel.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class VoucherDetailScreen extends StatefulWidget { class VoucherDetailScreen extends BaseScreen {
const VoucherDetailScreen({super.key}); const VoucherDetailScreen({super.key});
@override @override
_VoucherDetailScreenState createState() => _VoucherDetailScreenState(); _VoucherDetailScreenState createState() => _VoucherDetailScreenState();
} }
class _VoucherDetailScreenState extends State<VoucherDetailScreen> { class _VoucherDetailScreenState extends BaseState<VoucherDetailScreen> with BasicState {
late final int productId; late final int productId;
late final VoucherDetailViewModel _viewModel; late final VoucherDetailViewModel _viewModel;
double _infoHeight = 0; double _infoHeight = 0;
...@@ -39,10 +42,15 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -39,10 +42,15 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
productId = args['productId']; productId = args['productId'];
} }
_viewModel = Get.put(VoucherDetailViewModel(productId: productId)); _viewModel = Get.put(VoucherDetailViewModel(productId: productId));
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
} }
@override @override
Widget build(BuildContext context) { Widget createBody() {
return Scaffold( return Scaffold(
backgroundColor: Colors.grey.shade100, backgroundColor: Colors.grey.shade100,
body: Obx(() { body: Obx(() {
...@@ -51,7 +59,12 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -51,7 +59,12 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
} }
final product = _viewModel.product.value; final product = _viewModel.product.value;
if (product == null) { if (product == null) {
return const Center(child: EmptyWidget()); return Stack(
children: [
const Center(child: EmptyWidget()),
SafeArea(child: Padding(padding: const EdgeInsets.all(8), child: CustomBackButton())),
],
);
} }
return Stack( return Stack(
children: [ children: [
...@@ -76,10 +89,7 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -76,10 +89,7 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [CustomBackButton(), _buildFavoriteButton()],
CustomBackButton(),
_buildFavoriteButton(),
],
), ),
), ),
), ),
...@@ -145,9 +155,9 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -145,9 +155,9 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
product.content?.name ?? '', product.content?.name ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold) style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildExpireAndStock(product), _buildExpireAndStock(product),
...@@ -171,8 +181,13 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -171,8 +181,13 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: Text(product.brand?.name ?? '', style: const TextStyle(fontSize: 14))), Expanded(child: Text(product.brand?.name ?? '', style: const TextStyle(fontSize: 14))),
PriceTagWidget(point: product.amountToBePaid ?? 0), // PriceTagWidget(point: product.amountToBePaid ?? 0),
CustomPointText(
point: product.amountToBePaid ?? 0,
type: product.price?.method,
),
], ],
), ),
], ],
), ),
...@@ -189,7 +204,8 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -189,7 +204,8 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
if (hasExpire) if (hasExpire)
Text('Hạn dùng: ', style: const TextStyle(color: Colors.grey, fontWeight: FontWeight.bold, fontSize: 12)), Text('Hạn dùng: ', style: const TextStyle(color: Colors.grey, fontWeight: FontWeight.bold, fontSize: 12)),
if (hasExpire) if (hasExpire)
Text(product.expire, style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 12)), Text(product.expired ? "Hết hạn" : product.expire,
style: const TextStyle(color: BaseColor.primary500, fontWeight: FontWeight.bold, fontSize: 12)),
if (isOutOfStock) if (isOutOfStock)
Container( Container(
margin: const EdgeInsets.only(left: 8), margin: const EdgeInsets.only(left: 8),
...@@ -269,7 +285,8 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -269,7 +285,8 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
Icons.language, Icons.language,
brand.website ?? '', brand.website ?? '',
onTap: () { onTap: () {
final url = brand.website!.startsWith('http') ? brand.website! : 'https://${brand.website}'; final website = brand.website?.trim() ?? "";
final url = website.startsWith('http') ? website : 'https://${brand.website}';
_launchUri(Uri(scheme: url)); _launchUri(Uri(scheme: url));
}, },
), ),
...@@ -295,7 +312,7 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -295,7 +312,7 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
children: [ children: [
Icon(icon, size: 18, color: Colors.black54), Icon(icon, size: 18, color: Colors.black54),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: Text(value, style: const TextStyle(fontSize: 13, color: Colors.black54))), Expanded(child: Text(value, style: const TextStyle(fontSize: 13, color: Colors.black))),
const Icon(Icons.chevron_right, color: Colors.black54), const Icon(Icons.chevron_right, color: Colors.black54),
], ],
), ),
...@@ -304,11 +321,16 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -304,11 +321,16 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
} }
Widget _buildBottomAction(ProductModel product) { Widget _buildBottomAction(ProductModel product) {
// if (!(product.isMyProduct final bool isOutOfStock = !(product.inStock ?? true);
// ? product.customerInfoModel?.status == MyProductStatusType.waiting if (isOutOfStock) {
// : (product.inStock == true && !(product.expired == true)))) { return const SizedBox.shrink();
// return const SizedBox.shrink(); }
// } if (product.isMyProduct && product.customerInfoModel?.status != MyProductStatusType.waiting) {
return const SizedBox.shrink();
}
if (product.expired) {
return const SizedBox.shrink();
}
if (product.isMyProduct) { if (product.isMyProduct) {
return _buildUseButton(); return _buildUseButton();
} else if (product.price?.method == CashType.point) { } else if (product.price?.method == CashType.point) {
...@@ -342,8 +364,10 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -342,8 +364,10 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
backgroundColor: Colors.green, backgroundColor: Colors.green,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
), ),
child: const Text('Sử Dụng', child: const Text(
style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.bold)), 'Sử Dụng',
style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.bold),
),
), ),
), ),
); );
...@@ -394,10 +418,7 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -394,10 +418,7 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Obx(() => Text( Obx(() => Text('${_quantity.value}', style: const TextStyle(fontSize: 16))),
'${_quantity.value}',
style: const TextStyle(fontSize: 16),
)),
const SizedBox(width: 12), const SizedBox(width: 12),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
...@@ -453,15 +474,8 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> { ...@@ -453,15 +474,8 @@ class _VoucherDetailScreenState extends State<VoucherDetailScreen> {
child: Container( child: Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(color: Colors.grey.withOpacity(0.6), shape: BoxShape.circle),
color: Colors.grey.withOpacity(0.6), child: Icon(Icons.favorite, color: isFavorite ? BaseColor.primary500 : Colors.white, size: 24),
shape: BoxShape.circle,
),
child: Icon(
Icons.favorite,
color: isFavorite ? BaseColor.primary600 : Colors.white,
size: 24,
),
), ),
), ),
), ),
......
...@@ -7,10 +7,11 @@ import '../models/product_store_model.dart'; ...@@ -7,10 +7,11 @@ import '../models/product_store_model.dart';
class VoucherDetailViewModel extends RestfulApiViewModel { class VoucherDetailViewModel extends RestfulApiViewModel {
final int productId; final int productId;
VoucherDetailViewModel({required this.productId}); VoucherDetailViewModel({required this.productId});
var stores = RxList<ProductStoreModel>(); var stores = RxList<ProductStoreModel>();
var product = Rxn<ProductModel>(); var product = Rxn<ProductModel>();
var isLoading = false.obs; var isLoading = false.obs;
var liked = false.obs; var liked = false.obs;
void Function(String message)? onShowAlertError;
@override @override
void onInit() { void onInit() {
...@@ -20,23 +21,22 @@ class VoucherDetailViewModel extends RestfulApiViewModel { ...@@ -20,23 +21,22 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
} }
Future<void> toggleFavorite() async { Future<void> toggleFavorite() async {
// if (liked.value) {
// liked.value = false;
// } else {
// liked.value = true;
// }
final value = product.value; final value = product.value;
if (value == null) return; if (value == null) return;
if (value!.liked == true) { try {
await client.unlikeProduct(value?.likeId ?? 0); if (value!.liked == true) {
value?.likeId = 0; await client.unlikeProduct(value?.likeId ?? 0);
Future.microtask(() => liked.value = false); value?.likeId = 0;
} else { liked.value = false;
final response = await client.likeProduct(productId); } else {
value?.likeId = response.data?.id; final response = await client.likeProduct(productId);
Future.microtask(() => liked.value = (response.data?.id ?? 0) != 0); value?.likeId = response.data?.id;
liked.value = (response.data?.id ?? 0) != 0;
}
} catch (error) {
onShowAlertError?.call("Error toggling favorite: $error");
print("Error toggling favorite: $error");
} }
// product.refresh();
} }
Future<void> _getProductDetail() async { Future<void> _getProductDetail() async {
...@@ -47,6 +47,7 @@ class VoucherDetailViewModel extends RestfulApiViewModel { ...@@ -47,6 +47,7 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
product.value = response.data; product.value = response.data;
liked.value = product.value?.liked == true; liked.value = product.value?.liked == true;
} catch (error) { } catch (error) {
onShowAlertError?.call("Error fetching product detail: $error");
print("Error fetching product detail: $error"); print("Error fetching product detail: $error");
} finally { } finally {
isLoading.value = false; isLoading.value = false;
...@@ -58,8 +59,8 @@ class VoucherDetailViewModel extends RestfulApiViewModel { ...@@ -58,8 +59,8 @@ class VoucherDetailViewModel extends RestfulApiViewModel {
final response = await client.getProductStores(productId); final response = await client.getProductStores(productId);
stores.value = response.data ?? []; stores.value = response.data ?? [];
} catch (error) { } catch (error) {
print("Error fetching product detail: $error"); onShowAlertError?.call("Error product stores: $error");
} finally { print("Error product stores: $error");
} } finally {}
} }
} }
...@@ -18,7 +18,7 @@ part 'product_model.g.dart'; ...@@ -18,7 +18,7 @@ part 'product_model.g.dart';
class ProductModel { class ProductModel {
final int? id; final int? id;
@JsonKey(name: 'like_id') @JsonKey(name: 'like_id')
late final int? likeId; int? likeId;
@JsonKey(name: 'quantity_available') @JsonKey(name: 'quantity_available')
final int? quantityAvailable; final int? quantityAvailable;
final ProductContentModel? content; final ProductContentModel? content;
...@@ -60,6 +60,13 @@ class ProductModel { ...@@ -60,6 +60,13 @@ class ProductModel {
return ex.toDate()?.toFormattedString() ?? ""; return ex.toDate()?.toFormattedString() ?? "";
} }
bool get expired {
if (customerInfoModel?.status == MyProductStatusType.expired) return true;
final expireDate = expire.toDateFormat('dd/MM/yyyy');
if (expireDate == null) return false;
return expireDate!.isBefore(DateTime.now());
}
int? get amountToBePaid { int? get amountToBePaid {
if (previewFlashSale?.isFlashSalePrice == true) { if (previewFlashSale?.isFlashSalePrice == true) {
return previewFlashSale?.price; return previewFlashSale?.price;
...@@ -79,13 +86,6 @@ class ProductModel { ...@@ -79,13 +86,6 @@ class ProductModel {
return (likeId ?? 0) != 0; return (likeId ?? 0) != 0;
} }
bool get expired {
if (customerInfoModel != null) {
return customerInfoModel?.status == MyProductStatusType.expired;
}
return (quantityAvailable ?? 1) != 0;
}
ProductMediaItem? get banner { ProductMediaItem? get banner {
if (media == null) return null; if (media == null) return null;
return media!.firstWhere((item) => item.type == MediaType.banner16_9, orElse: () => media!.first); return media!.firstWhere((item) => item.type == MediaType.banner16_9, orElse: () => media!.first);
......
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