fix(orders+payments): SHIPPED 상태 중복 라벨 제거 + 입금관리 검색조건 추가
Deploy momo-erp / deploy (push) Successful in 48s
Deploy momo-erp / deploy (push) Successful in 48s
[출고 상태 셀렉트 중복 fix] - STATUS_LABEL 에 APPROVED='출고완료' / SHIPPED='출고완료' 둘 다 매핑돼 셀렉트 옵션에 '출고완료'가 두 번 노출됐음. 운영 DB 분포 확인 결과 SHIPPED 상태값은 0건(dead) → 라벨/색상 매핑에서 SHIPPED 제거. StatementPreview 의 'SHIPPED' OR 분기도 정리 [입금 관리 검색조건] - 시작일 / 종료일 / 입금 상태(전체·입금 전·입금완료) / 업체명·발주번호 키워드 - 기본 기간: 이번달 1일 ~ 오늘 - 입금 상태: UNPAID = APPROVED, PAID = PAID + INVOICED 묶어서 필터 - 초기화 / 조회 버튼 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,6 @@ const fmt = (n: number | string | undefined | null) =>
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
REQUESTED: "출고요청",
|
||||
APPROVED: "출고완료",
|
||||
SHIPPED: "출고완료",
|
||||
PAID: "입금완료",
|
||||
INVOICED: "계산서발행",
|
||||
CANCELLED: "취소",
|
||||
@@ -43,7 +42,6 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
REQUESTED: "bg-amber-100 text-amber-700 border-amber-200",
|
||||
APPROVED: "bg-blue-100 text-blue-700 border-blue-200",
|
||||
SHIPPED: "bg-cyan-100 text-cyan-700 border-cyan-200",
|
||||
PAID: "bg-emerald-100 text-emerald-700 border-emerald-200",
|
||||
INVOICED: "bg-violet-100 text-violet-700 border-violet-200",
|
||||
CANCELLED: "bg-slate-100 text-slate-500 border-slate-200",
|
||||
@@ -725,7 +723,7 @@ function StatementPreview({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{(order.STATUS === "APPROVED" || order.STATUS === "SHIPPED" || order.STATUS === "PAID") && (
|
||||
{(order.STATUS === "APPROVED" || order.STATUS === "PAID") && (
|
||||
<div className="flex items-center justify-between pt-3 border-t border-slate-200 gap-2 flex-wrap">
|
||||
<div className="text-[11px] text-emerald-700 inline-flex items-center gap-1">
|
||||
<Check size={12} /> 출고 완료 {order.APPROVE_DATE && `— ${order.APPROVE_DATE}`}
|
||||
|
||||
@@ -1,21 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
interface Order { OBJID: string; ORDER_NO: string; ORDER_DATE: string; COMPANY_NAME: string; STATUS: string; TOTAL_AMOUNT: number; PAID_AMOUNT: number }
|
||||
const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
|
||||
const STATUS_LABEL: Record<string, string> = { APPROVED: "출고완료", PAID: "입금완료", INVOICED: "계산서발행" };
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const todayStr = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
|
||||
const monthStartStr = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-01`; };
|
||||
|
||||
// 입금상태 필터: 전체 / 입금 전(APPROVED) / 입금완료(PAID, INVOICED)
|
||||
type PayFilter = "" | "UNPAID" | "PAID";
|
||||
|
||||
export default function PaymentsPage() {
|
||||
const [list, setList] = useState<Order[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
const res = await fetch("/api/m/orders/list", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) });
|
||||
const all = ((await res.json()).RESULTLIST ?? []) as Order[];
|
||||
setList(all.filter((o) => ["APPROVED", "PAID", "INVOICED"].includes(o.STATUS)));
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [dateFrom, setDateFrom] = useState(monthStartStr());
|
||||
const [dateTo, setDateTo] = useState(todayStr());
|
||||
const [payFilter, setPayFilter] = useState<PayFilter>("");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/m/orders/list", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
dateFrom: dateFrom || undefined,
|
||||
dateTo: dateTo || undefined,
|
||||
keyword: keyword || undefined,
|
||||
}),
|
||||
});
|
||||
const all = ((await res.json()).RESULTLIST ?? []) as Order[];
|
||||
// 출고완료 이후만 (대기/취소 제외) + 상태 필터
|
||||
const filtered = all.filter((o) => {
|
||||
if (!["APPROVED", "PAID", "INVOICED"].includes(o.STATUS)) return false;
|
||||
if (payFilter === "UNPAID" && o.STATUS !== "APPROVED") return false;
|
||||
if (payFilter === "PAID" && o.STATUS === "APPROVED") return false;
|
||||
return true;
|
||||
});
|
||||
setList(filtered);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dateFrom, dateTo, keyword, payFilter]);
|
||||
|
||||
// 최초 1회만 자동 로드. 검색 조건 변경은 [조회] 버튼으로
|
||||
useEffect(() => { load(); }, []); // eslint-disable-line
|
||||
|
||||
const onPay = async (o: Order) => {
|
||||
const remain = Number(o.TOTAL_AMOUNT) - Number(o.PAID_AMOUNT || 0);
|
||||
@@ -41,12 +76,56 @@ export default function PaymentsPage() {
|
||||
const paidCnt = list.filter((o) => o.STATUS !== "APPROVED").length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold">입금 관리</h1>
|
||||
<p className="text-xs sm:text-sm text-slate-500 mt-1">출고완료 후 거래처 입금 등록 — 완납 시 상태가 입금완료로 자동 변경됩니다.</p>
|
||||
</div>
|
||||
|
||||
{/* 검색 영역 — 모바일 1열 / sm 2열 / lg 5열 */}
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-3">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-[1fr_1fr_180px_1fr_auto] gap-2">
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-slate-500 mb-1">시작일</label>
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
className="w-full h-9 px-2 rounded-lg border border-slate-200 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-slate-500 mb-1">종료일</label>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
className="w-full h-9 px-2 rounded-lg border border-slate-200 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-slate-500 mb-1">입금 상태</label>
|
||||
<select value={payFilter} onChange={(e) => setPayFilter(e.target.value as PayFilter)}
|
||||
className="w-full h-9 px-2 rounded-lg border border-slate-200 text-sm bg-white">
|
||||
<option value="">전체</option>
|
||||
<option value="UNPAID">입금 전 (출고완료)</option>
|
||||
<option value="PAID">입금완료</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-2 sm:col-span-2 lg:col-span-1">
|
||||
<label className="block text-[11px] font-semibold text-slate-500 mb-1">업체명 / 발주번호</label>
|
||||
<input value={keyword} onChange={(e) => setKeyword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && load()}
|
||||
placeholder="검색어"
|
||||
className="w-full h-9 px-2 rounded-lg border border-slate-200 text-sm" />
|
||||
</div>
|
||||
<div className="col-span-2 sm:col-span-2 lg:col-span-1 flex items-end gap-2">
|
||||
<button
|
||||
onClick={() => { setDateFrom(monthStartStr()); setDateTo(todayStr()); setKeyword(""); setPayFilter(""); }}
|
||||
className="h-9 px-3 rounded-lg border border-slate-200 bg-white text-slate-600 text-xs font-semibold hover:bg-slate-50"
|
||||
>
|
||||
초기화
|
||||
</button>
|
||||
<button onClick={load} disabled={loading}
|
||||
className="flex-1 lg:flex-none h-9 px-4 rounded-lg bg-slate-800 text-white text-sm font-bold hover:bg-slate-900 disabled:opacity-50 inline-flex items-center justify-center gap-1.5">
|
||||
<RefreshCcw size={14} className={loading ? "animate-spin" : ""} /> 조회
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 요약 카드 */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2 sm:gap-3">
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-3">
|
||||
@@ -67,7 +146,7 @@ export default function PaymentsPage() {
|
||||
<div className="space-y-2 sm:hidden">
|
||||
{list.length === 0 ? (
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-8 text-center text-slate-400">
|
||||
출고완료 건이 없습니다.
|
||||
{loading ? "조회 중..." : "조회 결과가 없습니다."}
|
||||
</div>
|
||||
) : list.map((o) => {
|
||||
const remain = Number(o.TOTAL_AMOUNT) - Number(o.PAID_AMOUNT || 0);
|
||||
@@ -125,7 +204,7 @@ export default function PaymentsPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{list.length === 0 ? (
|
||||
<tr><td colSpan={8} className="text-center py-12 text-slate-400">출고완료 건이 없습니다.</td></tr>
|
||||
<tr><td colSpan={8} className="text-center py-12 text-slate-400">{loading ? "조회 중..." : "조회 결과가 없습니다."}</td></tr>
|
||||
) : list.map((o) => {
|
||||
const remain = Number(o.TOTAL_AMOUNT) - Number(o.PAID_AMOUNT || 0);
|
||||
return (
|
||||
|
||||
@@ -17,13 +17,12 @@ const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const todayISO = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; };
|
||||
const firstOfMonthISO = () => { const d = new Date(); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-01`; };
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
REQUESTED: "출고요청", APPROVED: "출고완료", SHIPPED: "출고완료",
|
||||
REQUESTED: "출고요청", APPROVED: "출고완료",
|
||||
PAID: "입금완료", INVOICED: "계산서발행", CANCELLED: "취소",
|
||||
};
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
REQUESTED: "bg-amber-100 text-amber-700",
|
||||
APPROVED: "bg-blue-100 text-blue-700",
|
||||
SHIPPED: "bg-cyan-100 text-cyan-700",
|
||||
INVOICED: "bg-violet-100 text-violet-700",
|
||||
PAID: "bg-emerald-100 text-emerald-700",
|
||||
CANCELLED: "bg-slate-100 text-slate-500",
|
||||
|
||||
@@ -31,13 +31,12 @@ interface Supplier {
|
||||
|
||||
const fmt = (n: number | string | undefined) => Number(n || 0).toLocaleString("ko-KR");
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
REQUESTED: "출고요청", APPROVED: "출고완료", SHIPPED: "출고완료",
|
||||
REQUESTED: "출고요청", APPROVED: "출고완료",
|
||||
PAID: "입금완료", INVOICED: "계산서발행", CANCELLED: "취소",
|
||||
};
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
REQUESTED: "bg-amber-100 text-amber-700",
|
||||
APPROVED: "bg-blue-100 text-blue-700",
|
||||
SHIPPED: "bg-cyan-100 text-cyan-700",
|
||||
INVOICED: "bg-violet-100 text-violet-700",
|
||||
PAID: "bg-emerald-100 text-emerald-700",
|
||||
CANCELLED: "bg-slate-100 text-slate-500",
|
||||
|
||||
Reference in New Issue
Block a user