Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
A
AppUms_Lecturer
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
tungnq
AppUms_Lecturer
Commits
b891c912
Commit
b891c912
authored
Aug 18, 2025
by
tungnq
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
TODO: Đã hoàn thiện màn hình giao diện học sinh chi tiết với checkbox theo dõi trạng thái
parent
b20d3c80
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
490 additions
and
3 deletions
+490
-3
ic_tick.png
src/assets/icon/icon_png/ic_tick.png
+0
-0
images.js
src/assets/images.js
+1
-2
CheckBox.js
src/components/CheckBox.js
+425
-0
index.js
src/screens/academic_advisor/list_student/detail/index.js
+26
-0
style.js
src/screens/academic_advisor/list_student/detail/style.js
+16
-0
view.js
src/screens/academic_advisor/list_student/detail/view.js
+22
-1
No files found.
src/assets/icon/icon_png/ic_tick.png
0 → 100644
View file @
b891c912
251 Bytes
src/assets/images.js
View file @
b891c912
const
images
=
{
iconWarn
:
require
(
'./images/iconWarn.png'
),
iconSuccess
:
require
(
'./images/iconSuccess.png'
),
...
...
@@ -60,6 +58,7 @@ const images = {
icSearchHeader
:
require
(
'./icon/icon_png/icon_search_header.png'
),
icSearch
:
require
(
'./icon/icon_png/icon_search.png'
),
icDocument
:
require
(
'./icon/icon_png/ic_document.png'
),
icTick
:
require
(
'./icon/icon_png/ic_tick.png'
),
//Image Logo
igLogo
:
require
(
'./images/logo.png'
),
...
...
src/components/CheckBox.js
0 → 100644
View file @
b891c912
import
React
,
{
useCallback
,
useMemo
,
useState
,
useEffect
}
from
'react'
;
import
{
TouchableOpacity
,
View
,
Text
,
StyleSheet
,
Animated
,
Image
}
from
'react-native'
;
import
R
from
'../assets/R'
;
const
IconTick
=
R
.
images
.
icTick
;
// COMPONENT: TickIcon - Hiển thị biểu tượng tick với animation
const
TickIcon
=
React
.
memo
(({
visible
,
size
,
color
})
=>
{
// PERFORMANCE: Chỉ tạo animatedValue một lần khi component mount
const
animatedValue
=
useMemo
(()
=>
new
Animated
.
Value
(
visible
?
1
:
0
),
[]);
// FUNCTIONALITY: Animation spring khi visible thay đổi
useEffect
(()
=>
{
const
animation
=
Animated
.
spring
(
animatedValue
,
{
toValue
:
visible
?
1
:
0
,
useNativeDriver
:
true
,
// PERFORMANCE: Sử dụng native driver để tối ưu animation
tension
:
300
,
friction
:
10
,
});
animation
.
start
();
// CLEANUP: Dọn dẹp animation khi component unmount
return
()
=>
{
animation
.
stop
();
};
},
[
visible
,
animatedValue
]);
// UI/UX: Style cho image tick được tính toán một lần
const
imageStyle
=
useMemo
(()
=>
({
width
:
size
,
height
:
size
,
tintColor
:
R
.
colors
.
white
,
backgroundColor
:
R
.
colors
.
black
,
borderRadius
:
5
}),
[
size
,
color
]);
return
(
<
Animated
.
View
style
=
{{
transform
:
[{
scale
:
animatedValue
}],
opacity
:
animatedValue
,
justifyContent
:
'center'
,
alignItems
:
'center'
,
}}
>
<
Image
source
=
{
IconTick
}
style
=
{
imageStyle
}
resizeMode
=
"contain"
/>
<
/Animated.View
>
);
});
// COMPONENT: Checkbox - Main component theo cấu trúc Button mới
const
Checkbox
=
({
// STATE: Checkbox value controls
value
,
// Controlled component value
defaultValue
=
false
,
// Uncontrolled component initial value
isCheck
=
false
,
// FEATURE: Mặc định bật tick ngay khi component mount
onValueChange
,
// FUNCTIONALITY: Custom logic handlers
onPress
,
// FEATURE: Custom onPress handler để thực thi logic riêng
preventToggleOnPress
=
false
,
// FEATURE: Ngăn toggle tự động khi có custom onPress
// UI/UX: Checkbox appearance
size
=
30
,
borderRadius
=
4
,
borderColor
=
R
.
colors
.
black
,
borderWidth
=
1
,
checkedColor
=
R
.
colors
.
main
,
uncheckedColor
=
'transparent'
,
tickColor
=
R
.
colors
.
white
,
// FUNCTIONALITY: Label configuration
label
,
labelPosition
=
'right'
,
// 'left' | 'right'
labelStyle
,
labelSize
=
16
,
// FEATURE: Kích thước font cho label
labelColor
,
// FEATURE: Màu sắc label (override default)
labelWeight
=
'normal'
,
// FEATURE: Font weight cho label ('normal', 'bold', '100', '200', ... '900')
labelOpacity
,
// FEATURE: Độ trong suốt label (override default)
labelSpacing
=
5
,
// Khoảng cách giữa checkbox và label
labelNumberOfLines
=
0
,
// FEATURE: Số dòng tối đa cho label (0 = unlimited)
labelEllipsizeMode
=
'tail'
,
// FEATURE: Cách cắt text khi quá dài ('head', 'middle', 'tail', 'clip')
labelLineHeight
,
// FEATURE: Chiều cao dòng cho label
labelLetterSpacing
,
// FEATURE: Khoảng cách giữa các ký tự
labelTextAlign
=
'left'
,
// FEATURE: Căn chỉnh text ('left', 'center', 'right')
labelTextDecorationLine
,
// FEATURE: Gạch chân/gạch ngang ('none', 'underline', 'line-through', 'underline line-through')
labelTextTransform
,
// FEATURE: Chuyển đổi chữ ('none', 'uppercase', 'lowercase', 'capitalize')
labelFontFamily
,
// FEATURE: Font family cho label
labelFontStyle
=
'normal'
,
// FEATURE: Font style ('normal', 'italic')
labelIncludeFontPadding
=
true
,
// FEATURE: Bao gồm font padding (Android only)
labelTextAlignVertical
=
'auto'
,
// FEATURE: Căn chỉnh vertical ('auto', 'top', 'bottom', 'center') - Android only
// UI/UX: Margin configuration
margin
,
// FEATURE: Margin cho toàn bộ component
marginTop
,
// FEATURE: Margin top riêng biệt
marginBottom
,
// FEATURE: Margin bottom riêng biệt
marginLeft
,
// FEATURE: Margin left riêng biệt
marginRight
,
// FEATURE: Margin right riêng biệt
marginHorizontal
,
// FEATURE: Margin horizontal (left + right)
marginVertical
,
// FEATURE: Margin vertical (top + bottom)
// STATE: Component state
disabled
=
false
,
style
,
testID
,
})
=>
{
// STATE: Internal state cho uncontrolled component
// FEATURE: Ưu tiên isCheck > defaultValue để tự động bật tick
const
[
internalValue
,
setInternalValue
]
=
useState
(
isCheck
||
defaultValue
);
// FUNCTIONALITY: Xác định component là controlled hay uncontrolled
const
isControlled
=
value
!==
undefined
;
const
currentValue
=
isControlled
?
value
:
internalValue
;
// FEATURE: Effect để xử lý isCheck khi component mount hoặc isCheck thay đổi
useEffect
(()
=>
{
// Chỉ áp dụng cho uncontrolled component
if
(
!
isControlled
&&
isCheck
!==
internalValue
)
{
setInternalValue
(
isCheck
);
// FUNCTIONALITY: Trigger callback nếu có khi isCheck thay đổi
if
(
onValueChange
&&
isCheck
!==
internalValue
)
{
onValueChange
(
isCheck
);
}
}
},
[
isCheck
,
isControlled
,
internalValue
,
onValueChange
]);
// FUNCTIONALITY: Xử lý toggle checkbox - Đơn giản hóa logic
const
handleToggle
=
useCallback
(()
=>
{
if
(
disabled
)
{
return
;
}
const
newValue
=
!
currentValue
;
// STATE: Cập nhật internal state nếu uncontrolled
if
(
!
isControlled
)
{
setInternalValue
(
newValue
);
}
// FUNCTIONALITY: Gọi callback nếu có - Wrap trong requestAnimationFrame để tránh conflict
if
(
onValueChange
)
{
requestAnimationFrame
(()
=>
{
onValueChange
(
newValue
);
});
}
},
[
currentValue
,
disabled
,
isControlled
,
onValueChange
]);
// FUNCTIONALITY: Xử lý press event - Tối ưu để tránh multiple calls
const
handlePress
=
useCallback
(()
=>
{
if
(
disabled
)
{
return
;
}
// PERFORMANCE: Debounce để tránh multiple rapid taps
const
now
=
Date
.
now
();
if
(
handlePress
.
lastCall
&&
(
now
-
handlePress
.
lastCall
)
<
100
)
{
return
;
}
handlePress
.
lastCall
=
now
;
// FEATURE: Nếu có custom onPress handler
if
(
onPress
)
{
const
checkboxInfo
=
{
currentValue
,
isControlled
,
toggleCheckbox
:
handleToggle
,
};
try
{
// FUNCTIONALITY: Thực thi custom logic
onPress
(
checkboxInfo
);
// FUNCTIONALITY: Chỉ auto-toggle khi không bị prevent
if
(
!
preventToggleOnPress
)
{
handleToggle
();
}
}
catch
(
error
)
{
console
.
error
(
'Checkbox onPress error:'
,
error
);
}
return
;
}
// FUNCTIONALITY: Không có custom onPress, toggle bình thường
handleToggle
();
},
[
disabled
,
onPress
,
preventToggleOnPress
,
currentValue
,
isControlled
,
handleToggle
]);
// UI/UX: Tính toán margin styles với priority
const
marginStyles
=
useMemo
(()
=>
{
const
styles
=
{};
// FEATURE: Áp dụng margin chung trước
if
(
margin
!==
undefined
)
{
styles
.
margin
=
margin
;
}
// FEATURE: Áp dụng marginHorizontal và marginVertical
if
(
marginHorizontal
!==
undefined
)
{
styles
.
marginHorizontal
=
marginHorizontal
;
}
if
(
marginVertical
!==
undefined
)
{
styles
.
marginVertical
=
marginVertical
;
}
// FEATURE: Áp dụng margin riêng biệt (có priority cao nhất)
if
(
marginTop
!==
undefined
)
{
styles
.
marginTop
=
marginTop
;
}
if
(
marginBottom
!==
undefined
)
{
styles
.
marginBottom
=
marginBottom
;
}
if
(
marginLeft
!==
undefined
)
{
styles
.
marginLeft
=
marginLeft
;
}
if
(
marginRight
!==
undefined
)
{
styles
.
marginRight
=
marginRight
;
}
return
styles
;
},
[
margin
,
marginTop
,
marginBottom
,
marginLeft
,
marginRight
,
marginHorizontal
,
marginVertical
]);
// UI/UX: Style cho checkbox được memoize để tối ưu performance
const
checkboxStyle
=
useMemo
(()
=>
({
width
:
size
,
height
:
size
,
borderRadius
,
borderWidth
,
borderColor
:
currentValue
?
checkedColor
:
borderColor
,
backgroundColor
:
currentValue
?
checkedColor
:
uncheckedColor
,
opacity
:
disabled
?
0.5
:
1
,
justifyContent
:
'center'
,
alignItems
:
'center'
,
}),
[
size
,
borderRadius
,
borderWidth
,
borderColor
,
currentValue
,
checkedColor
,
uncheckedColor
,
disabled
,
]);
// UI/UX: Style cho label với các thuộc tính mở rộng
const
defaultLabelStyle
=
useMemo
(()
=>
{
const
baseStyle
=
{
fontSize
:
labelSize
,
fontWeight
:
labelWeight
,
fontStyle
:
labelFontStyle
,
textAlign
:
labelTextAlign
,
includeFontPadding
:
labelIncludeFontPadding
,
textAlignVertical
:
labelTextAlignVertical
,
};
// FEATURE: Màu sắc label với logic fallback
if
(
labelColor
)
{
baseStyle
.
color
=
labelColor
;
}
else
{
baseStyle
.
color
=
disabled
?
R
.
colors
.
gray
:
R
.
colors
.
black
;
}
// FEATURE: Opacity label với logic fallback
if
(
labelOpacity
!==
undefined
)
{
baseStyle
.
opacity
=
labelOpacity
;
}
else
{
baseStyle
.
opacity
=
disabled
?
0.5
:
1
;
}
// FEATURE: Các thuộc tính typography tùy chọn
if
(
labelLineHeight
)
{
baseStyle
.
lineHeight
=
labelLineHeight
;
}
if
(
labelLetterSpacing
)
{
baseStyle
.
letterSpacing
=
labelLetterSpacing
;
}
if
(
labelTextDecorationLine
)
{
baseStyle
.
textDecorationLine
=
labelTextDecorationLine
;
}
if
(
labelTextTransform
)
{
baseStyle
.
textTransform
=
labelTextTransform
;
}
if
(
labelFontFamily
)
{
baseStyle
.
fontFamily
=
labelFontFamily
;
}
return
baseStyle
;
},
[
labelSize
,
labelColor
,
labelWeight
,
labelOpacity
,
labelLineHeight
,
labelLetterSpacing
,
labelTextAlign
,
labelTextDecorationLine
,
labelTextTransform
,
labelFontFamily
,
labelFontStyle
,
labelIncludeFontPadding
,
labelTextAlignVertical
,
disabled
]);
// UI/UX: Style cho container chính với margin
const
containerStyle
=
useMemo
(()
=>
({
flexDirection
:
labelPosition
===
'right'
?
'row'
:
'row-reverse'
,
alignItems
:
'center'
,
...
marginStyles
,
// FEATURE: Áp dụng margin styles
}),
[
labelPosition
,
marginStyles
]);
// UI/UX: Style cho spacing giữa checkbox và label
const
spacingStyle
=
useMemo
(()
=>
{
if
(
!
label
)
return
{};
return
labelPosition
===
'right'
?
{
marginLeft
:
labelSpacing
}
:
{
marginRight
:
labelSpacing
};
},
[
label
,
labelPosition
,
labelSpacing
]);
// FUNCTIONALITY: Render checkbox element
const
renderCheckbox
=
()
=>
(
<
View
style
=
{
checkboxStyle
}
>
{
currentValue
&&
(
<
TickIcon
visible
=
{
currentValue
}
size
=
{
size
}
color
=
{
tickColor
}
/
>
)}
<
/View
>
);
// FUNCTIONALITY: Render label element với các thuộc tính mở rộng
const
renderLabel
=
()
=>
{
if
(
!
label
)
return
null
;
return
(
<
Text
style
=
{[
defaultLabelStyle
,
labelStyle
,
spacingStyle
]}
numberOfLines
=
{
labelNumberOfLines
}
ellipsizeMode
=
{
labelEllipsizeMode
}
allowFontScaling
=
{
true
}
// FEATURE: Cho phép scale font theo system settings
adjustsFontSizeToFit
=
{
false
}
// FEATURE: Không auto-adjust font size
minimumFontScale
=
{
0.01
}
// FEATURE: Minimum scale factor khi adjustsFontSizeToFit = true
suppressHighlighting
=
{
true
}
// FEATURE: Ngăn highlight khi press (iOS)
selectable
=
{
false
}
// FEATURE: Không cho phép select text
textBreakStrategy
=
"simple"
// FEATURE: Text break strategy (Android)
>
{
label
}
<
/Text
>
);
};
// UI/UX: Nếu không có label, render checkbox đơn giản với margin
if
(
!
label
)
{
return
(
<
TouchableOpacity
style
=
{[
checkboxStyle
,
marginStyles
,
style
]}
// FEATURE: Áp dụng marginStyles
onPress
=
{
handlePress
}
// FEATURE: Sử dụng handlePress thay vì handleToggle
disabled
=
{
disabled
}
activeOpacity
=
{
0.8
}
testID
=
{
testID
}
accessible
=
{
true
}
accessibilityRole
=
"checkbox"
accessibilityState
=
{{
checked
:
currentValue
,
disabled
}}
accessibilityHint
=
"Tap to toggle checkbox"
>
{
currentValue
&&
(
<
TickIcon
visible
=
{
currentValue
}
size
=
{
size
}
color
=
{
tickColor
}
/
>
)}
<
/TouchableOpacity
>
);
}
// UI/UX: Render checkbox với label và margin
return
(
<
TouchableOpacity
style
=
{[
containerStyle
,
style
]}
// marginStyles đã được merge vào containerStyle
onPress
=
{
handlePress
}
// FEATURE: Sử dụng handlePress thay vì handleToggle
disabled
=
{
disabled
}
activeOpacity
=
{
0.8
}
testID
=
{
testID
}
accessible
=
{
true
}
accessibilityRole
=
"checkbox"
accessibilityState
=
{{
checked
:
currentValue
,
disabled
}}
accessibilityLabel
=
{
typeof
label
===
'string'
?
label
:
'Checkbox'
}
accessibilityHint
=
"Tap to toggle checkbox"
>
{
renderCheckbox
()}
{
renderLabel
()}
<
/TouchableOpacity
>
);
};
export
default
React
.
memo
(
Checkbox
);
\ No newline at end of file
src/screens/academic_advisor/list_student/detail/index.js
View file @
b891c912
...
...
@@ -36,9 +36,35 @@ const DetailStudent = (props) => {
},
]
});
const
handleCheckboxChange
=
(
itemId
,
newValue
)
=>
{
setStudent
(
prevStudent
=>
{
// Sao chép danh sách type cũ
const
updatedTypes
=
prevStudent
.
type
.
map
(
item
=>
{
if
(
item
.
id
===
itemId
)
{
// Nếu id trùng -> tạo object mới với status cập nhật
return
{
...
item
,
status
:
newValue
,
};
}
else
{
// Nếu không trùng -> giữ nguyên item
return
item
;
}
});
// Trả về student mới với type đã cập nhật
return
{
...
prevStudent
,
type
:
updatedTypes
,
};
});
};
return
(
<
DetailStudentView
student
=
{
student
}
onCheckboxChange
=
{
handleCheckboxChange
}
/
>
);
};
...
...
src/screens/academic_advisor/list_student/detail/style.js
View file @
b891c912
...
...
@@ -59,6 +59,22 @@ const styles = StyleSheet.create({
color
:
R
.
colors
.
blue
,
marginHorizontal
:
15
,
},
label
:{
fontSize
:
R
.
sizes
.
sm
,
fontFamily
:
R
.
fonts
.
fontMedium
,
fontWeight
:
'600'
,
color
:
R
.
colors
.
black
,
},
checkboxContainer
:
{
flexDirection
:
'row'
,
flexWrap
:
'wrap'
,
justifyContent
:
'space-between'
,
},
checkboxItem
:
{
width
:
'50%'
,
marginBottom
:
15
,
},
})
export
default
styles
src/screens/academic_advisor/list_student/detail/view.js
View file @
b891c912
...
...
@@ -12,8 +12,9 @@ import styles from './style';
import
*
as
SCREENNAME
from
'../../../../routers/ScreenNames'
;
import
{
useNavigation
}
from
'@react-navigation/native'
;
import
VersionCheck
from
'../../../../components/VersionCheck'
;
import
CheckBox
from
'../../../../components/CheckBox'
;
const
DetailStudentView
=
props
=>
{
const
{
student
}
=
props
;
const
{
student
,
onCheckboxChange
}
=
props
;
const
navigate
=
useNavigation
();
return
(
<
SafeAreaView
style
=
{
styles
.
container
}
>
...
...
@@ -116,6 +117,26 @@ const DetailStudentView = props => {
<
/Text
>
<
/View
>
<
/View
>
{
/*Row 6*/
}
<
View
style
=
{
styles
.
checkboxContainer
}
>
{
student
.
type
.
map
((
item
,
index
)
=>
(
<
View
key
=
{
item
.
id
}
style
=
{
styles
.
checkboxItem
}
>
<
CheckBox
value
=
{
item
.
status
}
onValueChange
=
{
value
=>
onCheckboxChange
(
item
.
id
,
value
)}
size
=
{
20
}
labelStyle
=
{
styles
.
label
}
checkedColor
=
{
R
.
colors
.
main
}
tickColor
=
{
R
.
colors
.
white
}
label
=
{
item
.
name
}
imageStyle
=
{
styles
.
imageStyle
}
/
>
<
/View
>
))}
<
/View
>
<
/View
>
<
Text
style
=
{
styles
.
textTitle
}
>
L
ị
ch
s
ử
t
ì
nh
tr
ạ
ng
c
ủ
a
sinh
vi
ê
n
<
/Text
>
...
...
Write
Preview
Markdown
is supported
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