Skip to content

Commit 4f96332

Browse files
committed
✨ feat[tasks]: added pagination
1 parent bc82216 commit 4f96332

File tree

3 files changed

+190
-30
lines changed

3 files changed

+190
-30
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from "react";
2+
import { MdOutlineNavigateBefore, MdOutlineNavigateNext } from "react-icons/md";
3+
4+
interface PaginationProps {
5+
pageState: { page: number; total: number };
6+
totalPages: number;
7+
setPageState: (state: { page: number }) => void;
8+
label: string;
9+
}
10+
11+
const Pagination: React.FC<PaginationProps> = ({
12+
pageState,
13+
totalPages,
14+
setPageState,
15+
label,
16+
}) => {
17+
return (
18+
<>
19+
{totalPages > 0 && (
20+
<div className="mt-4 flex flex-col gap-2 justify-center items-center pb-[6rem] lg:pb-0">
21+
<div className="mt-2">{label}</div>
22+
<div className="mb-12">
23+
<button
24+
onClick={() =>
25+
setPageState({ page: Math.max(1, pageState.page - 1) })
26+
}
27+
disabled={pageState.page === 1}
28+
className="px-3 py-2 font-bold border rounded-md"
29+
>
30+
<MdOutlineNavigateBefore />
31+
</button>
32+
<span className="ml-2">
33+
{Array.from({ length: Math.min(6, totalPages) }, (_, i) => {
34+
const pageNumber = i + 1;
35+
return (
36+
<button
37+
key={pageNumber}
38+
onClick={() => setPageState({ page: pageNumber })}
39+
className={`py-2 px-3 rounded-md border ${
40+
pageNumber === pageState.page
41+
? "bg-[#E4E3FF] text-primary"
42+
: ""
43+
}`}
44+
>
45+
{pageNumber}
46+
</button>
47+
);
48+
})}
49+
{totalPages > 6 && <span className="px-3 py-2">...</span>}
50+
</span>
51+
<button
52+
onClick={() =>
53+
setPageState({ page: Math.min(totalPages, pageState.page + 1) })
54+
}
55+
disabled={pageState.page === totalPages}
56+
className="px-3 py-2 ml-2 font-bold border rounded-md"
57+
>
58+
<MdOutlineNavigateNext />
59+
</button>
60+
</div>
61+
</div>
62+
)}
63+
</>
64+
);
65+
};
66+
67+
export default Pagination;

front-end/src/pages/Profie/TaskTable.tsx

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,76 @@ import React, { useEffect, useState } from "react";
22
import { api } from "@/utils/api";
33
import { Task } from "@/interfaces/Task.interface";
44
import { formatUTCtoThai } from "@/utils";
5+
import Pagination from "@/components/Pagination";
56

67
const TaskTable: React.FC = () => {
7-
// State to store tasks
88
const [tasks, setTasks] = useState<Task[]>([]);
9-
10-
// Fetch tasks from API
11-
const fetchTasks = async () => {
9+
const [currentPage, setCurrentPage] = useState<number>(1);
10+
const [totalPages, setTotalPages] = useState<number>(1);
11+
const [perPage, setPerPage] = useState<number>(10);
12+
const [total, setTotal] = useState<number>(0);
13+
14+
const fetchTasks = async (page: number, limit: number) => {
1215
try {
13-
const response = await api.get("/api/my-tasks");
16+
const response = await api.get(
17+
`/api/my-tasks?page=${page}&limit=${limit}`
18+
);
1419
setTasks(response.data.tasks);
20+
setTotalPages(response.data.meta.totalPage);
21+
setTotal(response.data.meta.total);
1522
} catch (err) {
1623
console.error("Failed to fetch tasks:", err);
1724
}
1825
};
1926

2027
useEffect(() => {
21-
fetchTasks();
22-
}, []);
28+
fetchTasks(currentPage, perPage);
29+
}, [currentPage, perPage]);
30+
31+
const handlePageChange = (page: number) => {
32+
if (page >= 1 && page <= totalPages) {
33+
setCurrentPage(page);
34+
}
35+
};
36+
37+
const handlePerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
38+
setPerPage(Number(event.target.value));
39+
setCurrentPage(1);
40+
};
2341

2442
return (
2543
<div className="w-full border">
26-
<h1 className="text-xl font-semibold pb-4 m-1 pl-3 mt-3 border-b-[1px]">ตารางงานที่ทำ</h1>
44+
<h1 className="text-xl font-semibold pb-4 m-1 pl-3 mt-3 border-b-[1px]">
45+
ตารางงานที่ทำ
46+
</h1>
2747
<div className="-m-1.5">
2848
<div className="p-1.5 min-w-full inline-block align-middle">
2949
<div>
30-
<table className="w-full">
50+
<table className="w-full">
3151
<thead className="table-thead">
3252
<tr>
33-
<th scope="col" className="table-th">ชื่องาน</th>
34-
<th scope="col" className="table-th">ประเภท</th>
35-
<th scope="col" className="table-th">วันที่ทำงาน</th>
36-
<th scope="col" className="table-th">วันที่ลงงาน</th>
53+
<th scope="col" className="table-th">
54+
ชื่องาน
55+
</th>
56+
<th scope="col" className="table-th">
57+
ประเภท
58+
</th>
59+
<th scope="col" className="table-th">
60+
วันที่ทำงาน
61+
</th>
62+
<th scope="col" className="table-th">
63+
วันที่ลงงาน
64+
</th>
3765
</tr>
3866
</thead>
3967
<tbody className="table-tbody">
40-
{tasks.map(task => (
68+
{tasks.map((task) => (
4169
<tr key={task.task_id} className="table-tr">
4270
<td className="table-td">{task.task_name}</td>
4371
<td className="table-td">{task.task_type}</td>
44-
<td className="table-td">{formatUTCtoThai(task.work_date)}</td>
72+
<td className="table-td">
73+
{formatUTCtoThai(task.work_date)}
74+
</td>
4575
<td className="table-td">{formatUTCtoThai(task.date)}</td>
4676
</tr>
4777
))}
@@ -50,6 +80,26 @@ const TaskTable: React.FC = () => {
5080
</div>
5181
</div>
5282
</div>
83+
{/* Pagination component */}
84+
<Pagination
85+
pageState={{ page: currentPage, total: totalPages }}
86+
totalPages={totalPages}
87+
setPageState={({ page }) => handlePageChange(page)}
88+
label={`รายการงานทั้งหมด ${total} งาน`}
89+
/>
90+
{/* Items per page selection */}
91+
<div className="flex items-center p-2">
92+
<span className="mr-2">Items per page:</span>
93+
<select
94+
value={perPage}
95+
onChange={handlePerPageChange}
96+
className="p-1 border border-gray-300 rounded-md"
97+
>
98+
<option value={10}>10</option>
99+
<option value={20}>20</option>
100+
<option value={30}>30</option>
101+
</select>
102+
</div>
53103
</div>
54104
);
55105
};

report-server/src/controllers/Task.controller.ts

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,46 @@ export class TaskController {
1010
static async getUserTask(req: Request, res: Response) {
1111
try {
1212
const authHeader = req.headers.authorization;
13+
1314
if (!authHeader) {
1415
return res.status(403).json({ error: "No token provided" });
1516
}
17+
let { limit, page } = req.query;
1618

1719
const acc_tk = authHeader.split(" ")[1];
1820
const user = (await Encrypt.getUserData(acc_tk)) as JwtPayload;
21+
// Validate and parse limit and page
22+
const parsedLimit = TaskController.validateLimit(Number(limit || 10));
23+
const parsedPage = TaskController.validatePage(Number(page || 1));
24+
25+
const offset = (parsedPage - 1) * parsedLimit;
1926

2027
if (!user) {
2128
return res
2229
.status(403)
2330
.json({ error: "You are not allowed to access this resource" });
2431
}
2532

26-
const tasks = await Task.findAll({
33+
const { rows: tasks, count } = await Task.findAndCountAll({
2734
order: [["date", "DESC"]],
2835
where: {
2936
user_id: user.user_id,
3037
},
38+
offset,
39+
limit: parsedLimit,
3140
});
3241

33-
return res
34-
.status(200)
35-
.json({ success: true, message: " Get tasks successfully!", tasks });
42+
return res.status(200).json({
43+
success: true,
44+
message: " Get tasks successfully!",
45+
meta: {
46+
total: count,
47+
page: parsedPage,
48+
totalPage: Math.ceil(count / parsedLimit),
49+
limit: parsedLimit,
50+
},
51+
tasks,
52+
});
3653
} catch (err) {
3754
console.log(err);
3855
res.status(500).json({ error: "Internal server error" });
@@ -41,17 +58,19 @@ export class TaskController {
4158

4259
static async getTasks(req: Request, res: Response) {
4360
try {
44-
let date = req.query.date as string | undefined;
45-
let status = req.query.status as string | undefined;
46-
let showUnchecked = req.query.showUnchecked as string | undefined;
61+
let { date, status, showUnchecked, limit, page } = req.query;
62+
63+
// Validate and parse limit and page
64+
const parsedLimit = TaskController.validateLimit(Number(limit || 10));
65+
const parsedPage = TaskController.validatePage(Number(page || 1));
4766

4867
let query: any = {};
4968

5069
if (showUnchecked === "true") {
5170
query.is_check = false;
5271
} else {
5372
if (date) {
54-
if (!isValidDate(date)) {
73+
if (!TaskController.isValidDate(date as string)) {
5574
return res.status(400).json({ error: "Invalid date format" });
5675
}
5776
query.date = {
@@ -66,27 +85,30 @@ export class TaskController {
6685
}
6786
}
6887

69-
const tasks = await Task.findAll({
88+
const offset = (parsedPage - 1) * parsedLimit;
89+
90+
const { rows: tasks, count } = await Task.findAndCountAll({
7091
order: [["date", "DESC"]],
7192
where: query,
7293
include: [User],
94+
offset,
95+
limit: parsedLimit,
7396
});
7497

7598
return res.status(200).json({
7699
success: true,
77100
message: "Get tasks successfully!",
78-
date,
101+
meta: {
102+
total: count,
103+
page: parsedPage,
104+
limit: parsedLimit,
105+
},
79106
tasks,
80107
});
81108
} catch (err) {
82109
console.error(err);
83110
res.status(500).json({ error: "Internal server error" });
84111
}
85-
86-
function isValidDate(dateString: string) {
87-
const regex = /^\d{4}-\d{2}-\d{2}$/;
88-
return regex.test(dateString);
89-
}
90112
}
91113

92114
static async getTaskById(req: Request, res: Response) {
@@ -302,4 +324,25 @@ export class TaskController {
302324
res.status(500).json({ error: "Internal server error" });
303325
}
304326
}
327+
// Helper function to validate limit (perPage)
328+
static validateLimit(perPage: number): number {
329+
return perPage > 0 && perPage <= 100 ? perPage : 10;
330+
}
331+
332+
// Helper function to validate page
333+
static validatePage(page: number): number {
334+
return page > 0 ? page : 1;
335+
}
336+
337+
// Helper function to validate numbers using regex (only numeric values allowed)
338+
static isValidNumber(value: any): boolean {
339+
const regex = /^[0-9]+$/;
340+
return regex.test(value);
341+
}
342+
343+
// Helper function to validate date format (YYYY-MM-DD)
344+
static isValidDate(dateString: string): boolean {
345+
const regex = /^\d{4}-\d{2}-\d{2}$/;
346+
return regex.test(dateString);
347+
}
305348
}

0 commit comments

Comments
 (0)