@@ -284,7 +284,7 @@ export default function SignUpForm() {
0 &&
- !isValidPassword(formData.password)
+ !isValidPassword(formData.password)
? "opacity-50 cursor-not-allowed"
: ""
}
diff --git a/src/components/common/CustomDateRangePicker.tsx b/src/components/common/CustomDateRangePicker.tsx
new file mode 100644
index 0000000..fd2c6d1
--- /dev/null
+++ b/src/components/common/CustomDateRangePicker.tsx
@@ -0,0 +1,328 @@
+"use client";
+
+import { useState, useRef, useEffect, useMemo } from "react";
+
+interface CustomDateRangePickerProps {
+ onFilterChange: (
+ startDate: string,
+ endDate: string,
+ startTime: string,
+ endTime: string
+ ) => void;
+}
+
+const DAYS_OF_WEEK = ["Th2", "Th3", "Th4", "Th5", "Th6", "Th7", "CN"];
+const MONTHS = [
+ "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"
+];
+
+const getStartOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
+const formatDateToString = (date: Date | null) => {
+ if (!date) return "";
+ const d = date.getDate().toString().padStart(2, "0");
+ const m = (date.getMonth() + 1).toString().padStart(2, "0");
+ const y = date.getFullYear();
+ return `${y}-${m}-${d}`;
+};
+
+export default function CustomDateRangePicker({ onFilterChange }: CustomDateRangePickerProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const containerRef = useRef
(null);
+
+ const [currentMonthDate, setCurrentMonthDate] = useState(getStartOfDay(new Date()));
+
+ // LOGIC MỚI: Tách biệt rõ ràng state Từ ngày và Đến ngày
+ const [startTimestamp, setStartTimestamp] = useState(null);
+ const [endTimestamp, setEndTimestamp] = useState(null);
+
+ // History dùng để theo dõi thứ tự click, phục vụ "Cửa sổ trượt" giữ 2 ngày gần nhất
+ const [history, setHistory] = useState([]);
+
+ const [startTime, setStartTime] = useState("00:00");
+ const [endTime, setEndTime] = useState("23:59");
+
+ const startDate = startTimestamp ? new Date(startTimestamp) : null;
+ const endDate = endTimestamp ? new Date(endTimestamp) : null;
+
+ // Click outside to close
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ // Update parent
+ useEffect(() => {
+ const sDate = startDate ? formatDateToString(startDate) : "";
+ const eDate = endDate ? formatDateToString(endDate) : "";
+ const sTime = startDate ? startTime : "";
+ const eTime = endDate ? endTime : "";
+ onFilterChange(sDate, eDate, sTime, eTime);
+ }, [startTimestamp, endTimestamp, startTime, endTime]);
+
+ const { daysInMonth, emptyDays } = useMemo(() => {
+ const year = currentMonthDate.getFullYear();
+ const month = currentMonthDate.getMonth();
+ const days = new Date(year, month + 1, 0).getDate();
+ let firstDayIndex = new Date(year, month, 1).getDay() - 1;
+ if (firstDayIndex < 0) firstDayIndex = 6;
+ return { daysInMonth: days, emptyDays: firstDayIndex };
+ }, [currentMonthDate]);
+
+ const handleDayClick = (day: number) => {
+ const clickedTime = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), day).getTime();
+
+ if (clickedTime === startTimestamp) {
+ setStartTimestamp(null);
+ setHistory(prev => prev.filter(t => t !== clickedTime));
+ return;
+ }
+ if (clickedTime === endTimestamp) {
+ setEndTimestamp(null);
+ setHistory(prev => prev.filter(t => t !== clickedTime));
+ return;
+ }
+
+ let newStart = startTimestamp;
+ let newEnd = endTimestamp;
+ let newHistory = [...history];
+
+ if (newStart && newEnd) {
+ const oldest = newHistory[0];
+ if (oldest === newStart) newStart = null;
+ else if (oldest === newEnd) newEnd = null;
+ newHistory.shift();
+ }
+
+ if (!newStart && !newEnd) {
+ newEnd = clickedTime;
+ }
+ else if (!newStart && newEnd) {
+ if (clickedTime < newEnd) {
+ newStart = clickedTime;
+ } else {
+ newStart = newEnd;
+ newEnd = clickedTime;
+ }
+ }
+ else if (newStart && !newEnd) {
+ if (clickedTime > newStart) {
+ newEnd = clickedTime;
+ } else {
+ newEnd = newStart;
+ newStart = clickedTime;
+ }
+ }
+
+ newHistory.push(clickedTime);
+ setStartTimestamp(newStart);
+ setEndTimestamp(newEnd);
+ setHistory(newHistory);
+ };
+
+ const handleManualDateChange = (val: string, target: 'start' | 'end') => {
+ const newTime = val ? new Date(val).getTime() : null;
+
+ if (target === 'start') {
+ const oldTime = startTimestamp;
+ setStartTimestamp(newTime);
+ setHistory(prev => {
+ let h = prev;
+ if (oldTime) h = h.filter(t => t !== oldTime);
+ if (newTime) h = [...h, newTime];
+ return h.slice(-2);
+ });
+ } else {
+ const oldTime = endTimestamp;
+ setEndTimestamp(newTime);
+ setHistory(prev => {
+ let h = prev;
+ if (oldTime) h = h.filter(t => t !== oldTime);
+ if (newTime) h = [...h, newTime];
+ return h.slice(-2);
+ });
+ }
+ };
+
+ const nextMonth = () => setCurrentMonthDate(new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth() + 1, 1));
+ const prevMonth = () => setCurrentMonthDate(new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth() - 1, 1));
+ const nextYear = () => setCurrentMonthDate(new Date(currentMonthDate.getFullYear() + 1, currentMonthDate.getMonth(), 1));
+ const prevYear = () => setCurrentMonthDate(new Date(currentMonthDate.getFullYear() - 1, currentMonthDate.getMonth(), 1));
+
+ const renderDateBox = (date: Date | null) => {
+ if (!date) return "mm/dd/yyyy";
+ const d = date.getDate().toString().padStart(2, "0");
+ const m = (date.getMonth() + 1).toString().padStart(2, "0");
+ const y = date.getFullYear();
+ return `${m}/${d}/${y}`;
+ };
+
+ const isSelected = (day: number) => {
+ const d = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), day).getTime();
+ return d === startTimestamp || d === endTimestamp;
+ };
+
+ const isInRange = (day: number) => {
+ const d = new Date(currentMonthDate.getFullYear(), currentMonthDate.getMonth(), day).getTime();
+ if (startTimestamp && endTimestamp && d > startTimestamp && d < endTimestamp) return true;
+ return false;
+ };
+
+ return (
+
+
+
+ {isOpen && (
+
+
+
Ngày
+
+
+
+
+
+
+
+
+
+ {MONTHS[currentMonthDate.getMonth()]} {currentMonthDate.getFullYear()}
+
+
+
+
+
+
+
+
+ {DAYS_OF_WEEK.map(day => (
+
{day}
+ ))}
+
+ {Array.from({ length: emptyDays }).map((_, i) => (
+
+ ))}
+
+ {Array.from({ length: daysInMonth }).map((_, i) => {
+ const day = i + 1;
+ const selected = isSelected(day);
+ const inRange = isInRange(day);
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/tables/ProjectsTable.tsx b/src/components/tables/ProjectsTable.tsx
index e1005f9..e44b15a 100644
--- a/src/components/tables/ProjectsTable.tsx
+++ b/src/components/tables/ProjectsTable.tsx
@@ -45,7 +45,7 @@ export default function ProjectsTable({
const formatDate = (dateString: string | null | undefined) => {
if (!dateString) return "-";
const date = new Date(dateString);
- return `Updated on ${date.toLocaleDateString("vi-VN", {
+ return `${date.toLocaleDateString("vi-VN", {
day: "2-digit",
month: "short",
year: "numeric",
@@ -92,11 +92,10 @@ export default function ProjectsTable({
return (
@@ -104,18 +103,32 @@ export default function ProjectsTable({
};
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
Sắp xếp:
-
+
+
+
+
+
+
+
+
+
+
+ Thành viên
+
+
@@ -123,12 +136,13 @@ export default function ProjectsTable({
data.map((item) => (
-
+
+
onViewDetails(item.id)}
- className="flex items-center gap-2 mb-2 cursor-pointer hover:underline"
+ className="flex items-center gap-2 cursor-pointer hover:underline w-full"
>
{item.user?.avatar_url ? (
@@ -136,20 +150,19 @@ export default function ProjectsTable({
) : (
- {item.user?.display_name?.charAt(0)?.toUpperCase() ||
- "U"}
+ {item.user?.display_name?.charAt(0)?.toUpperCase() || "U"}
)}
-
+
{item.user?.display_name || "Unknown"}
@@ -159,21 +172,25 @@ export default function ProjectsTable({
/
-
+
{item.title}
-
+
{getStatusBadge(item.project_status)}
-
-
- {formatDate(item.updated_at)}
-
-
+
+ {formatDate(item.created_at)}
+
+
+
+ {formatDate(item.updated_at)}
+
+
+
{item.members && item.members.length > 0 ? (
<>
@@ -217,6 +234,7 @@ export default function ProjectsTable({
)}
+
))
) : (
@@ -225,6 +243,8 @@ export default function ProjectsTable({
)}
+
+
);
-}
+}
\ No newline at end of file