From a2bab73e502bd849f37b44d8f9205a8096c1931f Mon Sep 17 00:00:00 2001 From: bokhonglo Date: Wed, 29 Apr 2026 12:08:09 +0700 Subject: [PATCH] update: projectsmanage --- .../(management)/project/page.tsx | 67 +- .../(others-pages)/projects/[id]/page.tsx | 588 ++++++++++++++++++ src/components/tables/ProjectsTable.tsx | 263 ++++---- src/interface/project.ts | 59 +- src/service/projectService.ts | 68 +- 5 files changed, 812 insertions(+), 233 deletions(-) create mode 100644 src/app/(admin)/(others-pages)/projects/[id]/page.tsx diff --git a/src/app/(admin)/(others-pages)/(management)/project/page.tsx b/src/app/(admin)/(others-pages)/(management)/project/page.tsx index 701eb82..478a54a 100644 --- a/src/app/(admin)/(others-pages)/(management)/project/page.tsx +++ b/src/app/(admin)/(others-pages)/(management)/project/page.tsx @@ -19,6 +19,7 @@ import { import Swal from "sweetalert2"; import { useRouter } from "next/navigation"; import { LIMIT_ITEM_TABLE } from "../../../../../../constant"; +import { ProjectsResponse } from "@/interface/project"; const formatDateTimeToISO = ( dateStr: string, @@ -30,19 +31,10 @@ const formatDateTimeToISO = ( 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() { +export default function ProjectsPage(_props: { + params: unknown; + searchParams: unknown; +}) { const router = useRouter(); const [page, setPage] = useState(1); const [limitInput, setLimitInput] = useState( @@ -69,7 +61,7 @@ export default function ProjectsPage() { toTime: "", }); - const [tableData, setTableData] = useState(null); + const [tableData, setTableData] = useState | null>(null); const [loading, setLoading] = useState(true); const [sortBy, setSortBy] = useState("created_at"); @@ -134,7 +126,7 @@ export default function ProjectsPage() { const response = await getProjects(payload); if (response?.status) { - setTableData(response as unknown as ProjectsResponse); + setTableData(response as unknown as ProjectsResponse); } } catch (err) { toast.error("Lỗi lấy danh sách dự án"); @@ -159,48 +151,9 @@ export default function ProjectsPage() { } }; - 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}`); + router.push(`/projects/${id}`); }; const pagination = tableData?.pagination; @@ -256,9 +209,7 @@ export default function ProjectsPage() { onSort={handleSort} sortBy={sortBy} sortOrder={sortOrder} - onUpdate={handleUpdate} - onDelete={handleDelete} - onTransferOwner={handleTransferOwner} + onViewDetails={handleViewDetails} /> diff --git a/src/app/(admin)/(others-pages)/projects/[id]/page.tsx b/src/app/(admin)/(others-pages)/projects/[id]/page.tsx new file mode 100644 index 0000000..2f8d347 --- /dev/null +++ b/src/app/(admin)/(others-pages)/projects/[id]/page.tsx @@ -0,0 +1,588 @@ +"use client"; + +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { toast } from "sonner"; +import { + getProjectDetailByID, + updateProject, + transferProjectOwnership, + addProjectMember, + updateProjectMemberRole, + removeProjectMember, + deleteProject, +} from "@/service/projectService"; +import { Project } from "@/interface/project"; +import Swal from "sweetalert2"; + +type TabType = "overview" | "members" | "settings"; + +export default function ProjectDetailsPage() { + const params = useParams(); + const router = useRouter(); + const id = params.id as string; + + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState("overview"); + + const [editForm, setEditForm] = useState({ + title: "", + description: "", + status: "PRIVATE" as any, + }); + const [newOwnerId, setNewOwnerId] = useState(""); + const [newMember, setNewMember] = useState({ + user_id: "", + role: "EDITOR" as any, + }); + + const fetchProject = async () => { + setLoading(true); + try { + const res = await getProjectDetailByID(id); + if (res?.status && res.data) { + setProject(res.data); + setEditForm({ + title: res.data.title, + description: res.data.description, + status: res.data.project_status, + }); + } + } catch (error) { + toast.error("Lỗi khi tải dữ liệu dự án"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (id) fetchProject(); + }, [id]); + + const handleUpdateInfo = async (e: React.FormEvent) => { + e.preventDefault(); + try { + await updateProject(id, editForm); + toast.success("Cập nhật thông tin thành công!"); + fetchProject(); + } catch (error) { + toast.error("Cập nhật thất bại"); + } + }; + + const handleTransferOwnership = async (e: React.FormEvent) => { + e.preventDefault(); + + const memberName = + project?.members?.find((m) => m.user_id === newOwnerId)?.display_name || + "thành viên này"; + + const result = await Swal.fire({ + title: "Chuyển quyền sở hữu?", + html: `Bạn có chắc chắn muốn chuyển dự án này cho ${memberName}?
Hành động này không thể hoàn tác và bạn sẽ không còn là chủ sở hữu nữa.`, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#238636", + cancelButtonColor: "#30363d", + confirmButtonText: "Tôi hiểu, chuyển quyền sở hữu", + cancelButtonText: "Hủy bỏ", + color: "#333", + customClass: { + popup: "border border-[#30363d] rounded-xl", + }, + }); + + if (result.isConfirmed) { + try { + const res = await transferProjectOwnership(id, { + new_owner_id: newOwnerId, + }); + if (res?.status) { + toast.success("Đã chuyển quyền sở hữu thành công!"); + setNewOwnerId(""); + fetchProject(); + } else { + toast.error(res?.message || "Chuyển quyền thất bại"); + } + } catch (error) { + toast.error("Lỗi hệ thống khi chuyển quyền"); + } + } + }; + + const handleAddMember = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newMember.user_id) return toast.error("Vui lòng nhập User ID"); + try { + await addProjectMember(id, newMember); + toast.success("Thêm thành viên thành công"); + setNewMember({ user_id: "", role: "EDITOR" }); + fetchProject(); + } catch (error) { + toast.error("Lỗi thêm thành viên"); + } + }; + + const handleUpdateRole = async (userId: string, newRole: string) => { + try { + await updateProjectMemberRole(id, userId, { role: newRole as any }); + toast.success("Cập nhật quyền thành công"); + fetchProject(); + } catch (error) { + toast.error("Cập nhật quyền thất bại"); + } + }; + + const handleRemoveMember = async (userId: string) => { + if (!confirm("Xác nhận xóa thành viên này?")) return; + try { + await removeProjectMember(id, userId); + toast.success("Đã xóa thành viên"); + fetchProject(); + } catch (error) { + toast.error("Xóa thành viên thất bại"); + } + }; + + const handleDeleteProject = async () => { + const result = await Swal.fire({ + title: "Xác nhận xóa dự án?", + text: "Hành động này sẽ xóa vĩnh viễn dự án. Bạn không thể hoàn tác sau khi xác nhận!", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#d33", + cancelButtonColor: "#30363d", + confirmButtonText: "Tôi hiểu, xóa dự án này", + cancelButtonText: "Hủy", + color: "#333", + customClass: { + popup: "border border-[#30363d] rounded-xl", + }, + }); + + if (result.isConfirmed) { + try { + const res = await deleteProject(id); + if (res?.status) { + toast.success("Đã xóa dự án thành công"); + router.push("/project"); + } else { + toast.error(res?.message || "Xóa dự án thất bại"); + } + } catch (error) { + toast.error("Đã xảy ra lỗi khi kết nối máy chủ"); + console.error(error); + } + } + }; + + if (loading) + return ( +
+ Đang tải dữ liệu... +
+ ); + if (!project) + return ( +
+ Không tìm thấy dự án +
+ ); + + return ( +
+
+
+
+
+ {project.user?.avatar_url ? ( +
+ avatar +
+ ) : ( +
+ + {project.user?.display_name?.charAt(0)?.toUpperCase() || + "U"} + +
+ )} +
+ + + {project.user?.display_name} + + / + + {project.title} + + + {project.project_status} + +
+ +
+ {[ + { + id: "overview", + label: "Overview", + icon: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", + }, + { + id: "members", + label: `Members`, + count: project.members?.length || 0, + icon: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z", + }, + { + id: "settings", + label: "Settings", + icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z", + }, + ].map((tab) => ( + + ))} +
+
+
+ +
+ {activeTab === "overview" && ( +
+
+
+
+ About +
+
+ {project.description || ( + + Không có mô tả cho dự án này. + + )} +
+
+
+ +
+
+

+ Owner +

+
+
+ {project.user?.avatar_url ? ( +
+ avatar +
+ ) : ( +
+ + {project.user?.display_name + ?.charAt(0) + ?.toUpperCase() || "U"} + +
+ )} +
+
+
+ {project.user?.display_name} +
+
+ {project.user?.email || "No email"} +
+
+
+
+
+
+ )} + + {activeTab === "members" && ( +
+

+ Manage access +

+ +
+ + setNewMember({ ...newMember, user_id: e.target.value }) + } + className="flex-1 px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 transition-shadow" + /> +
+ + +
+
+ +
+
+ {project.members && project.members.length > 0 ? ( + project.members.map((member) => ( +
+
+ {member.display_name} +
+
+ {member.display_name} +
+
+ ID: {member.user_id} +
+
+
+
+ + +
+
+ )) + ) : ( +
+ Chưa có thành viên nào. +
+ )} +
+
+
+ )} + + {activeTab === "settings" && ( +
+
+

+ General +

+
+
+ + + setEditForm({ ...editForm, title: e.target.value }) + } + className="w-full max-w-md px-3 py-2 text-sm rounded-md border border-gray-300 dark:border-[#30363d] bg-white dark:bg-[#0d1117] outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-500 transition-shadow" + /> +
+
+ +