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