feat(proc-payments): 입금완료 건 수정/입금취소 가능
입금완료(PAID) 행 동작에 [수정] 버튼 추가 — 입금일/입금액/방법/메모 수정, 또는 [입금 취소]로 입금완료 해제(입고 진행 상태로 복원 + 입금정보 삭제). - 신규 /api/m/admin/proc-payments/update (action: edit | cancel). - REQUESTED/PARTIAL/RECEIVED 행 동작은 기존 그대로 유지.
This commit is contained in:
@@ -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 (
|
||||
<div className="space-y-3">
|
||||
{busy && <Loading message="저장 중..." />}
|
||||
@@ -187,9 +255,17 @@ export default function ProcPaymentsPage() {
|
||||
{p.STATUS === "REQUESTED" ? (
|
||||
<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>
|
||||
) : 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">
|
||||
입금일 {p.PAID_DATE} · ₩{fmt(p.PAID_AMOUNT)} {p.PAID_METHOD && `· ${p.PAID_METHOD}`}
|
||||
<div className="text-[11px] text-slate-500">
|
||||
입금일 {p.PAID_DATE ?? "-"} · ₩{fmt(p.PAID_AMOUNT)} {p.PAID_METHOD && `· ${p.PAID_METHOD}`}
|
||||
</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">
|
||||
입금 처리
|
||||
</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>}
|
||||
</td>
|
||||
</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 });
|
||||
}
|
||||
Reference in New Issue
Block a user