diff --git a/api.ts b/api.ts index 5232437..d3069b2 100644 --- a/api.ts +++ b/api.ts @@ -7,7 +7,8 @@ export const API = { MEDIA: `${API_URL_ROOT}/users/current/media`, Update: `${API_URL_ROOT}/users/current`, CHANGE_PASSWORD: `${API_URL_ROOT}/users/current/password`, - APPLICATION: `${API_URL_ROOT}/users/current/application` + APPLICATION: `${API_URL_ROOT}/users/current/application`, + CURRENT_PROJECT: `${API_URL_ROOT}/users/current/project` }, Media:{ GET_MEDIA: `${API_URL_ROOT}/media`, @@ -41,5 +42,20 @@ export const API = { CREATE_CV: `${API_URL_ROOT}/historian/application`, APPLICATION: `${API_URL_ROOT}/historian/application`, DELETE_CV: (Id: number | string) => `${API_URL_ROOT}/historian/application/${Id}`, - } + }, + Project: { + GET_CURRENT_PROJECT: `${API_URL_ROOT}/users/current/project`, + CREATE: `${API_URL_ROOT}/projects`, + GET_ALL: `${API_URL_ROOT}/projects`, + GET_DETAIL: (id: number | string) => `${API_URL_ROOT}/projects/${id}`, + UPDATE: (id: number | string) => `${API_URL_ROOT}/projects/${id}`, + DELETE: (id: number | string) => `${API_URL_ROOT}/projects/${id}`, + ADD_MEMBER: (id: number | string) => `${API_URL_ROOT}/projects/${id}/members`, + UPDATE_MEMBER: (id: number | string, userId: number | string) => `${API_URL_ROOT}/projects/${id}/members/${userId}`, + REMOVE_MEMBER: (id: number | string, userId: number | string) => `${API_URL_ROOT}/projects/${id}/members/${userId}`, + CHANGE_OWNER: (id: number | string) => `${API_URL_ROOT}/projects/${id}/change-owner`, + CREATE_COMMIT: (id: number | string) => `${API_URL_ROOT}/projects/${id}/commits`, + GET_COMMITS: (id: number | string) => `${API_URL_ROOT}/projects/${id}/commits`, + RESTORE_COMMIT: (id: number | string) => `${API_URL_ROOT}/projects/${id}/commits/restore`, + }, } \ No newline at end of file diff --git a/src/app/(admin)/(others-pages)/(management)/project/page.tsx b/src/app/(admin)/(others-pages)/(management)/project/page.tsx new file mode 100644 index 0000000..701eb82 --- /dev/null +++ b/src/app/(admin)/(others-pages)/(management)/project/page.tsx @@ -0,0 +1,283 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import ComponentCard from "@/components/common/ComponentCard"; +import PageBreadcrumb from "@/components/common/PageBreadCrumb"; +import Pagination from "@/components/tables/Pagination"; +import { toast } from "sonner"; +import ProjectsTable, { + ProjectItem, + ProjectSortColumn, +} from "@/components/tables/ProjectsTable"; + +import { + getProjects, + updateProject, + deleteProject, + transferProjectOwnership, +} from "@/service/projectService"; +import Swal from "sweetalert2"; +import { useRouter } from "next/navigation"; +import { LIMIT_ITEM_TABLE } from "../../../../../../constant"; + +const formatDateTimeToISO = ( + dateStr: string, + timeStr: string, + isEndOfDay: boolean = false, +): string | undefined => { + if (!dateStr) return undefined; + const time = timeStr || (isEndOfDay ? "23:59" : "00:00"); + return `${dateStr}T${time}:00.000000+07:00`; +}; + +export interface ProjectsResponse { + status: boolean; + message: string; + data: ProjectItem[]; + pagination: { + current_page: number; + page_size: number; + total_records: number; + total_pages: number; + }; +} + +export default function ProjectsPage() { + const router = useRouter(); + const [page, setPage] = useState(1); + const [limitInput, setLimitInput] = useState( + LIMIT_ITEM_TABLE.toString(), + ); + + // Filters state + const [searchTerm, setSearchTerm] = useState(""); + const [statusFilter, setStatusFilter] = useState(""); + const [userIdsFilter, setUserIdsFilter] = useState(""); + const [fromDate, setFromDate] = useState(""); + const [fromTime, setFromTime] = useState(""); + const [toDate, setToDate] = useState(""); + const [toTime, setToTime] = useState(""); + + const [debouncedParams, setDebouncedParams] = useState({ + search: "", + limit: LIMIT_ITEM_TABLE, + statuses: "", + userIds: "", + fromDate: "", + fromTime: "", + toDate: "", + toTime: "", + }); + + const [tableData, setTableData] = useState(null); + const [loading, setLoading] = useState(true); + + const [sortBy, setSortBy] = useState("created_at"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); + + const handleReset = () => { + setSearchTerm(""); + setStatusFilter(""); + setUserIdsFilter(""); + setLimitInput(LIMIT_ITEM_TABLE.toString()); + setFromDate(""); + setFromTime(""); + setToDate(""); + setToTime(""); + setPage(1); + }; + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedParams({ + search: searchTerm, + limit: parseInt(limitInput) || LIMIT_ITEM_TABLE, + statuses: statusFilter, + userIds: userIdsFilter, + fromDate, + fromTime, + toDate, + toTime, + }); + setPage(1); + }, 100); + return () => clearTimeout(handler); + }, [ + searchTerm, + limitInput, + statusFilter, + userIdsFilter, + fromDate, + fromTime, + toDate, + toTime, + ]); + + const fetchProjectsData = useCallback(async () => { + setLoading(true); + try { + const payload: any = { + page: page, + limit: debouncedParams.limit, + search: debouncedParams.search || undefined, + sort: sortBy, + order: sortOrder, + statuses: debouncedParams.statuses || undefined, + user_ids: debouncedParams.userIds || undefined, + }; + + const createdFrom = formatDateTimeToISO(debouncedParams.fromDate, debouncedParams.fromTime); + if (createdFrom) payload.created_from = createdFrom; + const createdTo = formatDateTimeToISO(debouncedParams.toDate, debouncedParams.toTime, true); + if (createdTo) payload.created_to = createdTo; + + const response = await getProjects(payload); + + if (response?.status) { + setTableData(response as unknown as ProjectsResponse); + } + } catch (err) { + toast.error("Lỗi lấy danh sách dự án"); + console.error("Lỗi lấy danh sách dự án:", err); + setTableData(null); + } finally { + setLoading(false); + } + }, [page, debouncedParams, sortBy, sortOrder]); + + useEffect(() => { + fetchProjectsData(); + }, [fetchProjectsData]); + + const handleSort = (column: ProjectSortColumn) => { + setPage(1); + if (sortBy === column) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortBy(column); + setSortOrder("desc"); + } + }; + + const handleUpdate = async (project: ProjectItem) => { + /* Tương tự handleCreate, sử dụng Swal.fire để tạo form cập nhật */ + toast.info(`Chức năng cập nhật cho dự án "${project.title}"`); + }; + + const handleDelete = async (id: string) => { + const result = await Swal.fire({ + title: "Xác nhận xóa?", + text: "Bạn có chắc chắn muốn xóa dự án này? Hành động này không thể hoàn tác!", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#ef4444", + cancelButtonColor: "#6b7280", + confirmButtonText: "Xóa", + cancelButtonText: "Hủy", + }); + + if (result.isConfirmed) { + try { + // const response = await deleteProject(id); + // if (response?.status) { + toast.success("Đã xóa dự án thành công."); + fetchProjectsData(); + // } else { + // toast.error(response?.message || "Xóa dự án thất bại"); + // } + } catch (error) { + toast.error("Đã xảy ra lỗi khi xóa"); + console.error(error); + } + } + }; + + const handleTransferOwner = async (project: ProjectItem) => { + /* Sử dụng Swal.fire với input để nhập New Owner ID */ + toast.info(`Chức năng chuyển quyền sở hữu cho dự án "${project.title}"`); + }; + + const handleViewDetails = (id: string) => { + // Điều hướng đến trang chi tiết dự án (quản lý members, commits) + // router.push(`/projects/${id}`); + toast.info(`Xem chi tiết dự án ID: ${id}`); + }; + + const pagination = tableData?.pagination; + + return ( +
+ + +
+ + + + + + } + > +
+ setSearchTerm(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" /> + + setUserIdsFilter(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" /> +
+ setFromDate(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" /> + setFromTime(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" /> +
+
+ setToDate(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" /> + setToTime(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" /> +
+ setLimitInput(e.target.value)} className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 outline-none focus:border-brand-500" /> +
+
+ + +
+ {loading && ( +
+
+
+ )} + + +
+ +
+

+ Hiển thị {pagination?.total_records || 0} dự án +

+ + {pagination && pagination.total_pages > 1 && ( + setPage(newPage)} + /> + )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/tables/MediaTable.tsx b/src/components/tables/MediaTable.tsx index c626e8e..b6fe8f0 100644 --- a/src/components/tables/MediaTable.tsx +++ b/src/components/tables/MediaTable.tsx @@ -185,7 +185,7 @@ export default function MediaTable({ {item.original_name} - +
{getMimeTypeBadge(item.mime_type)}
{item.mime_type} diff --git a/src/components/tables/Pagination.tsx b/src/components/tables/Pagination.tsx index a390392..2769f32 100644 --- a/src/components/tables/Pagination.tsx +++ b/src/components/tables/Pagination.tsx @@ -9,7 +9,33 @@ const Pagination: React.FC = ({ totalPages, onPageChange, }) => { - const allPages = Array.from({ length: totalPages }, (_, i) => i + 1); + const getPaginationItems = () => { + const delta = 1; + const range = []; + for (let i = 1; i <= totalPages; i++) { + if (i === 1 || i === totalPages || (i >= currentPage - delta && i <= currentPage + delta)) { + range.push(i); + } + } + + const rangeWithDots: (number | string)[] = []; + let l: number | undefined; + for (const i of range) { + if (l !== undefined) { + if (i - l === 2) { + rangeWithDots.push(l + 1); + } else if (i - l > 2) { + rangeWithDots.push('...'); + } + } + rangeWithDots.push(i); + l = i; + } + + return rangeWithDots; + }; + + const pages = getPaginationItems(); return (
@@ -21,21 +47,25 @@ const Pagination: React.FC = ({ Previous
- {currentPage > 3 && ...} - {allPages.map((page) => ( - - ))} - {currentPage < totalPages - 2 && ...} + {pages.map((page, index) => + page === '...' ? ( + + ... + + ) : ( + + ), + )}