6a9fc06f0e
- frontend/lib/api/substitute.ts: 7개 API 함수 (Record<string, any> 컨벤션)
- components/admin/SubstituteSection.tsx (신규): 관리자용 대무자 지정 섹션
· 활성/예정 대무 관계 테이블, 사전 겹침 검증
· v5 토큰 (--v5-surface-solid, --v5-glow-sm) 사용, blur 금지
- components/admin/UserFormModal.tsx: 수정 모드일 때 SubstituteSection 노출
- components/layout/MySubstituteView.tsx (신규): ProfileModal 용 read-only 조회
· 내 대무자 + 내가 대무 중인 사람 양방향, D-day 카운트다운
- components/layout/ProfileModal.tsx: MySubstituteView 삽입
- app/(main)/approval/page.tsx: 대기함 행에 "대무 ← {원본 결재자}" 뱃지
· currentUser.user_id !== line.approver_id 비교 (별도 타입 필드 X)
116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
import { getMySubstitutes } from "@/lib/api/substitute";
|
|
|
|
/**
|
|
* ProfileModal 내부 read-only 섹션: 내가 위임한 / 나를 대무 중인 관계.
|
|
*
|
|
* Spec: .omc/specs/deep-dive-user-substitute-management.md
|
|
* Plan: .omc/plans/autopilot-impl.md (T12)
|
|
*
|
|
* 지정/수정/해지 버튼 없음 — 관리자가 admin/userMng 에서만 변경 가능 (B4).
|
|
*/
|
|
export function MySubstituteView({ isVisible }: { isVisible: boolean }) {
|
|
const [proxyingForMe, setProxyingForMe] = useState<Record<string, any>[]>([]);
|
|
const [myProxies, setMyProxies] = useState<Record<string, any>[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!isVisible) return;
|
|
let cancelled = false;
|
|
(async () => {
|
|
setLoading(true);
|
|
const res = await getMySubstitutes();
|
|
if (cancelled) return;
|
|
if (res.success && res.data) {
|
|
setProxyingForMe(res.data.proxying_for_me ?? []);
|
|
setMyProxies(res.data.my_proxies ?? []);
|
|
}
|
|
setLoading(false);
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [isVisible]);
|
|
|
|
const statusBadge = (row: Record<string, any>) => {
|
|
const status = row.status as string | undefined;
|
|
const baseClass =
|
|
"ml-1 inline-block rounded px-1.5 py-0.5 text-[0.65rem] font-medium border";
|
|
if (status === "active") return <span className={`${baseClass} border-emerald-500 text-emerald-600`}>활성</span>;
|
|
if (status === "upcoming") return <span className={`${baseClass} border-blue-500 text-blue-600`}>예정</span>;
|
|
return null;
|
|
};
|
|
|
|
const renderRow = (r: Record<string, any>, target: "proxy" | "original") => {
|
|
const personName = target === "proxy"
|
|
? r.proxy_user_name || r.proxy_user_id
|
|
: r.original_user_name || r.original_user_id;
|
|
const deptName = target === "proxy" ? r.proxy_dept_name : r.original_dept_name;
|
|
const days = r.days_remaining;
|
|
return (
|
|
<div
|
|
key={r.substitute_id}
|
|
className="flex items-center justify-between border-b border-border/40 py-1.5 text-xs"
|
|
>
|
|
<div>
|
|
<span className="font-medium">{personName}</span>
|
|
{deptName && <span className="ml-1 text-muted-foreground">({deptName})</span>}
|
|
{statusBadge(r)}
|
|
</div>
|
|
<div className="text-muted-foreground">
|
|
{r.start_date ?? "즉시"} ~ {r.end_date}
|
|
{typeof days === "number" && days >= 0 && (
|
|
<span className="ml-2 text-foreground">D-{days}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="rounded-lg border border-border p-3"
|
|
style={{
|
|
background: "var(--v5-surface-solid, hsl(var(--background)))",
|
|
boxShadow: "var(--v5-glow-sm)",
|
|
}}
|
|
>
|
|
<div className="mb-2 text-sm font-semibold">나의 대무 관계</div>
|
|
|
|
{loading ? (
|
|
<div className="text-xs text-muted-foreground">불러오는 중...</div>
|
|
) : (
|
|
<>
|
|
<div className="mb-3">
|
|
<div className="mb-1 text-[0.7rem] font-medium text-muted-foreground">
|
|
내 대무자 (내 부재 시 대신 처리할 사람)
|
|
</div>
|
|
{proxyingForMe.length === 0 ? (
|
|
<div className="text-xs text-muted-foreground">없음</div>
|
|
) : (
|
|
proxyingForMe.map((r) => renderRow(r, "proxy"))
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<div className="mb-1 text-[0.7rem] font-medium text-muted-foreground">
|
|
내가 대무 중인 사람 (현재 권한을 빌려쓰고 있음)
|
|
</div>
|
|
{myProxies.length === 0 ? (
|
|
<div className="text-xs text-muted-foreground">없음</div>
|
|
) : (
|
|
myProxies.map((r) => renderRow(r, "original"))
|
|
)}
|
|
</div>
|
|
|
|
<p className="mt-2 text-[0.65rem] text-muted-foreground">
|
|
대무자 지정/해지는 관리자만 가능합니다.
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|