Commit 7cf52c5d by tungnq

TODO: thêm layout sự kiện lịch và tiện ích định dạng thời gian

parent df7585d6
......@@ -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
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',
];
// 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,
};
};
......@@ -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>
......
Markdown is supported
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