feat(proc-payments): 입금완료 건 수정/입금취소 가능

입금완료(PAID) 행 동작에 [수정] 버튼 추가 — 입금일/입금액/방법/메모 수정,
또는 [입금 취소]로 입금완료 해제(입고 진행 상태로 복원 + 입금정보 삭제).
- 신규 /api/m/admin/proc-payments/update (action: edit | cancel).
- REQUESTED/PARTIAL/RECEIVED 행 동작은 기존 그대로 유지.
This commit is contained in:
chpark
2026-05-27 00:55:57 +09:00
parent 252bab500b
commit 3bfb4f31e2
2 changed files with 144 additions and 2 deletions
+83 -2
View File
@@ -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: `
<div class="text-left text-sm space-y-2">
<div><b>발주번호</b> ${p.PROC_NO}</div>
<div><b>공급업체</b> ${p.VENDOR_NAME ?? "-"}</div>
<div><b>발주금액</b> ₩${fmt(p.TOTAL_AMOUNT)}</div>
</div>
<div class="mt-3 space-y-2 text-left">
<label class="text-xs text-slate-500">입금일</label>
<input id="sw-date" class="swal2-input" type="date" value="${p.PAID_DATE || today}" />
<label class="text-xs text-slate-500">입금 금액</label>
<input id="sw-amount" class="swal2-input" type="number" value="${p.PAID_AMOUNT ?? p.TOTAL_AMOUNT}" />
<input id="sw-method" class="swal2-input" placeholder="입금 방법 (예: 계좌이체)" value="${p.PAID_METHOD ?? ""}" />
<input id="sw-memo" class="swal2-input" placeholder="메모 (선택)" value="${p.PAID_MEMO ?? ""}" />
</div>
`,
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 ( return (
<div className="space-y-3"> <div className="space-y-3">
{busy && <Loading message="저장 중..." />} {busy && <Loading message="저장 중..." />}
@@ -187,9 +255,17 @@ export default function ProcPaymentsPage() {
{p.STATUS === "REQUESTED" ? ( {p.STATUS === "REQUESTED" ? (
<button onClick={() => onPay(p)} disabled={busy} <button onClick={() => onPay(p)} disabled={busy}
className="w-full h-8 rounded bg-emerald-700 text-white text-xs font-bold disabled:opacity-50"> </button> className="w-full h-8 rounded bg-emerald-700 text-white text-xs font-bold disabled:opacity-50"> </button>
) : p.STATUS === "PAID" ? (
<div className="flex items-center justify-between gap-2">
<div className="text-[11px] text-emerald-700 min-w-0 truncate">
{p.PAID_DATE} · {fmt(p.PAID_AMOUNT)} {p.PAID_METHOD && `· ${p.PAID_METHOD}`}
</div>
<button onClick={() => onEdit(p)} disabled={busy}
className="shrink-0 h-7 px-2 rounded border border-slate-300 bg-white text-slate-600 text-[11px] font-bold disabled:opacity-50"></button>
</div>
) : ( ) : (
<div className="text-[11px] text-emerald-700"> <div className="text-[11px] text-slate-500">
{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}`}
</div> </div>
)} )}
</div> </div>
@@ -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"> className="h-7 px-2 rounded bg-emerald-700 text-white text-[11px] font-bold disabled:opacity-50">
</button> </button>
) : p.STATUS === "PAID" ? (
<button onClick={() => onEdit(p)} disabled={busy}
className="h-7 px-2 rounded border border-slate-300 bg-white text-slate-600 text-[11px] font-bold hover:bg-slate-50 disabled:opacity-50">
</button>
) : <span className="text-[11px] text-slate-400"></span>} ) : <span className="text-[11px] text-slate-400"></span>}
</td> </td>
</tr> </tr>
@@ -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 });
}