Commit 417358c5 authored by DatHV's avatar DatHV
Browse files

update authen 401, device manager, interestied category

parent efb4662c
import 'package:flutter/material.dart';
import 'device_manager_model.dart';
class DeviceInfoSheet extends StatelessWidget {
final DeviceItemModel item;
final VoidCallback? onDelete;
const DeviceInfoSheet({super.key, required this.item, this.onDelete});
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if ((item.appName ?? '').isNotEmpty)
Text(
item.appName ?? '',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
),
if (item.isCurrent == true)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.all(Radius.circular(24)),
),
child: Text(
'Thiết bị hiện tại',
style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
),
),
const SizedBox(height: 32),
_InfoRow(label: 'Đăng nhập:', value: item.lastLogin ?? '-'),
const SizedBox(height: 12),
_InfoRow(label: 'Phương thức:', value: item.loginMethod2 ?? '-'),
const SizedBox(height: 12),
if ((item.location ?? '').isNotEmpty) _InfoRow(label: 'Địa điểm:', value: item.location ?? '-'),
if ((item.location ?? '').isNotEmpty) SizedBox(height: 12),
_InfoRow(label: 'Địa chỉ IP:', value: item.ipAddress ?? '-'),
const SizedBox(height: 32),
if (item.isCurrent != true)
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: onDelete,
style: OutlinedButton.styleFrom(
side: BorderSide(color: Colors.black26),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
foregroundColor: Colors.red,
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
child: const Text('Xóa thiết bị'),
),
),
],
),
),
);
}
}
class _InfoRow extends StatelessWidget {
final String label;
final String value;
const _InfoRow({required this.label, required this.value});
@override
Widget build(BuildContext context) {
final styleLabel = TextStyle(color: Colors.grey.shade700, fontSize: 16, fontWeight: FontWeight.w600);
const styleValue = TextStyle(fontSize: 16, fontWeight: FontWeight.w600);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 110, // cố định để canh lề đẹp
child: Text(label, style: styleLabel),
),
const SizedBox(width: 8),
Expanded(child: Text(value, style: styleValue)),
],
);
}
}
class DeviceItemModel {
String? deviceKey;
String? lastLogin;
String? model;
String? version;
String? appName;
String? platform;
String? ipAddress;
String? loginMethod;
String? loginMethod2;
String? location;
String? pastTime;
String? statusDisplay;
bool? isCurrent;
DeviceItemModel({
this.deviceKey,
this.lastLogin,
this.model,
this.version,
this.appName,
this.platform,
this.ipAddress,
this.loginMethod,
this.loginMethod2,
this.location,
this.pastTime,
this.statusDisplay,
this.isCurrent,
});
factory DeviceItemModel.fromJson(Map<String, dynamic> json) {
return DeviceItemModel(
deviceKey: json['device_key'] as String?,
lastLogin: json['last_login'] as String?,
model: json['model'] as String?,
version: json['version'] as String?,
appName: json['app_name'] as String?,
platform: json['platform'] as String?,
ipAddress: json['ip_address'] as String?,
loginMethod: json['login_method'] as String?,
loginMethod2: json['login_method2'] as String?,
location: json['location'] as String?,
pastTime: json['past_time'] as String?,
statusDisplay: json['status_display'] as String?,
isCurrent: json['is_current'] as bool?,
);
}
Map<String, dynamic> toJson() {
return {
'device_key': deviceKey,
'last_login': lastLogin,
'model': model,
'version': version,
'app_name': appName,
'platform': platform,
'ip_address': ipAddress,
'login_method': loginMethod,
'login_method2': loginMethod2,
'location': location,
'past_time': pastTime,
'status_display': statusDisplay,
'is_current': isCurrent,
};
}
}
class DevicesLogoutListResponse {
List<DeviceItemModel>? devices;
int? total;
DevicesLogoutListResponse({this.devices, this.total});
factory DevicesLogoutListResponse.fromJson(Map<String, dynamic> json) {
return DevicesLogoutListResponse(
devices: (json['devices'] as List<dynamic>?)
?.map((e) => DeviceItemModel.fromJson(e as Map<String, dynamic>))
.toList(),
total: json['total'] as int?,
);
}
Map<String, dynamic> toJson() {
return {
'devices': devices?.map((e) => e.toJson()).toList(),
'total': total,
};
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../widgets/bottom_sheet_helper.dart';
import '../../widgets/custom_navigation_bar.dart';
import 'device_info_popup.dart';
import 'device_manager_model.dart';
import 'device_manager_viewmodel.dart';
import 'logged_out_devices_screen.dart';
class DeviceManagerScreen extends BaseScreen {
const DeviceManagerScreen({super.key});
@override
State<DeviceManagerScreen> createState() => _DeviceManagerScreenState();
}
class _DeviceManagerScreenState extends BaseState<DeviceManagerScreen> with BasicState {
final _viewModel = DeviceManagerViewModel();
@override
void initState() {
super.initState();
_viewModel.getData();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(title: 'Quản lý thiết bị đăng nhập'),
body: Obx(() {
final logoutDevices = _viewModel.logoutDevicesResponse.value?.devices ?? [];
final logoutDisplayDevices = logoutDevices.take(3).toList() ?? [];
final currentDevice = _viewModel.currentDevice.value;
return RefreshIndicator(
onRefresh: () async => _refresh(),
child: ListView(
padding: EdgeInsets.zero,
children: [
const _SectionHeader('Thiết bị hiện tại'),
if (currentDevice == null)
const _EmptyRow('Không có thiết bị hiện tại')
else
DeviceItemWidget(item: currentDevice, onMore: () => _showMore(currentDevice)),
const Divider(height: 12, thickness: 8),
const _SectionHeader('Thiết bị đã đăng xuất gần đây'),
const SizedBox(height: 12),
if (logoutDisplayDevices.isEmpty)
EmptyWidget(content: 'Chưa có thiết bị đã đăng xuất gần đây')
else
...logoutDisplayDevices.map((e) => DeviceItemWidget(item: e, onMore: () => _showMore(e))),
const SizedBox(height: 16),
if (logoutDisplayDevices.isNotEmpty)
Center(
child: TextButton(
onPressed: () {
Get.to(() => LoggedOutDeviceScreen());
},
child: Text(
'Xem tất cả thiết bị đã đăng xuất (${logoutDevices.length})',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Colors.blueAccent),
),
),
),
const SizedBox(height: 24),
],
),
);
}),
);
}
void _refresh() {
_viewModel.getData();
}
void _showMore(DeviceItemModel item) async {
BottomSheetHelper.showBottomSheetPopup(
child: DeviceInfoSheet(
item: item,
onDelete: () {
Get.back();
_viewModel.deleteDevice(item);
},
),
backgroundContainerColor: Colors.white,
);
}
}
class _SectionHeader extends StatelessWidget {
final String text;
const _SectionHeader(this.text);
@override
Widget build(BuildContext context) {
return Container(
color: Colors.grey.shade100,
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(text, style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w800)),
);
}
}
class _EmptyRow extends StatelessWidget {
final String text;
const _EmptyRow(this.text);
@override
Widget build(BuildContext context) {
return ListTile(
leading: const Icon(Icons.devices_other, color: Colors.grey),
title: Text(text, style: const TextStyle(color: Colors.grey)),
);
}
}
class DeviceItemWidget extends StatelessWidget {
final DeviceItemModel item;
final VoidCallback onMore;
const DeviceItemWidget({super.key, required this.item, required this.onMore});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onMore,
child: Column(
children: [
ListTile(
leading: _DeviceIcon(platform: item.platform),
title:
(item.appName ?? '').isNotEmpty
? Text(
item.appName ?? '',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87),
)
: null,
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if ((item.loginMethod ?? '').isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2.0, bottom: 2.0),
child: Text(item.loginMethod!, style: TextStyle(color: Colors.grey.shade600)),
),
if ((item.lastLogin ?? '').isNotEmpty)
Text(item.lastLogin!, style: TextStyle(color: Colors.grey.shade600)),
],
),
trailing: IconButton(icon: const Icon(Icons.more_horiz), onPressed: onMore),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
),
Divider(height: 1, color: Colors.grey.shade200),
],
),
);
}
}
class _DeviceIcon extends StatelessWidget {
final String? platform;
const _DeviceIcon({this.platform});
@override
Widget build(BuildContext context) {
final isIOS = (platform ?? '').toLowerCase().contains('ios');
final isAndroid = (platform ?? '').toLowerCase().contains('android');
final icon =
isIOS
? Icons.phone_iphone_rounded
: isAndroid
? Icons.android_rounded
: Icons.devices_other_rounded;
return Icon(icon, size: 36, color: Colors.grey.shade700);
}
}
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 '../../base/restful_api_viewmodel.dart';
import 'device_manager_model.dart';
class DeviceManagerViewModel extends RestfulApiViewModel {
var logoutDevicesResponse = Rxn<DevicesLogoutListResponse>();
var currentDevice = Rxn<DeviceItemModel>();
void Function(String message)? onShowAlertError;
getData() {
getLogoutDevicesResponse();
getCurrentDevice();
}
Future<void> getLogoutDevicesResponse() async {
final body = {"page": 0, "limit": 200};
showLoading();
try {
final response = await client.getLogoutDevices(body);
hideLoading();
if (response.isSuccess && response.data != null) {
logoutDevicesResponse.value = response.data;
} else {
onShowAlertError?.call(response.message ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
}
}
Future<void> getCurrentDevice() async {
final response = await client.getCurrentDevice();
if (response.isSuccess && response.data != null) {
currentDevice.value = response.data;
}
}
Future<void> deleteDevice(DeviceItemModel item) async {
if ((item.deviceKey ?? '').isEmpty) return;
showLoading();
try {
final response = await client.deleteDevice(item.deviceKey ?? '');
hideLoading();
if (response.isSuccess) {
getLogoutDevicesResponse();
onShowAlertError?.call(response.data ?? "Đã xóa thiết bị thành công");
} else {
onShowAlertError?.call(response.message ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call("Error fetching product detail: $error");
}
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../widgets/bottom_sheet_helper.dart';
import '../../widgets/custom_navigation_bar.dart';
import 'device_info_popup.dart';
import 'device_manager_model.dart';
import 'device_manager_screen.dart';
import 'device_manager_viewmodel.dart';
class LoggedOutDeviceScreen extends BaseScreen {
const LoggedOutDeviceScreen({super.key});
@override
State<LoggedOutDeviceScreen> createState() => _LoggedOutDeviceScreenState();
}
class _LoggedOutDeviceScreenState extends BaseState<LoggedOutDeviceScreen> with BasicState {
final _viewModel = DeviceManagerViewModel();
@override
void initState() {
super.initState();
_viewModel.getLogoutDevicesResponse();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message);
}
};
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(title: 'Các thiết bị đã đăng xuất'),
body: Obx(() {
final logoutDevices = _viewModel.logoutDevicesResponse.value?.devices ?? [];
return RefreshIndicator(
onRefresh: () async => _refresh(),
child: ListView(
padding: EdgeInsets.zero,
children: [
const SizedBox(height: 12),
if (logoutDevices.isEmpty)
EmptyWidget(content: 'Chưa có thiết bị đã đăng xuất gần đây')
else
...logoutDevices.map((e) => DeviceItemWidget(item: e, onMore: () => _showMore(e))),
const SizedBox(height: 32),
],
),
);
}),
);
}
void _refresh() {
_viewModel.getLogoutDevicesResponse();
}
void _showMore(DeviceItemModel item) async {
BottomSheetHelper.showBottomSheetPopup(
child: DeviceInfoSheet(item: item, onDelete: () {
Get.back();
_viewModel.deleteDevice(item);
}),
backgroundContainerColor: Colors.white,
);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/widgets/image_loader.dart';
import '../../resources/base_color.dart';
import '../../shared/router_gage.dart';
import '../../widgets/custom_navigation_bar.dart';
import '../transaction/model/payment_method_model.dart';
import '../webview/payment_web_view_screen.dart';
import 'models/customer_contract_object_model.dart';
import 'electric_payment_bill_viewmodel.dart';
class ElectricPaymentBillScreen extends StatefulWidget {
final CustomerContractModel bill;
const ElectricPaymentBillScreen({super.key, required this.bill});
@override
State<ElectricPaymentBillScreen> createState() => _ElectricPaymentBillScreenState();
}
class _ElectricPaymentBillScreenState extends State<ElectricPaymentBillScreen> {
final _viewModel = ElectricPaymentBillViewModel();
@override
void initState() {
super.initState();
_viewModel.customerEvnPaymentGatewayResponse = (data) {
Get.toNamed(
paymentWebViewScreen,
arguments: PaymentWebViewInput(
url: data?.vitapayData ?? "",
isContract: false,
orderId: data?.requestId ?? "",
showAlertBack: false,
callback: (result) {
if (result == PaymentProcess.success) {
Get.offNamed(
transactionHistoryDetailScreen,
arguments: {"orderId": data?.requestId ?? "", "canBack": true},
);
}
},
)
);
};
_viewModel.getPaymentMethods();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomNavigationBar(title: 'Chi tiết hóa đơn điện'),
body: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Image.asset('assets/images/ic_evn_logo.png', height: 48, fit: BoxFit.cover),
const SizedBox(width: 12),
Text(
'Điện lực ${widget.bill.location ?? ''}',
style: TextStyle(fontSize: 18, color: Colors.blueAccent, fontWeight: FontWeight.w700),
),
],
),
),
Divider(thickness: 1, color: Colors.grey[200]),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow('Mã khách hàng:', widget.bill.maKH),
_buildInfoRow('Tên khách hàng:', widget.bill.nameKH),
_buildInfoRow('Mã hóa đơn:', widget.bill.idHoaHon),
_buildInfoRow('Kỳ hóa đơn:', widget.bill.ky),
_buildInfoRow(
'Tổng giá trị hóa đơn:',
(widget.bill.amount ?? 0).money(CurrencyUnit.VND),
valueColor: Colors.orange,
),
],
),
),
const SizedBox(height: 16),
Divider(thickness: 1, color: Colors.grey[200]),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Phương thức thanh toán',
style: TextStyle(fontSize: 18, color: Colors.black87, fontWeight: FontWeight.w600),
),
),
const SizedBox(height: 12),
_buildPaymentMethods(),
],
),
);
},
),
bottomNavigationBar: _buildBottomButton(),
);
}
Widget _buildBottomButton() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 4))],
),
child: SafeArea(
top: false,
child: ElevatedButton(
onPressed: () {
_viewModel.customerEvnPaymentGatewayRequest(widget.bill);
},
style: ElevatedButton.styleFrom(
backgroundColor: BaseColor.primary500,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
child: Text('Thanh toán', style: TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w700)),
),
),
);
}
Widget _buildInfoRow(String label, String? value, {Color? valueColor}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
SizedBox(
width: 180,
child: Text(
label,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black54),
),
),
Text(
textAlign: TextAlign.start,
value ?? '',
style: TextStyle(
fontSize: 17,
color: valueColor ?? Colors.black87,
fontWeight: valueColor != null ? FontWeight.bold : FontWeight.normal,
),
),
],
),
);
}
Widget _buildPaymentMethods() {
final methods = _viewModel.paymentMethods;
return Obx(
() => Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.white,
border: Border.all(color: Colors.grey.shade300, width: 1),
),
child: Column(
children: List.generate(methods.length, (index) => _buildPaymentMethodItem(methods[index], index)),
),
),
);
}
Widget _buildPaymentMethodItem(PaymentMethodModel method, int index) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RadioListTile<int>(
activeColor: BaseColor.primary400,
value: index,
groupValue: _viewModel.selectedPaymentMethodIndex.value,
onChanged: (val) {
setState(() {
_viewModel.selectedPaymentMethodIndex.value = val ?? 0;
});
},
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: Row(
children: [
if (method.logo != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: loadNetworkImage(
url: method.logo,
width: 24,
height: 24,
fit: BoxFit.cover,
placeholderAsset: 'assets/images/ic_logo.png',
),
),
Text(method.name ?? '', style: const TextStyle(fontSize: 16)),
],
),
),
if (index < _viewModel.paymentMethods.length - 1)
Container(height: 1, color: Colors.grey.shade200, margin: const EdgeInsets.symmetric(horizontal: 16)),
],
);
}
}
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/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
import '../transaction/model/payment_method_model.dart';
import 'models/customer_contract_object_model.dart';
import 'models/electric_payment_response_model.dart';
class ElectricPaymentBillViewModel extends RestfulApiViewModel {
var paymentMethods = RxList<PaymentMethodModel>();
var selectedPaymentMethodIndex = 0.obs;
void Function(ElectricPaymentResponseModel data)? customerEvnPaymentGatewayResponse;
void Function(String message)? onShowAlertError;
Future<void> getPaymentMethods() async {
showLoading();
try {
final response = await client.getPreviewPaymentMethods();
hideLoading();
selectedPaymentMethodIndex.value = 0;
paymentMethods.value = response.data ?? [];
} catch (error) {
hideLoading();
}
}
customerEvnPaymentGatewayRequest(CustomerContractModel bill) async {
final paymentMethod = paymentMethods.value.safe(selectedPaymentMethodIndex.value ?? 0) ?? paymentMethods.firstOrNull;
final paymentMethodType = paymentMethod?.type?.methodBillEVN ?? '';
if (paymentMethodType.isEmpty) {
onShowAlertError?.call("Vui lòng chọn phương thức thanh toán.");
return;
}
showLoading();
try {
final response = await client.customerEvnPaymentGatewayRequest(bill, paymentMethodType);
hideLoading();
if (response.isSuccess && response.data != null) {
customerEvnPaymentGatewayResponse?.call(response.data!);
} else {
onShowAlertError?.call(response.errorMessage ?? "Lỗi khi thanh toán, vui lòng thử lại sau.");
}
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
}
}
}
...@@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; ...@@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/extensions/num_extension.dart'; import 'package:mypoint_flutter_app/extensions/num_extension.dart';
import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart'; import 'package:mypoint_flutter_app/widgets/custom_empty_widget.dart';
import '../../resouce/base_color.dart'; import '../../resources/base_color.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import '../../widgets/custom_navigation_bar.dart'; import '../../widgets/custom_navigation_bar.dart';
import 'customer_contract_object_model.dart'; import 'models/customer_contract_object_model.dart';
import 'electric_payment_bill_screen.dart';
import 'electric_payment_viewmodel.dart'; import 'electric_payment_viewmodel.dart';
class ElectricPaymentHistoryScreen extends StatefulWidget { class ElectricPaymentHistoryScreen extends StatefulWidget {
...@@ -121,42 +122,98 @@ class _ElectricPaymentHistoryScreenState extends State<ElectricPaymentHistoryScr ...@@ -121,42 +122,98 @@ class _ElectricPaymentHistoryScreenState extends State<ElectricPaymentHistoryScr
final bill = _viewModel.billContracts.value[index]; final bill = _viewModel.billContracts.value[index];
final isSelected = selectedCodes.contains(bill.maKH ?? ''); final isSelected = selectedCodes.contains(bill.maKH ?? '');
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade100, color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: ListTile( child: Padding(
leading: padding: const EdgeInsets.symmetric(vertical: 8),
isEditMode child: Container(
? Checkbox(value: isSelected, onChanged: (val) => toggleItem(bill, val)) padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
: null, decoration: BoxDecoration(
title: Text(bill.location ?? ''), color: Colors.grey.shade100,
subtitle: Text(bill.maKH ?? ''), borderRadius: BorderRadius.circular(8),
trailing: ),
((bill.amount ?? 0) == 0) child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (isEditMode)
Padding(
padding: const EdgeInsets.only(right: 8),
child: Checkbox(value: isSelected, onChanged: (val) => toggleItem(bill, val)),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(bill.location ?? '', style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
if ((bill.maKH ?? '').isNotEmpty) Text(bill.maKH ?? ''),
const SizedBox(height: 6),
if ((bill.nameKH ?? '').isNotEmpty) Text(bill.nameKH ?? ''),
],
),
),
const SizedBox(width: 12),
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100, maxWidth: 140),
child:
(bill.amount ?? 0) == 0
? Column( ? Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ crossAxisAlignment: CrossAxisAlignment.end,
const Icon(Icons.check_circle, color: Colors.green), children: const [
Icon(Icons.check_circle, color: Colors.green),
SizedBox(height: 4),
Text( Text(
'Bạn đã hết nợ cước', 'Bạn đã hết nợ cước',
style: const TextStyle(fontSize: 12, color: Colors.green), style: TextStyle(fontSize: 12, color: Colors.green),
), ),
], ],
) )
: Column( : Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
const Icon(Icons.error, color: Colors.red),
Text( Text(
(bill.amount ?? 0).money(CurrencyUnit.vnd), (bill.amount ?? 0).money(CurrencyUnit.VND),
style: const TextStyle(fontSize: 12, color: Colors.red), style: const TextStyle(
fontSize: 14,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () {
Get.to(ElectricPaymentBillScreen(bill: bill,));
print('Thanh toán hoá đơn: ${bill.maKH ?? ''}');
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: BaseColor.primary500,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Thanh toán',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
), ),
], ],
), ),
), ),
],
),
),
),
), ),
); );
}, },
......
...@@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; ...@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../base/base_screen.dart'; import '../../base/base_screen.dart';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../resouce/base_color.dart'; import '../../resources/base_color.dart';
import '../../widgets/custom_navigation_bar.dart'; import '../../widgets/custom_navigation_bar.dart';
import 'electric_payment_viewmodel.dart'; import 'electric_payment_viewmodel.dart';
......
import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart'; import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart'; import '../../configs/constants.dart';
import 'customer_contract_object_model.dart'; import 'electric_payment_bill_screen.dart';
import 'models/customer_contract_object_model.dart';
class ElectricPaymentViewModel extends RestfulApiViewModel { class ElectricPaymentViewModel extends RestfulApiViewModel {
var responseData = Rxn<CustomerContractModel>();
void Function(String message)? onShowAlertError; void Function(String message)? onShowAlertError;
final RxList<CustomerContractModel> billContracts = <CustomerContractModel>[].obs; final RxList<CustomerContractModel> billContracts = <CustomerContractModel>[].obs;
...@@ -22,8 +22,7 @@ class ElectricPaymentViewModel extends RestfulApiViewModel { ...@@ -22,8 +22,7 @@ class ElectricPaymentViewModel extends RestfulApiViewModel {
if ((result.amount ?? 0) == 0) { if ((result.amount ?? 0) == 0) {
onShowAlertError?.call("Bạn đã thanh toán hết hóa đơn."); onShowAlertError?.call("Bạn đã thanh toán hết hóa đơn.");
} else { } else {
// TODO Get.to(ElectricPaymentBillScreen(bill: result,));
responseData.value = result;
} }
} else { } else {
onShowAlertError?.call("Không tìm thấy thông tin mã khách hàng, vui lòng kiểm tra và thử lại."); onShowAlertError?.call("Không tìm thấy thông tin mã khách hàng, vui lòng kiểm tra và thử lại.");
......
import 'package:json_annotation/json_annotation.dart';
part 'electric_payment_response_model.g.dart';
@JsonSerializable()
class ElectricPaymentResponseModel {
@JsonKey(name: 'request_id')
final String? requestId;
@JsonKey(name: 'vitapay_status')
final String? vitapayStatus;
@JsonKey(name: 'vitapay_message')
final String? vitapayMessage;
@JsonKey(name: 'vitapay_amount')
final double? vitapayAmount;
@JsonKey(name: 'vitapay_datatype')
final String? vitapayDatatype;
@JsonKey(name: 'vitapay_data')
final String? vitapayData;
@JsonKey(name: 'item_ids')
final String? itemIds;
ElectricPaymentResponseModel({
this.requestId,
this.vitapayStatus,
this.vitapayMessage,
this.vitapayAmount,
this.vitapayDatatype,
this.vitapayData,
this.itemIds,
});
factory ElectricPaymentResponseModel.fromJson(Map<String, dynamic> json) =>
_$ElectricPaymentResponseModelFromJson(json);
Map<String, dynamic> toJson() => _$ElectricPaymentResponseModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'electric_payment_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ElectricPaymentResponseModel _$ElectricPaymentResponseModelFromJson(
Map<String, dynamic> json,
) => ElectricPaymentResponseModel(
requestId: json['request_id'] as String?,
vitapayStatus: json['vitapay_status'] as String?,
vitapayMessage: json['vitapay_message'] as String?,
vitapayAmount: (json['vitapay_amount'] as num?)?.toDouble(),
vitapayDatatype: json['vitapay_datatype'] as String?,
vitapayData: json['vitapay_data'] as String?,
itemIds: json['item_ids'] as String?,
);
Map<String, dynamic> _$ElectricPaymentResponseModelToJson(
ElectricPaymentResponseModel instance,
) => <String, dynamic>{
'request_id': instance.requestId,
'vitapay_status': instance.vitapayStatus,
'vitapay_message': instance.vitapayMessage,
'vitapay_amount': instance.vitapayAmount,
'vitapay_datatype': instance.vitapayDatatype,
'vitapay_data': instance.vitapayData,
'item_ids': instance.itemIds,
};
...@@ -5,7 +5,7 @@ import 'package:mypoint_flutter_app/screen/pageDetail/campaign_detail_screen.dar ...@@ -5,7 +5,7 @@ import 'package:mypoint_flutter_app/screen/pageDetail/campaign_detail_screen.dar
import 'package:mypoint_flutter_app/widgets/back_button.dart'; import 'package:mypoint_flutter_app/widgets/back_button.dart';
import '../../base/base_screen.dart'; import '../../base/base_screen.dart';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../resouce/base_color.dart'; import '../../resources/base_color.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import 'faqs_viewmodel.dart'; import 'faqs_viewmodel.dart';
......
...@@ -7,6 +7,8 @@ import 'package:mypoint_flutter_app/screen/home/custom_widget/product_grid_widge ...@@ -7,6 +7,8 @@ import 'package:mypoint_flutter_app/screen/home/custom_widget/product_grid_widge
import 'package:mypoint_flutter_app/screen/home/pipi_detail_screen.dart'; import 'package:mypoint_flutter_app/screen/home/pipi_detail_screen.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart'; import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../../preference/point/header_home_model.dart'; import '../../preference/point/header_home_model.dart';
import '../popup_manager/popup_manager_model.dart';
import '../popup_manager/popup_manager_popup.dart';
import '../voucher/sub_widget/voucher_section_title.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';
...@@ -223,15 +225,44 @@ class _HomeScreenState extends State<HomeScreen> { ...@@ -223,15 +225,44 @@ class _HomeScreenState extends State<HomeScreen> {
} }
void _handleHoverViewTap() { void _handleHoverViewTap() {
final result = _viewModel.hoverData.value?.direction?.begin(); showPopup(
if (result != true) { context,
showModalBottomSheet( modelPopup: PopupManagerModel(
context: context, timeCountDown: '10',
backgroundColor: Colors.transparent, id: 'popup123',
isScrollControlled: true, requestId: 'req_abc',
builder: (_) => PipiDetailScreen(), popupTitleTemplate: 'Khuyến mãi đặc biệt',
popupBodyTemplate: 'Giảm 50% cho đơn hàng hôm nay.',
imageURL: 'https://picsum.photos/1200/800',
clickActionType: 'VIEW_GIFT',
clickActionParam: 'gift_999',
),
onNavigate: ({
required String name,
required String identifier,
String? title,
String? body,
}) {
// Tự nối qua router của bạn
// ví dụ:
// context.pushNamed(name, extra: {'id': identifier, 'title': title, 'body': body});
debugPrint('Navigate -> $name, id=$identifier');
},
onDismissed: () {
// Thay cho NotificationCenter.dismissPopup
debugPrint('Popup dismissed');
},
); );
}
// final result = _viewModel.hoverData.value?.direction?.begin();
// if (result != true) {
// showModalBottomSheet(
// context: context,
// backgroundColor: Colors.transparent,
// isScrollControlled: true,
// builder: (_) => PipiDetailScreen(),
// );
// }
} }
void _handleCloseHoverView() { void _handleCloseHoverView() {
...@@ -241,7 +272,6 @@ class _HomeScreenState extends State<HomeScreen> { ...@@ -241,7 +272,6 @@ class _HomeScreenState extends State<HomeScreen> {
} }
Future<void> _onRefresh() async { Future<void> _onRefresh() async {
print("onRefresh");
await _viewModel.getSectionLayoutHome(); await _viewModel.getSectionLayoutHome();
await _viewModel.loadDataPiPiHome(); await _viewModel.loadDataPiPiHome();
await _headerHomeVM.freshData(); await _headerHomeVM.freshData();
...@@ -250,41 +280,4 @@ class _HomeScreenState extends State<HomeScreen> { ...@@ -250,41 +280,4 @@ class _HomeScreenState extends State<HomeScreen> {
void _showMiniGame(BuildContext context) async { void _showMiniGame(BuildContext context) async {
Navigator.push(context, MaterialPageRoute(builder: (_) => const GameMiniAppScreen())); Navigator.push(context, MaterialPageRoute(builder: (_) => const GameMiniAppScreen()));
} }
void _logout(BuildContext context) async {
final confirm = await showDialog<bool>(
context: context,
builder:
(ctx) => AlertDialog(
title: const Text('Xác nhận'),
content: const Text('Bạn có chắc muốn đăng xuất?'),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Hủy')),
TextButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Đăng xuất')),
],
),
);
if (confirm == true) {
DataPreference.instance.clearLoginToken();
_safeBackToLogin();
}
}
void _safeBackToLogin() {
bool found = false;
Navigator.popUntil(Get.context!, (route) {
final matched = route.settings.name == loginScreen;
if (matched) found = true;
return matched;
});
final phone = DataPreference.instance.phone;
if (phone != null) {
if (!found) {
Get.offAllNamed(loginScreen, arguments: phone);
}
} else {
DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen);
}
}
} }
import 'package:flutter/material.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resources/base_color.dart';
import '../../widgets/custom_navigation_bar.dart';
import 'interestied_categories_viewmodel.dart';
class InterestCategoriesScreen extends BaseScreen {
const InterestCategoriesScreen({super.key});
@override
State<InterestCategoriesScreen> createState() => _InterestCategoriesScreenState();
}
class _InterestCategoriesScreenState extends BaseState<InterestCategoriesScreen> with BasicState {
final _viewModel = InterestedCategoriesViewModel();
bool _onChange = false;
@override
void initState() {
super.initState();
_viewModel.onShowAlertError = (message) {
if (message.isNotEmpty) {
showAlertError(content: message, headerImage: "assets/images/ic_pipi_05.png");
}
};
_viewModel.getInterestedCategories();
}
@override
Widget createBody() {
return Scaffold(
appBar: CustomNavigationBar(title: 'Các lĩnh vực quan tâm'),
body: Obx(() {
final listItems = _viewModel.interestedCategories.value?.listItems ?? [];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: const Align(
alignment: Alignment.centerLeft,
child: Text(
'Bạn quan tâm lĩnh vực nào? Cho MyPoint biết để được gợi ý các ưu đãi phù hợp nhé!',
style: TextStyle(fontSize: 17, height: 1.35, color: Colors.black87),
),
),
),
const SizedBox(height: 16),
Flexible(
child: GridView.builder(
padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16),
itemCount: listItems.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.6,
),
itemBuilder: (context, index) {
final it = listItems[index];
final id = it.categoryCode ?? '$index';
final selected = _viewModel.selectedIds.contains(id);
return _InterestCard(
title: it.categoryName ?? '',
imageUrl: it.imageUrl,
selected: selected,
onTap: () {
setState(() {
_onChange = true;
if (selected) {
_viewModel.selectedIds.remove(id);
} else {
_viewModel.selectedIds.add(id);
}
});
},
);
},
),
),
],
);
}),
bottomNavigationBar: _buildBottomButton(),
);
}
Widget _buildBottomButton() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 4))],
),
child: SafeArea(
top: false,
child: ElevatedButton(
onPressed:
_onChange
? () {
_viewModel.submitInterestedCategories();
}
: null,
style: ElevatedButton.styleFrom(
backgroundColor: _onChange ? BaseColor.primary500 : Colors.grey,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
child: Text(
'Cập nhật',
style: TextStyle(
fontSize: 16,
color: _onChange ? Colors.white : Colors.black87,
fontWeight: FontWeight.w700,
),
),
),
),
);
}
}
class _InterestCard extends StatelessWidget {
final String title;
final String? imageUrl;
final bool selected;
final VoidCallback onTap;
const _InterestCard({required this.title, required this.imageUrl, required this.selected, required this.onTap});
@override
Widget build(BuildContext context) {
final Color base = selected ? const Color(0xFFFF5A67) : const Color(0xFFFF7B85);
final Color light = selected ? const Color(0xFFFF9CA3) : const Color(0xFFFFC2C6);
return InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onTap,
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [base, light]),
boxShadow:
selected ? [BoxShadow(color: base.withOpacity(0.35), blurRadius: 12, offset: const Offset(0, 6))] : null,
),
child: Stack(
children: [
Image.asset('assets/images/bg_item_category.png', fit: BoxFit.contain),
Positioned(
bottom: 10,
left: 12,
child: Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w700),
),
),
Positioned(
top: 10,
right: 12,
child: Container(
width: 52,
height: 52,
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)),
clipBehavior: Clip.antiAlias,
child:
imageUrl?.isNotEmpty == true
? Image.network(imageUrl!, fit: BoxFit.cover)
: const Icon(Icons.category, color: Colors.black54),
),
),
// tick chọn
Positioned(
top: 10,
left: 12,
child: AnimatedScale(
duration: const Duration(milliseconds: 150),
scale: selected ? 1 : 0,
child: Container(
padding: const EdgeInsets.all(0.5),
decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle),
child: const Icon(Icons.check_circle, color: Colors.green, size: 28),
),
),
),
],
),
),
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
import 'models/interested_categories_model.dart';
class InterestedCategoriesViewModel extends RestfulApiViewModel {
var interestedCategories = Rxn<InterestedCategoriesResponse>();
Set<String> selectedIds = {};
void Function(String message)? onShowAlertError;
Future<void> getInterestedCategories() async {
showLoading();
try {
final response = await client.categoryTopLevelGetList();
hideLoading();
if (response.isSuccess && response.data != null) {
final categories = response.data!;
selectedIds =
categories.listItems
?.where((item) => item.subscribed == "1")
.map((item) => item.categoryCode ?? '')
.where((code) => code.isNotEmpty)
.toList()
.toSet() ??
<String>{};
interestedCategories.value = categories;
} else {
onShowAlertError?.call(response.message ?? Constants.commonError);
}
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
}
}
submitInterestedCategories() async {
final categories = selectedIds.toList();
showLoading();
try {
final response = await client.submitCategorySubscribe(categories.join(','));
hideLoading();
onShowAlertError?.call(
response.isSuccess ? "Cập nhật sở thích thành công" : response.message ?? Constants.commonError,
);
final List<String> categoryCodes =
interestedCategories.value?.listItems
?.map((item) => item.categoryCode ?? '')
.where((code) => code.isNotEmpty)
.toList() ??
[];
final filteredList = categoryCodes?.where((item) => !categories.contains(item)).toList();
if (filteredList == null || filteredList.isEmpty) return;
submitUnsubscribeInterestedCategories(filteredList!);
} catch (error) {
hideLoading();
onShowAlertError?.call(Constants.commonError);
}
}
submitUnsubscribeInterestedCategories(List<String> categories) async {
final _ = await client.submitCategoryUnsubscribeList(categories.join(','));
}
}
import 'package:json_annotation/json_annotation.dart';
part 'interested_categories_model.g.dart';
@JsonSerializable()
class InterestedCategoriesResponse {
@JsonKey(name: 'list_items')
final List<InterestedCategoryItem>? listItems;
InterestedCategoriesResponse({this.listItems});
factory InterestedCategoriesResponse.fromJson(Map<String, dynamic> json) =>
_$InterestedCategoriesResponseFromJson(json);
Map<String, dynamic> toJson() => _$InterestedCategoriesResponseToJson(this);
}
@JsonSerializable()
class InterestedCategoryItem {
final String? id;
final String? subscribed;
@JsonKey(name: 'category_code')
final String? categoryCode;
@JsonKey(name: 'category_name')
final String? categoryName;
@JsonKey(name: 'image_url')
final String? imageUrl;
InterestedCategoryItem({
this.id,
this.subscribed,
this.categoryCode,
this.categoryName,
this.imageUrl,
});
factory InterestedCategoryItem.fromJson(Map<String, dynamic> json) =>
_$InterestedCategoryItemFromJson(json);
Map<String, dynamic> toJson() => _$InterestedCategoryItemToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'interested_categories_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
InterestedCategoriesResponse _$InterestedCategoriesResponseFromJson(
Map<String, dynamic> json,
) => InterestedCategoriesResponse(
listItems:
(json['list_items'] as List<dynamic>?)
?.map(
(e) => InterestedCategoryItem.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
Map<String, dynamic> _$InterestedCategoriesResponseToJson(
InterestedCategoriesResponse instance,
) => <String, dynamic>{'list_items': instance.listItems};
InterestedCategoryItem _$InterestedCategoryItemFromJson(
Map<String, dynamic> json,
) => InterestedCategoryItem(
id: json['id'] as String?,
subscribed: json['subscribed'] as String?,
categoryCode: json['category_code'] as String?,
categoryName: json['category_name'] as String?,
imageUrl: json['image_url'] as String?,
);
Map<String, dynamic> _$InterestedCategoryItemToJson(
InterestedCategoryItem instance,
) => <String, dynamic>{
'id': instance.id,
'subscribed': instance.subscribed,
'category_code': instance.categoryCode,
'category_name': instance.categoryName,
'image_url': instance.imageUrl,
};
...@@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; ...@@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; 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/resouce/base_color.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: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';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../resources/base_color.dart';
import '../../shared/router_gage.dart'; import '../../shared/router_gage.dart';
import '../../widgets/custom_navigation_bar.dart'; import '../../widgets/custom_navigation_bar.dart';
import '../../widgets/image_loader.dart'; import '../../widgets/image_loader.dart';
......
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