feat: Enhance approval request handling and user management

- Updated the approval request controller to include target_record_id in query parameters for improved filtering.
- Refactored the approval request creation logic to merge approval_mode into target_record_data, allowing for better handling of approval processes.
- Enhanced the user search functionality in the approval request modal to accommodate additional user attributes such as position and department.
- Improved error handling messages for clarity regarding required fields in the approval request modal.
- Added new menu item for accessing the approval box directly from user dropdown and app layout.

Made-with: Cursor
This commit is contained in:
DDD1542
2026-03-04 18:26:16 +09:00
parent c22b468599
commit f6a2668bdc
18 changed files with 2054 additions and 65 deletions
@@ -41,12 +41,16 @@ interface ApprovalRequestModalProps {
}
interface UserSearchResult {
user_id: string;
user_name: string;
userId: string;
userName: string;
positionName?: string;
deptName?: string;
deptCode?: string;
email?: string;
user_id?: string;
user_name?: string;
position_name?: string;
dept_name?: string;
dept_code?: string;
email?: string;
}
function genId(): string {
@@ -98,10 +102,17 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
try {
const res = await getUserList({ search: query.trim(), limit: 20 });
const data = res?.data || res || [];
const users: UserSearchResult[] = Array.isArray(data) ? data : [];
// 이미 추가된 결재자 제외
const rawUsers: any[] = Array.isArray(data) ? data : [];
const users: UserSearchResult[] = rawUsers.map((u: any) => ({
userId: u.userId || u.user_id || "",
userName: u.userName || u.user_name || "",
positionName: u.positionName || u.position_name || "",
deptName: u.deptName || u.dept_name || "",
deptCode: u.deptCode || u.dept_code || "",
email: u.email || "",
}));
const existingIds = new Set(approvers.map((a) => a.user_id));
setSearchResults(users.filter((u) => !existingIds.has(u.user_id)));
setSearchResults(users.filter((u) => u.userId && !existingIds.has(u.userId)));
} catch {
setSearchResults([]);
} finally {
@@ -128,10 +139,10 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
...prev,
{
id: genId(),
user_id: user.user_id,
user_name: user.user_name,
position_name: user.position_name || "",
dept_name: user.dept_name || "",
user_id: user.userId,
user_name: user.userName,
position_name: user.positionName || "",
dept_name: user.deptName || "",
},
]);
setSearchQuery("");
@@ -162,8 +173,8 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
setError("결재자를 1명 이상 추가해주세요.");
return;
}
if (!eventDetail?.targetTable || !eventDetail?.targetRecordId) {
setError("결재 대상 정보가 없습니다. 레코드를 선택 후 다시 시도해주세요.");
if (!eventDetail?.targetTable) {
setError("결재 대상 테이블 정보가 없습니다. 버튼 설정을 확인해주세요.");
return;
}
@@ -174,11 +185,9 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
title: title.trim(),
description: description.trim() || undefined,
target_table: eventDetail.targetTable,
target_record_id: eventDetail.targetRecordId,
target_record_data: {
...eventDetail.targetRecordData,
approval_mode: approvalMode,
},
target_record_id: eventDetail.targetRecordId || undefined,
target_record_data: eventDetail.targetRecordData,
approval_mode: approvalMode,
screen_id: eventDetail.screenId,
button_component_id: eventDetail.buttonComponentId,
approvers: approvers.map((a, idx) => ({
@@ -321,7 +330,7 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
<div className="max-h-48 overflow-y-auto">
{searchResults.map((user) => (
<button
key={user.user_id}
key={user.userId}
type="button"
onClick={() => addApprover(user)}
className="flex w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-accent"
@@ -331,13 +340,13 @@ export const ApprovalRequestModal: React.FC<ApprovalRequestModalProps> = ({
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-xs font-medium sm:text-sm">
{user.user_name}
{user.userName}
<span className="text-muted-foreground ml-1 text-[10px]">
({user.user_id})
({user.userId})
</span>
</p>
<p className="text-muted-foreground truncate text-[10px]">
{[user.dept_name, user.position_name].filter(Boolean).join(" / ") || "-"}
{[user.deptName, user.positionName].filter(Boolean).join(" / ") || "-"}
</p>
</div>
<Plus className="text-muted-foreground h-4 w-4 shrink-0" />
+11
View File
@@ -18,6 +18,7 @@ import {
LogOut,
User,
Building2,
FileCheck,
} from "lucide-react";
import { useMenu } from "@/contexts/MenuContext";
import { useAuth } from "@/hooks/useAuth";
@@ -524,6 +525,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
<User className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/admin/approvalBox")}>
<FileCheck className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
@@ -692,6 +698,11 @@ function AppLayoutInner({ children }: AppLayoutProps) {
<User className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/admin/approvalBox")}>
<FileCheck className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
+9 -1
View File
@@ -8,7 +8,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { LogOut, User } from "lucide-react";
import { LogOut, User, FileCheck } from "lucide-react";
import { useRouter } from "next/navigation";
interface UserDropdownProps {
user: any;
@@ -20,6 +21,8 @@ interface UserDropdownProps {
* 사용자 드롭다운 메뉴 컴포넌트
*/
export function UserDropdown({ user, onProfileClick, onLogout }: UserDropdownProps) {
const router = useRouter();
if (!user) return null;
return (
@@ -79,6 +82,11 @@ export function UserDropdown({ user, onProfileClick, onLogout }: UserDropdownPro
<User className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/admin/approvalBox")}>
<FileCheck className="mr-2 h-4 w-4" />
<span></span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onLogout}>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
@@ -577,8 +577,10 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
const getActionDisplayName = (actionType: ButtonActionType): string => {
const displayNames: Record<ButtonActionType, string> = {
save: "저장",
cancel: "취소",
delete: "삭제",
edit: "수정",
copy: "복사",
add: "추가",
search: "검색",
reset: "초기화",
@@ -589,6 +591,9 @@ export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
newWindow: "새 창",
navigate: "페이지 이동",
control: "제어",
transferData: "데이터 전달",
quickInsert: "즉시 저장",
approval: "결재",
};
return displayNames[actionType] || actionType;
};