diff --git a/src/app/(main)/m/admin/proc-payments/page.tsx b/src/app/(main)/m/admin/proc-payments/page.tsx index 8b3e26f..2644244 100644 --- a/src/app/(main)/m/admin/proc-payments/page.tsx +++ b/src/app/(main)/m/admin/proc-payments/page.tsx @@ -125,6 +125,74 @@ export default function ProcPaymentsPage() { } }; + // 입금완료(PAID) 건 수정 — 입금액/입금일/방법/메모 수정 또는 입금 취소 + const onEdit = async (p: Proc) => { + const today = new Date().toISOString().slice(0, 10); + const result = await Swal.fire({ + title: "입금 정보 수정", + html: ` +
+
발주번호 ${p.PROC_NO}
+
공급업체 ${p.VENDOR_NAME ?? "-"}
+
발주금액 ₩${fmt(p.TOTAL_AMOUNT)}
+
+
+ + + + + + +
+ `, + showCancelButton: true, + showDenyButton: true, + confirmButtonText: "저장", + denyButtonText: "입금 취소", + cancelButtonText: "닫기", + confirmButtonColor: "#0f766e", + denyButtonColor: "#dc2626", + focusConfirm: false, + preConfirm: () => ({ + paidDate: (document.getElementById("sw-date") as HTMLInputElement)?.value || undefined, + amount: Number((document.getElementById("sw-amount") as HTMLInputElement)?.value) || Number(p.TOTAL_AMOUNT), + method: (document.getElementById("sw-method") as HTMLInputElement)?.value, + memo: (document.getElementById("sw-memo") as HTMLInputElement)?.value, + }), + }); + + if (result.isConfirmed && result.value) { + setBusy(true); + try { + const res = await fetch("/api/m/admin/proc-payments/update", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objid: p.OBJID, action: "edit", ...result.value }), + }); + const j = await res.json(); + if (j.success) { await Swal.fire({ icon: "success", title: "수정 완료", timer: 1200, showConfirmButton: false }); load(); } + else Swal.fire({ icon: "error", title: "수정 실패", text: j.message }); + } finally { setBusy(false); } + } else if (result.isDenied) { + const ok = await Swal.fire({ + icon: "warning", title: "입금을 취소하시겠습니까?", + text: "입금완료를 해제하고 입금 정보를 지웁니다. (입고 진행 상태로 되돌아갑니다)", + showCancelButton: true, confirmButtonText: "입금 취소", cancelButtonText: "닫기", + confirmButtonColor: "#dc2626", + }); + if (!ok.isConfirmed) return; + setBusy(true); + try { + const res = await fetch("/api/m/admin/proc-payments/update", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objid: p.OBJID, action: "cancel" }), + }); + const j = await res.json(); + if (j.success) { await Swal.fire({ icon: "success", title: "입금 취소됨", timer: 1200, showConfirmButton: false }); load(); } + else Swal.fire({ icon: "error", title: "취소 실패", text: j.message }); + } finally { setBusy(false); } + } + }; + return (
{busy && } @@ -187,9 +255,17 @@ export default function ProcPaymentsPage() { {p.STATUS === "REQUESTED" ? ( + ) : p.STATUS === "PAID" ? ( +
+
+ 입금일 {p.PAID_DATE} · ₩{fmt(p.PAID_AMOUNT)} {p.PAID_METHOD && `· ${p.PAID_METHOD}`} +
+ +
) : ( -
- 입금일 {p.PAID_DATE} · ₩{fmt(p.PAID_AMOUNT)} {p.PAID_METHOD && `· ${p.PAID_METHOD}`} +
+ 입금일 {p.PAID_DATE ?? "-"} · ₩{fmt(p.PAID_AMOUNT)} {p.PAID_METHOD && `· ${p.PAID_METHOD}`}
)}
@@ -233,6 +309,11 @@ export default function ProcPaymentsPage() { className="h-7 px-2 rounded bg-emerald-700 text-white text-[11px] font-bold disabled:opacity-50"> 입금 처리 + ) : p.STATUS === "PAID" ? ( + ) : 완료} diff --git a/src/app/api/m/admin/proc-payments/update/route.ts b/src/app/api/m/admin/proc-payments/update/route.ts new file mode 100644 index 0000000..e4bad77 --- /dev/null +++ b/src/app/api/m/admin/proc-payments/update/route.ts @@ -0,0 +1,61 @@ +// 입금완료(PAID) 건 수정 — 입금액/입금일/방법/메모 수정 또는 입금 취소(상태 복원) +import { NextRequest, NextResponse } from "next/server"; +import { execute, queryOne } from "@/lib/db"; +import { requireMomoAdmin } from "@/lib/momo-guard"; + +export async function POST(req: NextRequest) { + const g = await requireMomoAdmin(); + if (g instanceof NextResponse) return g; + + const body = await req.json().catch(() => ({})); + const { objid, action, amount, method, memo, paidDate } = body as { + objid?: string; + action?: "edit" | "cancel"; + amount?: number; method?: string; memo?: string; paidDate?: string; + }; + if (!objid) return NextResponse.json({ success: false, message: "objid 필수" }, { status: 400 }); + + const proc = await queryOne<{ status: string; total_amount: number }>( + `SELECT status, total_amount FROM momo_procurements WHERE objid::text = $1`, + [objid] + ); + if (!proc) return NextResponse.json({ success: false, message: "발주를 찾을 수 없습니다." }, { status: 404 }); + if (proc.status !== "PAID") { + return NextResponse.json({ success: false, message: "입금완료 건만 수정할 수 있습니다." }, { status: 400 }); + } + + // 입금 취소 — 입고 진행 상태로 복원하고 입금 정보 제거 + if (action === "cancel") { + const st = await queryOne<{ pending: string; started: string; cnt: string }>( + `SELECT COUNT(*) FILTER (WHERE COALESCE(received_qty,0) < qty) AS pending, + COUNT(*) FILTER (WHERE COALESCE(received_qty,0) > 0) AS started, + COUNT(*) AS cnt + FROM momo_procurement_items WHERE proc_objid::text = $1`, + [objid] + ); + const pending = Number(st?.pending ?? 0); + const started = Number(st?.started ?? 0); + const cnt = Number(st?.cnt ?? 0); + const restored = cnt > 0 && pending === 0 ? "RECEIVED" : started > 0 ? "PARTIAL" : "REQUESTED"; + await execute( + `UPDATE momo_procurements + SET status = $1, paid_date = NULL, paid_amount = NULL, paid_method = NULL, paid_memo = NULL + WHERE objid::text = $2`, + [restored, objid] + ); + return NextResponse.json({ success: true, status: restored }); + } + + // 입금 정보 수정 (상태는 PAID 유지) + const paidAmount = Number(amount) > 0 ? Number(amount) : Number(proc.total_amount); + await execute( + `UPDATE momo_procurements + SET paid_amount = $1, + paid_method = $2, + paid_memo = $3, + paid_date = COALESCE($4::timestamp, paid_date) + WHERE objid::text = $5`, + [paidAmount, method || null, memo || null, paidDate || null, objid] + ); + return NextResponse.json({ success: true }); +}