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 (
|
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 });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user