diff --git a/src/lib/api.ts b/src/lib/api.ts index 87a6cf4..26cf0a4 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -2,9 +2,9 @@ import axios from "axios"; import type { Account, AcctSummary, ApprovalQueue, Attendance, AuditLog, ClientContact, Company, Contract, ContractFile, Dashboard, Department, DirectoryEntry, IncentiveConfig, - LeaveBalance, LeaveRequest, Me, Member, MyIncentive, NavItem, Notification, - OvertimeRequest, PaymentSplit, PaymentStage, Product, Project, ProjectMember, - ProjectTask, Settlement, SimResult, TaskComment, TaxRecord, Timesheet, Transaction, + LeaveBalance, LeaveRequest, MailNote, Me, Member, MyIncentive, NavItem, Notification, + OvertimeRequest, PaymentSplit, PaymentStage, Product, Project, ProjectMailsResponse, + ProjectMember, ProjectTask, Settlement, SimResult, TaskComment, TaxRecord, Timesheet, Transaction, UserIncentive, Version, WorkPolicy, WorkStatusEvent, WorkStatusKind, } from "@/types"; @@ -142,6 +142,13 @@ export const updateTask = (tId: string, b: Partial) => api.patch(`/tasks/${tId}`, b).then((r) => r.data); export const deleteTask = (tId: string) => api.delete(`/tasks/${tId}`).then((r) => r.data); +export const getProjectMails = (id: string) => + api.get(`/projects/${id}/mails`).then((r) => r.data); +export const getMailNotes = (id: string) => + api.get(`/projects/${id}/mail-notes`).then((r) => r.data); +export const putMailNote = (id: string, messageId: string, body: string) => + api.put(`/projects/${id}/mail-notes`, { messageId, body }).then((r) => r.data); + export const getTaskComments = (tId: string) => api.get(`/tasks/${tId}/comments`).then((r) => r.data); export const createTaskComment = (tId: string, body: string) => diff --git a/src/pages/ProjectDetail.tsx b/src/pages/ProjectDetail.tsx index 9587fe1..511b35d 100644 --- a/src/pages/ProjectDetail.tsx +++ b/src/pages/ProjectDetail.tsx @@ -3,11 +3,13 @@ import { useParams, Link } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Plus, GanttChartSquare, Columns3, CalendarDays, Trash2, Upload, Download, Lock, Pencil, + Mail, ChevronDown, ChevronRight, } from "lucide-react"; import { getProject, getProjectMembers, getContacts, getTasks, getContract, getContractFiles, getPayments, upsertProjectMember, deleteProjectMember, createTask, updateTask, deleteTask, getTaskComments, createTaskComment, deleteTaskComment, + getProjectMails, getMailNotes, putMailNote, upsertContact, deleteContact, putContract, uploadContractFile, getFileDownloadUrl, deleteContractFile, createPayment, updatePayment, deletePayment, recomputeProject, updateProject, @@ -26,7 +28,7 @@ import { formatDate, formatDateTime, formatWon, formatSize, PROJECT_STATUS_META, LANE_LABELS, PRIORITY_META, classNames, } from "@/lib/format"; -import type { Lane, PaymentSplit, Project, ProjectTask, TaskPriority } from "@/types"; +import type { Lane, MailNote, PaymentSplit, Project, ProjectMail, ProjectTask, TaskPriority } from "@/types"; export function ProjectDetailPage() { const { id = "" } = useParams(); @@ -44,6 +46,7 @@ export function ProjectDetailPage() { { key: "overview", label: "개요" }, { key: "members", label: "작업자" }, { key: "timeline", label: "타임라인" }, + { key: "mail", label: "메일" }, { key: "contacts", label: "업체 담당자" }, ...(isAdmin ? [{ key: "contract", label: "계약 · 정산" }] : []), ]; @@ -63,6 +66,7 @@ export function ProjectDetailPage() { {tab === "overview" && } {tab === "members" && } {tab === "timeline" && } + {tab === "mail" && } {tab === "contacts" && } {tab === "contract" && isAdmin && } @@ -478,6 +482,91 @@ function Contacts({ projectId, isAdmin }: { projectId: string; isAdmin: boolean ); } +/* ---- project mail (Google Workspace) + 공동 메모 ---- */ +function MailTab({ projectId }: { projectId: string }) { + const { nameOf } = useDirectory(); + const q = useQuery({ queryKey: ["mails", projectId], queryFn: () => getProjectMails(projectId) }); + const notesQ = useQuery({ queryKey: ["mail-notes", projectId], queryFn: () => getMailNotes(projectId) }); + const noteByMsg = new Map((notesQ.data ?? []).map((n) => [n.messageId, n])); + + if (q.isLoading) return ; + const data = q.data; + if (!data?.enabled) { + return } + description="관리자가 Google Workspace 서비스계정(도메인 위임)을 연결하면, 이 프로젝트 고객사 도메인과 주고받은 메일이 팀 메일함에서 모여 표시됩니다." />; + } + if (!data.domain) { + return } + description="‘프로젝트 수정’에서 고객사 메일 도메인(예: meditech.com)을 입력하면 관련 메일을 모아 봅니다." />; + } + return ( +
+
+

+ @{data.domain} 와(과) 주고받은 메일 · 프로젝트 팀 메일함 집계 +

+ {data.error && 일부 메일함을 읽지 못했습니다} +
+ {data.messages.length === 0 ? ( + } description="팀 구성원이 이 고객사 도메인과 주고받은 메일이 없습니다." /> + ) : ( +
+ {data.messages.map((m) => ( + + ))} +
+ )} +
+ ); +} + +function MailRow({ projectId, mail, note, nameOf }: { projectId: string; mail: ProjectMail; note?: MailNote; nameOf: (e?: string | null) => string }) { + const qc = useQueryClient(); + const [open, setOpen] = useState(false); + const [memo, setMemo] = useState(note?.body ?? ""); + useEffect(() => { setMemo(note?.body ?? ""); }, [note?.updatedAt]); + const save = useMutation({ + mutationFn: () => putMailNote(projectId, mail.id, memo), + onSuccess: () => qc.invalidateQueries({ queryKey: ["mail-notes", projectId] }), + }); + const when = mail.ts ? formatDateTime(new Date(mail.ts).toISOString()) : mail.date; + + return ( + + + {open && ( +
+
+
보낸사람 {mail.from}
+
받는사람 {mail.to}
+
발견된 메일함 {nameOf(mail.mailbox)} ({mail.mailbox})
+
+
+
+ 공동 메모 · 프로젝트 구성원 누구나 수정 + {note?.lastEditedBy && 마지막 수정 {nameOf(note.lastEditedBy)}} +
+