From 769fc73898b43f7a694328372b97b49fff51a9bd Mon Sep 17 00:00:00 2001 From: RKWojs Date: Mon, 29 Sep 2025 20:09:11 +0200 Subject: [PATCH] feat: Add ProjectAssigneeDropdown component and integrate it into ProjectViewPage --- src/app/projects/[id]/page.js | 7 + src/components/ProjectAssigneeDropdown.js | 219 ++++++++++++++++++++++ src/lib/queries/projects.js | 9 +- 3 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 src/components/ProjectAssigneeDropdown.js diff --git a/src/app/projects/[id]/page.js b/src/app/projects/[id]/page.js index 4206c2e..c186628 100644 --- a/src/app/projects/[id]/page.js +++ b/src/app/projects/[id]/page.js @@ -15,6 +15,7 @@ import { formatDate, formatCoordinates } from "@/lib/utils"; import PageContainer from "@/components/ui/PageContainer"; import PageHeader from "@/components/ui/PageHeader"; import ProjectStatusDropdown from "@/components/ProjectStatusDropdown"; +import ProjectAssigneeDropdown from "@/components/ProjectAssigneeDropdown"; import ClientProjectMap from "@/components/ui/ClientProjectMap"; import FileUploadBox from "@/components/FileUploadBox"; import FileItem from "@/components/FileItem"; @@ -479,6 +480,12 @@ export default function ProjectViewPage() { +
+ + Przypisany do + + +
{daysRemaining !== null && (
diff --git a/src/components/ProjectAssigneeDropdown.js b/src/components/ProjectAssigneeDropdown.js new file mode 100644 index 0000000..cbcdec5 --- /dev/null +++ b/src/components/ProjectAssigneeDropdown.js @@ -0,0 +1,219 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { createPortal } from "react-dom"; +import Badge from "@/components/ui/Badge"; +import { useTranslation } from "@/lib/i18n"; + +export default function ProjectAssigneeDropdown({ + project, + size = "md", + showDropdown = true, +}) { + const { t } = useTranslation(); + const [assignee, setAssignee] = useState({ + id: project.assigned_to, + name: project.assigned_to_name, + username: project.assigned_to_username, + initial: project.assigned_to_initial, + }); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [dropdownPosition, setDropdownPosition] = useState({ + top: 0, + left: 0, + width: 0, + }); + const buttonRef = useRef(null); + + // Update assignee state when project prop changes + useEffect(() => { + setAssignee({ + id: project.assigned_to, + name: project.assigned_to_name, + username: project.assigned_to_username, + initial: project.assigned_to_initial, + }); + }, [project.assigned_to, project.assigned_to_name, project.assigned_to_username, project.assigned_to_initial]); + + // Fetch users for assignment + useEffect(() => { + const fetchUsers = async () => { + try { + const response = await fetch("/api/projects/users"); + if (response.ok) { + const userData = await response.json(); + setUsers(userData); + } + } catch (error) { + console.error("Failed to fetch users:", error); + } + }; + + if (isOpen && users.length === 0) { + fetchUsers(); + } + }, [isOpen, users.length]); + + const handleChange = async (newAssigneeId) => { + if (newAssigneeId === assignee.id) { + setIsOpen(false); + return; + } + + const newAssignee = users.find(u => u.id === newAssigneeId) || null; + setAssignee(newAssignee); + setLoading(true); + setIsOpen(false); + + try { + const updateData = { ...project, assigned_to: newAssigneeId }; + + const response = await fetch(`/api/projects/${project.project_id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updateData), + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error('Update failed:', errorData); + } + + window.location.reload(); + } catch (error) { + console.error("Failed to update assignee:", error); + setAssignee({ + id: project.assigned_to, + name: project.assigned_to_name, + username: project.assigned_to_username, + initial: project.assigned_to_initial, + }); // Revert on error + } finally { + setLoading(false); + } + }; + + const updateDropdownPosition = () => { + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.bottom + window.scrollY + 4, + left: rect.left + window.scrollX, + width: rect.width, + }); + } + }; + + const handleOpen = () => { + setIsOpen(true); + }; + + useEffect(() => { + if (isOpen) { + const handleResize = () => updateDropdownPosition(); + const handleScroll = () => updateDropdownPosition(); + + window.addEventListener("resize", handleResize); + window.addEventListener("scroll", handleScroll, true); + + updateDropdownPosition(); + + return () => { + window.removeEventListener("resize", handleResize); + window.removeEventListener("scroll", handleScroll, true); + }; + } + }, [isOpen]); + + const displayText = loading + ? "Updating..." + : assignee?.name + ? assignee.name + : isOpen && users.length === 0 + ? "Loading..." + : t("projects.unassigned"); + + if (!showDropdown) { + return ( + + {displayText} + + ); + } + + return ( +
+ {" "} + {/* Assignee Options Dropdown */} + {isOpen && ( +
+ {/* Unassigned option */} + + + {/* User options */} + {users.map((user) => ( + + ))} +
+ )}{" "} + {/* Backdrop */} + {isOpen && ( +
{ + setIsOpen(false); + }} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/src/lib/queries/projects.js b/src/lib/queries/projects.js index 34160f3..7712df6 100644 --- a/src/lib/queries/projects.js +++ b/src/lib/queries/projects.js @@ -219,9 +219,16 @@ export function getProjectWithContract(id) { c.contract_number, c.contract_name, c.customer, - c.investor + c.investor, + creator.name as created_by_name, + creator.username as created_by_username, + assignee.name as assigned_to_name, + assignee.username as assigned_to_username, + assignee.initial as assigned_to_initial FROM projects p LEFT JOIN contracts c ON p.contract_id = c.contract_id + LEFT JOIN users creator ON p.created_by = creator.id + LEFT JOIN users assignee ON p.assigned_to = assignee.id WHERE p.project_id = ? ` )