Skip to content

Commit

Permalink
✨ feat[tasks]: added pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
boytur committed Sep 12, 2024
1 parent bc82216 commit 4f96332
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 30 deletions.
67 changes: 67 additions & 0 deletions front-end/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from "react";
import { MdOutlineNavigateBefore, MdOutlineNavigateNext } from "react-icons/md";

interface PaginationProps {
pageState: { page: number; total: number };
totalPages: number;
setPageState: (state: { page: number }) => void;
label: string;
}

const Pagination: React.FC<PaginationProps> = ({
pageState,
totalPages,
setPageState,
label,
}) => {
return (
<>
{totalPages > 0 && (
<div className="mt-4 flex flex-col gap-2 justify-center items-center pb-[6rem] lg:pb-0">
<div className="mt-2">{label}</div>
<div className="mb-12">
<button
onClick={() =>
setPageState({ page: Math.max(1, pageState.page - 1) })
}
disabled={pageState.page === 1}
className="px-3 py-2 font-bold border rounded-md"
>
<MdOutlineNavigateBefore />
</button>
<span className="ml-2">
{Array.from({ length: Math.min(6, totalPages) }, (_, i) => {
const pageNumber = i + 1;
return (
<button
key={pageNumber}
onClick={() => setPageState({ page: pageNumber })}
className={`py-2 px-3 rounded-md border ${
pageNumber === pageState.page
? "bg-[#E4E3FF] text-primary"
: ""
}`}
>
{pageNumber}
</button>
);
})}
{totalPages > 6 && <span className="px-3 py-2">...</span>}
</span>
<button
onClick={() =>
setPageState({ page: Math.min(totalPages, pageState.page + 1) })
}
disabled={pageState.page === totalPages}
className="px-3 py-2 ml-2 font-bold border rounded-md"
>
<MdOutlineNavigateNext />
</button>
</div>
</div>
)}
</>
);
};

export default Pagination;
80 changes: 65 additions & 15 deletions front-end/src/pages/Profie/TaskTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,76 @@ import React, { useEffect, useState } from "react";
import { api } from "@/utils/api";
import { Task } from "@/interfaces/Task.interface";
import { formatUTCtoThai } from "@/utils";
import Pagination from "@/components/Pagination";

const TaskTable: React.FC = () => {
// State to store tasks
const [tasks, setTasks] = useState<Task[]>([]);

// Fetch tasks from API
const fetchTasks = async () => {
const [currentPage, setCurrentPage] = useState<number>(1);
const [totalPages, setTotalPages] = useState<number>(1);
const [perPage, setPerPage] = useState<number>(10);
const [total, setTotal] = useState<number>(0);

const fetchTasks = async (page: number, limit: number) => {
try {
const response = await api.get("/api/my-tasks");
const response = await api.get(
`/api/my-tasks?page=${page}&limit=${limit}`
);
setTasks(response.data.tasks);
setTotalPages(response.data.meta.totalPage);
setTotal(response.data.meta.total);
} catch (err) {
console.error("Failed to fetch tasks:", err);
}
};

useEffect(() => {
fetchTasks();
}, []);
fetchTasks(currentPage, perPage);
}, [currentPage, perPage]);

const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
}
};

const handlePerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setPerPage(Number(event.target.value));
setCurrentPage(1);
};

return (
<div className="w-full border">
<h1 className="text-xl font-semibold pb-4 m-1 pl-3 mt-3 border-b-[1px]">ตารางงานที่ทำ</h1>
<h1 className="text-xl font-semibold pb-4 m-1 pl-3 mt-3 border-b-[1px]">
ตารางงานที่ทำ
</h1>
<div className="-m-1.5">
<div className="p-1.5 min-w-full inline-block align-middle">
<div>
<table className="w-full">
<table className="w-full">
<thead className="table-thead">
<tr>
<th scope="col" className="table-th">ชื่องาน</th>
<th scope="col" className="table-th">ประเภท</th>
<th scope="col" className="table-th">วันที่ทำงาน</th>
<th scope="col" className="table-th">วันที่ลงงาน</th>
<th scope="col" className="table-th">
ชื่องาน
</th>
<th scope="col" className="table-th">
ประเภท
</th>
<th scope="col" className="table-th">
วันที่ทำงาน
</th>
<th scope="col" className="table-th">
วันที่ลงงาน
</th>
</tr>
</thead>
<tbody className="table-tbody">
{tasks.map(task => (
{tasks.map((task) => (
<tr key={task.task_id} className="table-tr">
<td className="table-td">{task.task_name}</td>
<td className="table-td">{task.task_type}</td>
<td className="table-td">{formatUTCtoThai(task.work_date)}</td>
<td className="table-td">
{formatUTCtoThai(task.work_date)}
</td>
<td className="table-td">{formatUTCtoThai(task.date)}</td>
</tr>
))}
Expand All @@ -50,6 +80,26 @@ const TaskTable: React.FC = () => {
</div>
</div>
</div>
{/* Pagination component */}
<Pagination
pageState={{ page: currentPage, total: totalPages }}
totalPages={totalPages}
setPageState={({ page }) => handlePageChange(page)}
label={`รายการงานทั้งหมด ${total} งาน`}
/>
{/* Items per page selection */}
<div className="flex items-center p-2">
<span className="mr-2">Items per page:</span>
<select
value={perPage}
onChange={handlePerPageChange}
className="p-1 border border-gray-300 rounded-md"
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={30}>30</option>
</select>
</div>
</div>
);
};
Expand Down
73 changes: 58 additions & 15 deletions report-server/src/controllers/Task.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,46 @@ export class TaskController {
static async getUserTask(req: Request, res: Response) {
try {
const authHeader = req.headers.authorization;

if (!authHeader) {
return res.status(403).json({ error: "No token provided" });
}
let { limit, page } = req.query;

const acc_tk = authHeader.split(" ")[1];
const user = (await Encrypt.getUserData(acc_tk)) as JwtPayload;
// Validate and parse limit and page
const parsedLimit = TaskController.validateLimit(Number(limit || 10));
const parsedPage = TaskController.validatePage(Number(page || 1));

const offset = (parsedPage - 1) * parsedLimit;

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

const tasks = await Task.findAll({
const { rows: tasks, count } = await Task.findAndCountAll({
order: [["date", "DESC"]],
where: {
user_id: user.user_id,
},
offset,
limit: parsedLimit,
});

return res
.status(200)
.json({ success: true, message: " Get tasks successfully!", tasks });
return res.status(200).json({
success: true,
message: " Get tasks successfully!",
meta: {
total: count,
page: parsedPage,
totalPage: Math.ceil(count / parsedLimit),
limit: parsedLimit,
},
tasks,
});
} catch (err) {
console.log(err);
res.status(500).json({ error: "Internal server error" });
Expand All @@ -41,17 +58,19 @@ export class TaskController {

static async getTasks(req: Request, res: Response) {
try {
let date = req.query.date as string | undefined;
let status = req.query.status as string | undefined;
let showUnchecked = req.query.showUnchecked as string | undefined;
let { date, status, showUnchecked, limit, page } = req.query;

// Validate and parse limit and page
const parsedLimit = TaskController.validateLimit(Number(limit || 10));
const parsedPage = TaskController.validatePage(Number(page || 1));

let query: any = {};

if (showUnchecked === "true") {
query.is_check = false;
} else {
if (date) {
if (!isValidDate(date)) {
if (!TaskController.isValidDate(date as string)) {
return res.status(400).json({ error: "Invalid date format" });
}
query.date = {
Expand All @@ -66,27 +85,30 @@ export class TaskController {
}
}

const tasks = await Task.findAll({
const offset = (parsedPage - 1) * parsedLimit;

const { rows: tasks, count } = await Task.findAndCountAll({
order: [["date", "DESC"]],
where: query,
include: [User],
offset,
limit: parsedLimit,
});

return res.status(200).json({
success: true,
message: "Get tasks successfully!",
date,
meta: {
total: count,
page: parsedPage,
limit: parsedLimit,
},
tasks,
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "Internal server error" });
}

function isValidDate(dateString: string) {
const regex = /^\d{4}-\d{2}-\d{2}$/;
return regex.test(dateString);
}
}

static async getTaskById(req: Request, res: Response) {
Expand Down Expand Up @@ -302,4 +324,25 @@ export class TaskController {
res.status(500).json({ error: "Internal server error" });
}
}
// Helper function to validate limit (perPage)
static validateLimit(perPage: number): number {
return perPage > 0 && perPage <= 100 ? perPage : 10;
}

// Helper function to validate page
static validatePage(page: number): number {
return page > 0 ? page : 1;
}

// Helper function to validate numbers using regex (only numeric values allowed)
static isValidNumber(value: any): boolean {
const regex = /^[0-9]+$/;
return regex.test(value);
}

// Helper function to validate date format (YYYY-MM-DD)
static isValidDate(dateString: string): boolean {
const regex = /^\d{4}-\d{2}-\d{2}$/;
return regex.test(dateString);
}
}

0 comments on commit 4f96332

Please sign in to comment.