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
7cf52c5d
Commit
7cf52c5d
authored
Sep 12, 2025
by
tungnq
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
TODO: thêm layout sự kiện lịch và tiện ích định dạng thời gian
parent
df7585d6
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
243 additions
and
173 deletions
+243
-173
Functions.js
src/config/Functions.js
+106
-0
constants.js
src/config/constants.js
+10
-170
useFilterDay.js
src/hooks/useFilterDay.js
+104
-0
index.js
src/screens/class_schedule/filterday/index.js
+0
-0
view.js
src/screens/class_schedule/filterday/view.js
+23
-3
No files found.
src/config/Functions.js
View file @
7cf52c5d
...
...
@@ -605,3 +605,108 @@ export const getMimeType = fileExt => {
return
'*/*'
;
}
};
//Calendar
export
const
parseMinutes
=
(
timeStr
)
=>
{
if
(
!
timeStr
||
typeof
timeStr
!==
'string'
)
return
0
;
const
parts
=
timeStr
.
split
(
':'
);
if
(
parts
.
length
!==
2
)
return
0
;
const
[
h
,
m
]
=
parts
.
map
(
Number
);
if
(
isNaN
(
h
)
||
isNaN
(
m
))
return
0
;
return
h
*
60
+
m
;
};
export
const
formatDateToString
=
(
date
)
=>
{
const
y
=
date
.
getFullYear
();
const
m
=
String
(
date
.
getMonth
()
+
1
).
padStart
(
2
,
'0'
);
const
d
=
String
(
date
.
getDate
()).
padStart
(
2
,
'0'
);
return
`
${
y
}
-
${
m
}
-
${
d
}
`
;
};
export
const
isSameDay
=
(
a
,
b
)
=>
a
.
getFullYear
()
===
b
.
getFullYear
()
&&
a
.
getMonth
()
===
b
.
getMonth
()
&&
a
.
getDate
()
===
b
.
getDate
();
/**
* Trả về mảng events đã có toạ độ:
* { topPosition, height, leftOffset, rightOffset, zIndex, columnIndex, numColumns }
*/
export
const
layoutDayEvents
=
(
events
,
hourHeight
=
80
)
=>
{
if
(
!
events
||
events
.
length
===
0
)
return
[];
// Chuẩn hoá + sort theo start
const
mapped
=
events
.
filter
(
e
=>
e
?.
time
&&
e
?.
endTime
)
.
map
((
e
,
i
)
=>
({
...
e
,
start
:
parseMinutes
(
e
.
time
),
end
:
parseMinutes
(
e
.
endTime
),
_idx
:
i
,
}))
.
filter
(
e
=>
e
.
end
>
e
.
start
)
.
sort
((
a
,
b
)
=>
a
.
start
-
b
.
start
);
// Gom nhóm overlap tuyến tính
const
groups
=
[];
let
cur
=
null
;
for
(
const
ev
of
mapped
)
{
if
(
!
cur
||
ev
.
start
>=
cur
.
maxEnd
)
{
cur
=
{
id
:
groups
.
length
,
items
:
[
ev
],
maxEnd
:
ev
.
end
};
groups
.
push
(
cur
);
}
else
{
cur
.
items
.
push
(
ev
);
if
(
ev
.
end
>
cur
.
maxEnd
)
cur
.
maxEnd
=
ev
.
end
;
}
}
const
out
=
[];
// Gán cột theo greedy interval partitioning
for
(
const
g
of
groups
)
{
const
colEnds
=
[];
// end time cuối mỗi cột
const
colOf
=
{};
// event.id -> columnIndex
const
items
=
[...
g
.
items
].
sort
((
a
,
b
)
=>
a
.
start
-
b
.
start
);
for
(
const
ev
of
items
)
{
let
placed
=
false
;
for
(
let
c
=
0
;
c
<
colEnds
.
length
;
c
++
)
{
if
(
ev
.
start
>=
colEnds
[
c
])
{
colEnds
[
c
]
=
ev
.
end
;
colOf
[
ev
.
id
]
=
c
;
placed
=
true
;
break
;
}
}
if
(
!
placed
)
{
colOf
[
ev
.
id
]
=
colEnds
.
length
;
colEnds
.
push
(
ev
.
end
);
}
}
const
numCols
=
Math
.
max
(
colEnds
.
length
,
1
);
for
(
const
ev
of
items
)
{
const
columnIndex
=
colOf
[
ev
.
id
]
??
0
;
const
topPosition
=
(
ev
.
start
/
60
)
*
hourHeight
;
const
height
=
((
ev
.
end
-
ev
.
start
)
/
60
)
*
hourHeight
;
const
widthPct
=
100
/
numCols
;
const
leftPct
=
columnIndex
*
widthPct
;
out
.
push
({
...
ev
,
topPosition
,
height
,
leftOffset
:
`
${
Math
.
round
(
leftPct
*
100
)
/
100
}
%`
,
rightOffset
:
`
${
Math
.
round
((
100
-
(
leftPct
+
widthPct
))
*
100
)
/
100
}
%`
,
widthPct
,
columnIndex
,
numColumns
:
numCols
,
zIndex
:
10
+
columnIndex
,
});
}
}
// Trả về theo thứ tự ban đầu để render ổn định
return
out
.
sort
((
a
,
b
)
=>
a
.
_idx
-
b
.
_idx
);
};
\ No newline at end of file
src/config/constants.js
View file @
7cf52c5d
import
i18n
from
"../helper/i18/i18n"
;
import
R
from
"../assets/R"
;
export
const
HISTORY_STATUS
=
{
ALL
:
{
code
:
"ALL"
,
color
:
R
.
colors
.
secondary
,
icon
:
R
.
images
.
icCompleted
,
name
:
i18n
.
t
(
"All"
),
status
:
"-1"
,
},
COMPLETED
:
{
code
:
"COMPLETED"
,
color
:
R
.
colors
.
secondary
,
icon
:
R
.
images
.
icCompleted
,
name
:
i18n
.
t
(
"Completed"
),
status
:
"3"
,
},
IN_PROCESSING
:
{
code
:
"IN_PROCESSING"
,
color
:
R
.
colors
.
lightBlue
,
icon
:
R
.
images
.
icInfo
,
name
:
i18n
.
t
(
"InProcessing"
),
status
:
"4"
,
},
FAILED
:
{
code
:
"FAILED"
,
color
:
R
.
colors
.
red1
,
icon
:
R
.
images
.
icFailed
,
name
:
i18n
.
t
(
"Failed"
),
status
:
"7"
,
},
WATTING
:
{
code
:
"TRANS_WAIT_APPROVED"
,
color
:
R
.
colors
.
orange
,
icon
:
R
.
images
.
icInfo
,
name
:
i18n
.
t
(
"InProcessing"
),
status
:
"4"
,
},
};
export
const
SEX
=
{
MALE
:
0
,
FEMALE
:
1
,
};
export
const
TRANSACTION_TYPE
=
{
ALL
:
{
code
:
"ALL"
,
color
:
R
.
colors
.
secondary
,
backgroundButton
:
R
.
images
.
bgDepositButton
,
name
:
i18n
.
t
(
"All"
),
transferType
:
-
1
,
},
DEPOSIT
:
{
code
:
"PUSH"
,
color
:
R
.
colors
.
secondary
,
backgroundButton
:
R
.
images
.
bgDepositButton
,
name
:
i18n
.
t
(
"Deposit"
),
transferType
:
2
,
transferTypeTxt
:
"PUSH"
,
},
WITHDRAW
:
{
code
:
"PULL"
,
color
:
R
.
colors
.
red1
,
backgroundButton
:
R
.
images
.
bgWithdrawButton
,
name
:
i18n
.
t
(
"Withdraw"
),
transferType
:
1
,
transferTypeTxt
:
"PULL"
,
},
BORROW_REQUEST
:
{
code
:
"BORROW_REQUEST"
,
color
:
R
.
colors
.
gray1
,
backgroundButton
:
R
.
images
.
bgBorrowRequest
,
name
:
i18n
.
t
(
"BorrowRequest"
),
transferType
:
3
,
transferTypeTxt
:
"3"
,
},
};
export
const
ACCOUNT_BANK_TYPE
=
{
BANK
:
"Bank"
,
CREDIT
:
"Credit"
,
};
export
const
CELL_COUNT
=
4
;
export
const
SHARE_TYPE
=
{
ALL
:
1
,
FACEBOOK
:
2
,
TWITTER
:
3
,
};
export
const
RATINGS_TYPE
=
{
REFER_FRIEND
:
1
,
BENEFIT
:
2
,
};
export
const
LANGUAGE_LIST
=
[
{
id
:
56
,
name
:
i18n
.
t
(
"Vietnamese"
),
value
:
"vi"
,
code
:
"vi"
,
},
{
id
:
57
,
name
:
i18n
.
t
(
"English"
),
value
:
"en"
,
code
:
"en"
,
},
];
export
const
ASYNC_STORE_KEY
=
{
TOKEN
:
"@TOKEN"
,
...
...
@@ -112,73 +9,6 @@ export const ASYNC_STORE_KEY = {
LANGUAGE
:
"@LANGUAGE"
,
};
export
const
OTP_TYPE
=
{
CHECK_PHONE_NUMBER
:
0
,
FORGOT_PASSWORD
:
1
,
};
export
const
PROVINCE_LIST
=
[
{
id
:
1
,
code
:
"AG"
,
name
:
"An Giang"
,
},
{
id
:
2
,
code
:
"BR_VT"
,
name
:
"Bà Rịa - Vũng Tàu"
,
},
{
id
:
3
,
code
:
"BL"
,
name
:
"Bạc Liêu"
,
},
{
id
:
4
,
code
:
"BK"
,
name
:
"Bắc Kạn"
,
},
{
id
:
5
,
code
:
"BC"
,
name
:
"Bắc Giang"
,
},
{
id
:
6
,
code
:
"BN"
,
name
:
"Bắc Ninh"
,
},
{
id
:
7
,
code
:
"BT"
,
name
:
"Bến Tre"
,
},
{
id
:
8
,
code
:
"BD"
,
name
:
"Bình Dương"
,
},
{
id
:
9
,
code
:
"BD"
,
name
:
"Bình Định"
,
},
{
id
:
10
,
code
:
"BP"
,
name
:
"Bình Phước"
,
},
{
id
:
11
,
code
:
"HN"
,
name
:
"Hà Nội"
,
},
{
id
:
12
,
code
:
"HCM"
,
name
:
"Hồ Chí Minh"
,
},
];
export
const
DEVICE_EVENT_KEY
=
{
RELOAD_BALANCE_WALLET
:
"reloadBalanceWallet"
,
LOGOUT_EVENT
:
"logoutEvent"
,
...
...
@@ -186,3 +16,13 @@ export const DEVICE_EVENT_KEY = {
export
const
BACKSPACE
=
'Backspace'
;
export
const
DELIMITERS
=
[
','
,
';'
,
' '
];
//Calendar
export
const
HOUR_HEIGHT
=
80
;
export
const
DAYS_SHORT
=
[
'CN'
,
'T2'
,
'T3'
,
'T4'
,
'T5'
,
'T6'
,
'T7'
];
export
const
MONTHS_VI
=
[
'Tháng 1'
,
'Tháng 2'
,
'Tháng 3'
,
'Tháng 4'
,
'Tháng 5'
,
'Tháng 6'
,
'Tháng 7'
,
'Tháng 8'
,
'Tháng 9'
,
'Tháng 10'
,
'Tháng 11'
,
'Tháng 12'
,
];
src/hooks/useFilterDay.js
0 → 100644
View file @
7cf52c5d
// hooks/useFilterDay.js
import
{
useState
,
useRef
,
useEffect
,
useCallback
}
from
'react'
;
import
{
DeviceEventEmitter
,
PanResponder
}
from
'react-native'
;
import
{
useFocusEffect
}
from
'@react-navigation/native'
;
import
{
HOUR_HEIGHT
,
DAYS_SHORT
,
MONTHS_VI
}
from
'../config/constants'
;
import
{
formatDateToString
,
layoutDayEvents
}
from
'../config/Functions'
;
export
const
useFilterDay
=
(
initialEvents
)
=>
{
const
[
currentDate
,
setCurrentDate
]
=
useState
(
new
Date
());
const
[
selectedDate
,
setSelectedDate
]
=
useState
(
new
Date
());
const
[
showMonthPicker
,
setShowMonthPicker
]
=
useState
(
false
);
const
scrollViewRef
=
useRef
(
null
);
useEffect
(()
=>
{
DeviceEventEmitter
.
emit
(
'onDateChange'
,
selectedDate
);
},
[
selectedDate
]);
useFocusEffect
(
useCallback
(()
=>
{
const
today
=
new
Date
();
setCurrentDate
(
today
);
setSelectedDate
(
today
);
DeviceEventEmitter
.
emit
(
'onDateChange'
,
today
);
DeviceEventEmitter
.
emit
(
'updateHeaderMonth'
,
today
.
getMonth
());
},
[])
);
const
getEventsForDate
=
useCallback
((
date
)
=>
{
const
dateStr
=
formatDateToString
(
date
);
return
initialEvents
.
filter
(
e
=>
e
.
date
===
dateStr
);
},
[
initialEvents
]);
const
getDayName
=
(
date
)
=>
DAYS_SHORT
[
date
.
getDay
()];
const
getMonthName
=
(
i
)
=>
MONTHS_VI
[
i
];
const
handleMonthSelect
=
(
monthIndex
)
=>
{
const
newDate
=
new
Date
(
currentDate
);
newDate
.
setMonth
(
monthIndex
);
setCurrentDate
(
newDate
);
setSelectedDate
(
newDate
);
setShowMonthPicker
(
false
);
DeviceEventEmitter
.
emit
(
'onDateChange'
,
newDate
);
DeviceEventEmitter
.
emit
(
'updateHeaderMonth'
,
monthIndex
);
};
const
setDay
=
(
d
)
=>
{
setSelectedDate
(
d
);
setCurrentDate
(
d
);
DeviceEventEmitter
.
emit
(
'onDateChange'
,
d
);
DeviceEventEmitter
.
emit
(
'updateHeaderMonth'
,
d
.
getMonth
());
};
const
swipeToNextDay
=
()
=>
{
const
next
=
new
Date
(
selectedDate
);
next
.
setDate
(
selectedDate
.
getDate
()
+
1
);
setDay
(
next
);
};
const
swipeToPrevDay
=
()
=>
{
const
prev
=
new
Date
(
selectedDate
);
prev
.
setDate
(
selectedDate
.
getDate
()
-
1
);
setDay
(
prev
);
};
const
toggleMonthPicker
=
()
=>
setShowMonthPicker
(
v
=>
!
v
);
const
panResponder
=
PanResponder
.
create
({
onMoveShouldSetPanResponder
:
(
_
,
g
)
=>
Math
.
abs
(
g
.
dx
)
>
30
&&
Math
.
abs
(
g
.
dy
)
<
100
,
onPanResponderRelease
:
(
_
,
g
)
=>
{
if
(
g
.
dx
>
50
)
swipeToPrevDay
();
else
if
(
g
.
dx
<
-
50
)
swipeToNextDay
();
},
});
// Adapter cho view cũ
const
calculateEventPosition
=
(
start
,
end
)
=>
{
const
[
sh
,
sm
]
=
start
.
split
(
':'
).
map
(
Number
);
const
[
eh
,
em
]
=
end
.
split
(
':'
).
map
(
Number
);
const
startMin
=
sh
*
60
+
sm
;
const
endMin
=
eh
*
60
+
em
;
const
topPosition
=
(
startMin
/
60
)
*
HOUR_HEIGHT
;
const
height
=
((
endMin
-
startMin
)
/
60
)
*
HOUR_HEIGHT
;
return
{
topPosition
,
height
};
};
const
calculateEventLayout
=
useCallback
((
events
)
=>
{
return
layoutDayEvents
(
events
,
HOUR_HEIGHT
);
},
[]);
return
{
// state
currentDate
,
selectedDate
,
showMonthPicker
,
scrollViewRef
,
// getters
getEventsForDate
,
getDayName
,
getMonthName
,
// actions
handleMonthSelect
,
toggleMonthPicker
,
swipeToNextDay
,
swipeToPrevDay
,
// gestures
panResponder
,
// layout
calculateEventPosition
,
calculateEventLayout
,
};
};
src/screens/class_schedule/filterday/index.js
View file @
7cf52c5d
This diff is collapsed.
Click to expand it.
src/screens/class_schedule/filterday/view.js
View file @
7cf52c5d
...
...
@@ -88,20 +88,40 @@ const FilterDayView = props => {
right
:
event
.
rightOffset
,
zIndex
:
event
.
zIndex
,
backgroundColor
:
R
.
colors
.
blue
,
// Add minimum width to prevent events from being too narrow
minWidth
:
event
.
numColumns
>
3
?
60
:
undefined
,
},
]}
activeOpacity
=
{
0.7
}
>
<
Text
style
=
{
styles
.
eventTitle
}
style
=
{[
styles
.
eventTitle
,
{
fontSize
:
event
.
numColumns
>
2
?
10
:
event
.
numColumns
>
1
?
12
:
14
,
}
]}
numberOfLines
=
{
event
.
height
>
60
?
2
:
1
}
>
{
event
.
title
}
<
/Text
>
{
event
.
height
>
40
&&
(
<
Text
style
=
{
styles
.
eventSubtitle
}
numberOfLines
=
{
1
}
>
<
Text
style
=
{[
styles
.
eventSubtitle
,
{
fontSize
:
event
.
numColumns
>
2
?
9
:
event
.
numColumns
>
1
?
11
:
13
,
}
]}
numberOfLines
=
{
1
}
>
{
event
.
subtitle
}
<
/Text
>
)}
<
Text
style
=
{
styles
.
eventTime
}
>
<
Text
style
=
{[
styles
.
eventTime
,
{
fontSize
:
event
.
numColumns
>
2
?
8
:
event
.
numColumns
>
1
?
10
:
12
,
}
]}
>
{
event
.
time
}
-
{
event
.
endTime
}
<
/Text
>
<
/TouchableOpacity
>
...
...
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