A. 유통기한 임박 알림 (/m/admin/expiry-alerts) - momo_inbounds.expiry_date/completed_by 컬럼 운영 DB 추가 - inbounds/save API: 입고 시 expiryDate/completedBy 함께 저장 - 페이지: 만료/7일이내/30일이내 분류 카드 + 행별 D-N 뱃지 - 빠른 필터 (7/14/30/60/90일) B. 창고 이동 통계 (/m/admin/transfers) - stock_moves WHERE ref_type='TRANSFER' AND move_type='OUT' 기준 - 출발창고 / 도착창고 / 품목 / 수량 / 단가(cost_price) / 금액 - 이동자(regid + user_name), 이동일시, 메모 - 합계 카드 + 엑셀 다운로드 운영 DB: - ALTER TABLE momo_inbounds ADD COLUMN expiry_date, completed_by - 인덱스 idx_momo_inbounds_expiry - menu_info: 9000511 (유통기한), 9000512 (창고이동) 등록 — 통계 메뉴 산하
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { AlertTriangle, AlertCircle, Clock, RefreshCcw } from "lucide-react";
|
||||
|
||||
interface Row {
|
||||
INBOUND_OBJID: string; INBOUND_NO: string;
|
||||
INBOUND_DATE: string; EXPIRY_DATE: string; DAYS_LEFT: number;
|
||||
WH_NAME: string | null; VENDOR_NAME: string | null;
|
||||
COMPLETED_BY: string | null; MEMO: string | null; TOTAL_AMOUNT: number;
|
||||
}
|
||||
|
||||
const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
|
||||
|
||||
export default function ExpiryAlertsPage() {
|
||||
const [days, setDays] = useState(30);
|
||||
const [rows, setRows] = useState<Row[]>([]);
|
||||
const [counts, setCounts] = useState({ expired: 0, urgent: 0, soon: 0 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/m/admin/expiry-alerts", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ days }),
|
||||
});
|
||||
const j = await res.json();
|
||||
setRows(j.RESULTLIST ?? []);
|
||||
setCounts({ expired: j.EXPIRED_CNT ?? 0, urgent: j.URGENT_CNT ?? 0, soon: j.SOON_CNT ?? 0 });
|
||||
} finally { setLoading(false); }
|
||||
}, [days]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const rowStyle = (daysLeft: number) => {
|
||||
if (daysLeft < 0) return "bg-rose-50 border-l-4 border-l-rose-500";
|
||||
if (daysLeft <= 7) return "bg-amber-50 border-l-4 border-l-amber-500";
|
||||
return "bg-white";
|
||||
};
|
||||
const badge = (daysLeft: number) => {
|
||||
if (daysLeft < 0) return <span className="text-[10px] px-1.5 py-0.5 rounded bg-rose-100 text-rose-700 font-bold">만료 {Math.abs(daysLeft)}일 경과</span>;
|
||||
if (daysLeft === 0) return <span className="text-[10px] px-1.5 py-0.5 rounded bg-rose-100 text-rose-700 font-bold">오늘 만료</span>;
|
||||
if (daysLeft <= 7) return <span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 font-bold">D-{daysLeft}</span>;
|
||||
return <span className="text-[10px] px-1.5 py-0.5 rounded bg-slate-100 text-slate-600">D-{daysLeft}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-end justify-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold flex items-center gap-2">
|
||||
<AlertTriangle size={20} className="text-amber-600" />
|
||||
유통기한 임박 알림
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
입고 시 입력한 소비기한 기준. 만료 / D-7 이내 / D-30 이내 분류. 관리자 그룹 전용.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select value={days} onChange={(e) => setDays(Number(e.target.value))}
|
||||
className="h-9 px-3 rounded border border-slate-300 bg-white text-sm">
|
||||
<option value={7}>7일 이내</option>
|
||||
<option value={14}>14일 이내</option>
|
||||
<option value={30}>30일 이내</option>
|
||||
<option value={60}>60일 이내</option>
|
||||
<option value={90}>90일 이내</option>
|
||||
</select>
|
||||
<button onClick={load} disabled={loading}
|
||||
className="h-9 px-3 rounded bg-slate-800 text-white text-sm font-bold inline-flex items-center gap-1 hover:bg-slate-900 disabled:opacity-50">
|
||||
<RefreshCcw size={14} /> 조회
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 알림 카드 */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="bg-rose-50 border border-rose-200 rounded-xl p-4">
|
||||
<div className="text-[11px] text-rose-700 font-semibold mb-1 flex items-center gap-1">
|
||||
<AlertCircle size={14} /> 이미 만료
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-rose-700 tabular-nums">{counts.expired}건</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
|
||||
<div className="text-[11px] text-amber-700 font-semibold mb-1 flex items-center gap-1">
|
||||
<AlertTriangle size={14} /> 7일 이내 임박
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-amber-700 tabular-nums">{counts.urgent}건</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
||||
<div className="text-[11px] text-slate-600 font-semibold mb-1 flex items-center gap-1">
|
||||
<Clock size={14} /> 30일 이내 주의
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-slate-700 tabular-nums">{counts.soon}건</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 리스트 */}
|
||||
<div className="bg-white border border-slate-200 rounded-xl overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 text-slate-600">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2">입고번호</th>
|
||||
<th className="text-left px-3 py-2">소비기한</th>
|
||||
<th className="text-center px-3 py-2">남은일</th>
|
||||
<th className="text-left px-3 py-2">창고</th>
|
||||
<th className="text-left px-3 py-2">공급업체</th>
|
||||
<th className="text-left px-3 py-2">입고완료자</th>
|
||||
<th className="text-left px-3 py-2">메모</th>
|
||||
<th className="text-right px-3 py-2">입고금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tabular-nums">
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={8} className="text-center py-12 text-slate-400">
|
||||
{loading ? "조회 중..." : "임박한 유통기한이 없습니다."}
|
||||
</td></tr>
|
||||
) : rows.map((r) => (
|
||||
<tr key={r.INBOUND_OBJID} className={`border-t border-slate-100 ${rowStyle(Number(r.DAYS_LEFT))}`}>
|
||||
<td className="px-3 py-2 font-semibold">{r.INBOUND_NO}
|
||||
<div className="text-[10px] text-slate-400">입고 {r.INBOUND_DATE}</div></td>
|
||||
<td className="px-3 py-2 font-mono">{r.EXPIRY_DATE}</td>
|
||||
<td className="px-3 py-2 text-center">{badge(Number(r.DAYS_LEFT))}</td>
|
||||
<td className="px-3 py-2">{r.WH_NAME ?? "-"}</td>
|
||||
<td className="px-3 py-2">{r.VENDOR_NAME ?? "-"}</td>
|
||||
<td className="px-3 py-2 text-xs">{r.COMPLETED_BY ?? "-"}</td>
|
||||
<td className="px-3 py-2 text-[10px] text-slate-500 whitespace-pre-line max-w-[300px] truncate" title={r.MEMO ?? ""}>
|
||||
{r.MEMO ?? ""}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right font-semibold">₩{fmt(Number(r.TOTAL_AMOUNT))}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -188,6 +188,8 @@ export default function InboundsPage() {
|
||||
whObjid,
|
||||
lines: whLines,
|
||||
memo: checklistMemo,
|
||||
expiryDate: checklist.expiryDate || undefined,
|
||||
completedBy: checklist.completedBy || undefined,
|
||||
}),
|
||||
});
|
||||
const j = await res.json();
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ArrowRightLeft, RefreshCcw, Download } from "lucide-react";
|
||||
import { downloadXlsx } from "@/lib/xlsx-export";
|
||||
|
||||
interface Row {
|
||||
OBJID: string; MOVED_AT: string;
|
||||
ITEM_CODE: string; ITEM_NAME: string; UNIT: string;
|
||||
COST_PRICE: number; QTY: number; AMOUNT: number;
|
||||
FROM_WH: string; FROM_CODE: string;
|
||||
TO_WH: string; TO_CODE: string;
|
||||
MEMO: string | null; REGID: string; REG_USER_NAME: string | null;
|
||||
}
|
||||
|
||||
const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
|
||||
const todayStr = () => new Date().toISOString().slice(0, 10);
|
||||
const monthAgo = () => { const d = new Date(); d.setMonth(d.getMonth() - 1); return d.toISOString().slice(0, 10); };
|
||||
|
||||
export default function TransfersPage() {
|
||||
const [dateFrom, setDateFrom] = useState(monthAgo());
|
||||
const [dateTo, setDateTo] = useState(todayStr());
|
||||
const [rows, setRows] = useState<Row[]>([]);
|
||||
const [totals, setTotals] = useState({ qty: 0, amount: 0 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/m/admin/transfers", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ dateFrom, dateTo }),
|
||||
});
|
||||
const j = await res.json();
|
||||
setRows(j.RESULTLIST ?? []);
|
||||
setTotals({ qty: Number(j.TOTAL_QTY ?? 0), amount: Number(j.TOTAL_AMOUNT ?? 0) });
|
||||
} finally { setLoading(false); }
|
||||
}, [dateFrom, dateTo]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const onExport = () => {
|
||||
if (rows.length === 0) return;
|
||||
downloadXlsx(`창고이동_${dateFrom}_${dateTo}`, rows, [
|
||||
{ header: "이동일시", key: "MOVED_AT" },
|
||||
{ header: "품목코드", key: "ITEM_CODE" },
|
||||
{ header: "품목명", key: "ITEM_NAME" },
|
||||
{ header: "단위", key: "UNIT" },
|
||||
{ header: "수량", key: (r) => Number(r.QTY) },
|
||||
{ header: "단가", key: (r) => Number(r.COST_PRICE) },
|
||||
{ header: "금액", key: (r) => Number(r.AMOUNT) },
|
||||
{ header: "출발창고", key: (r) => `${r.FROM_WH ?? ""} (${r.FROM_CODE ?? ""})` },
|
||||
{ header: "도착창고", key: (r) => `${r.TO_WH ?? ""} (${r.TO_CODE ?? ""})` },
|
||||
{ header: "메모", key: (r) => r.MEMO ?? "" },
|
||||
{ header: "이동자ID", key: "REGID" },
|
||||
{ header: "이동자", key: (r) => r.REG_USER_NAME ?? "" },
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-end justify-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold flex items-center gap-2">
|
||||
<ArrowRightLeft size={20} className="text-sky-700" />
|
||||
창고 이동 통계
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
창고 간 이동 이력 (TRANSFER). 수량 × 단가(cost_price) = 이동 금액. 이동자/날짜/시간 포함.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
className="h-9 px-3 rounded border border-slate-300 text-sm" />
|
||||
<span className="text-slate-400">~</span>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
className="h-9 px-3 rounded border border-slate-300 text-sm" />
|
||||
<button onClick={load} disabled={loading}
|
||||
className="h-9 px-3 rounded bg-slate-800 text-white text-sm font-bold inline-flex items-center gap-1 hover:bg-slate-900 disabled:opacity-50">
|
||||
<RefreshCcw size={14} /> 조회
|
||||
</button>
|
||||
<button onClick={onExport} disabled={rows.length === 0}
|
||||
className="h-9 px-3 rounded bg-emerald-700 text-white text-sm font-bold inline-flex items-center gap-1 hover:bg-emerald-800 disabled:opacity-50">
|
||||
<Download size={14} /> 엑셀
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 합계 카드 */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<div className="text-[11px] text-slate-500 mb-1">이동 건수</div>
|
||||
<div className="text-xl font-bold tabular-nums">{fmt(rows.length)}건</div>
|
||||
</div>
|
||||
<div className="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<div className="text-[11px] text-slate-500 mb-1">총 이동 수량</div>
|
||||
<div className="text-xl font-bold tabular-nums">{fmt(totals.qty)}</div>
|
||||
</div>
|
||||
<div className="bg-sky-50 border border-sky-200 rounded-xl p-4">
|
||||
<div className="text-[11px] text-sky-700 mb-1 font-semibold">총 이동 금액 (단가 기준)</div>
|
||||
<div className="text-xl font-bold text-sky-700 tabular-nums">₩{fmt(totals.amount)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 리스트 */}
|
||||
<div className="bg-white border border-slate-200 rounded-xl overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 text-slate-600 text-xs">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2">이동일시</th>
|
||||
<th className="text-left px-3 py-2">품목</th>
|
||||
<th className="text-right px-3 py-2">수량</th>
|
||||
<th className="text-right px-3 py-2">단가</th>
|
||||
<th className="text-right px-3 py-2">금액</th>
|
||||
<th className="text-left px-3 py-2">출발 → 도착</th>
|
||||
<th className="text-left px-3 py-2">이동자</th>
|
||||
<th className="text-left px-3 py-2">메모</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tabular-nums">
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={8} className="text-center py-12 text-slate-400">
|
||||
{loading ? "조회 중..." : "이동 내역이 없습니다."}
|
||||
</td></tr>
|
||||
) : rows.map((r) => (
|
||||
<tr key={r.OBJID} className="border-t border-slate-100 hover:bg-slate-50">
|
||||
<td className="px-3 py-2 text-xs font-mono">{r.MOVED_AT}</td>
|
||||
<td className="px-3 py-2">
|
||||
<div className="font-semibold">{r.ITEM_NAME}</div>
|
||||
<div className="text-[10px] text-slate-400">{r.ITEM_CODE}</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right font-semibold">{fmt(Number(r.QTY))} {r.UNIT}</td>
|
||||
<td className="px-3 py-2 text-right text-slate-500">{fmt(Number(r.COST_PRICE))}</td>
|
||||
<td className="px-3 py-2 text-right font-bold text-sky-700">₩{fmt(Number(r.AMOUNT))}</td>
|
||||
<td className="px-3 py-2 text-xs">
|
||||
<span className="text-slate-700">{r.FROM_WH}</span>
|
||||
<span className="text-slate-300 mx-1">→</span>
|
||||
<span className="text-emerald-700 font-semibold">{r.TO_WH}</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs">
|
||||
<div className="font-semibold">{r.REG_USER_NAME ?? "-"}</div>
|
||||
<div className="text-[10px] text-slate-400 font-mono">{r.REGID}</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-[10px] text-slate-500 max-w-[200px] truncate" title={r.MEMO ?? ""}>
|
||||
{r.MEMO ?? ""}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 유통기한 임박 알림 — momo_inbounds.expiry_date 기준 N일 이내
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } 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 days = Number(body.days) || 30; // 기본 30일 이내
|
||||
|
||||
// 입고건 중 expiry_date 가 있고 today + days 이내인 행. 이미 지난 것(과기) 도 포함.
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
I.objid AS "INBOUND_OBJID",
|
||||
I.inbound_no AS "INBOUND_NO",
|
||||
TO_CHAR(I.inbound_date, 'YYYY-MM-DD') AS "INBOUND_DATE",
|
||||
TO_CHAR(I.expiry_date, 'YYYY-MM-DD') AS "EXPIRY_DATE",
|
||||
(I.expiry_date - CURRENT_DATE) AS "DAYS_LEFT",
|
||||
W.wh_name AS "WH_NAME",
|
||||
V.supply_name AS "VENDOR_NAME",
|
||||
I.completed_by AS "COMPLETED_BY",
|
||||
I.memo AS "MEMO",
|
||||
I.total_amount AS "TOTAL_AMOUNT"
|
||||
FROM momo_inbounds I
|
||||
LEFT JOIN momo_warehouses W ON W.objid = I.wh_objid
|
||||
LEFT JOIN supply_mng V ON V.objid::text = I.vendor_objid
|
||||
WHERE I.expiry_date IS NOT NULL
|
||||
AND I.expiry_date <= CURRENT_DATE + ($1 || ' days')::interval
|
||||
AND COALESCE(I.is_del,'N') != 'Y'
|
||||
ORDER BY I.expiry_date ASC`,
|
||||
[days]
|
||||
);
|
||||
|
||||
// 분류: 이미 만료 / 7일 이내 임박 / 30일 이내 주의
|
||||
const now = new Date();
|
||||
const expired = rows.filter((r) => Number(r.DAYS_LEFT) < 0);
|
||||
const urgent = rows.filter((r) => Number(r.DAYS_LEFT) >= 0 && Number(r.DAYS_LEFT) <= 7);
|
||||
const soon = rows.filter((r) => Number(r.DAYS_LEFT) > 7);
|
||||
|
||||
return NextResponse.json({
|
||||
RESULTLIST: rows,
|
||||
TOTAL_CNT: rows.length,
|
||||
EXPIRED_CNT: expired.length,
|
||||
URGENT_CNT: urgent.length,
|
||||
SOON_CNT: soon.length,
|
||||
NOW: now.toISOString().slice(0, 10),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 창고 이동 통계 — momo_stock_moves 의 ref_type='TRANSFER' OUT 행 기준 + cost_price 로 금액 산정
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { queryRows } 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 dateFrom = body.dateFrom as string | undefined;
|
||||
const dateTo = body.dateTo as string | undefined;
|
||||
|
||||
const conditions: string[] = ["SM.ref_type = 'TRANSFER'", "SM.move_type = 'OUT'"];
|
||||
const params: unknown[] = [];
|
||||
let i = 1;
|
||||
if (dateFrom) { conditions.push(`SM.regdate >= $${i++}::date`); params.push(dateFrom); }
|
||||
if (dateTo) { conditions.push(`SM.regdate < ($${i++}::date + 1)`); params.push(dateTo); }
|
||||
|
||||
const rows = await queryRows(
|
||||
`SELECT
|
||||
SM.objid AS "OBJID",
|
||||
TO_CHAR(SM.regdate, 'YYYY-MM-DD HH24:MI') AS "MOVED_AT",
|
||||
I.item_code AS "ITEM_CODE",
|
||||
I.item_name AS "ITEM_NAME",
|
||||
I.unit AS "UNIT",
|
||||
I.cost_price AS "COST_PRICE",
|
||||
ABS(SM.qty) AS "QTY",
|
||||
ABS(SM.qty) * COALESCE(I.cost_price, 0) AS "AMOUNT",
|
||||
SW.wh_name AS "FROM_WH",
|
||||
SW.wh_code AS "FROM_CODE",
|
||||
TW.wh_name AS "TO_WH",
|
||||
TW.wh_code AS "TO_CODE",
|
||||
SM.memo AS "MEMO",
|
||||
SM.regid AS "REGID",
|
||||
U.user_name AS "REG_USER_NAME"
|
||||
FROM momo_stock_moves SM
|
||||
LEFT JOIN momo_items I ON I.objid = SM.item_objid
|
||||
LEFT JOIN momo_warehouses SW ON SW.objid = SM.wh_objid
|
||||
LEFT JOIN momo_warehouses TW ON TW.objid::text = SM.ref_objid
|
||||
LEFT JOIN user_info U ON U.user_id = SM.regid
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY SM.regdate DESC
|
||||
LIMIT 500`,
|
||||
params
|
||||
);
|
||||
|
||||
// 합계
|
||||
const totalQty = rows.reduce((a, r) => a + Number(r.QTY), 0);
|
||||
const totalAmt = rows.reduce((a, r) => a + Number(r.AMOUNT), 0);
|
||||
|
||||
return NextResponse.json({
|
||||
RESULTLIST: rows,
|
||||
TOTAL_CNT: rows.length,
|
||||
TOTAL_QTY: totalQty,
|
||||
TOTAL_AMOUNT: totalAmt,
|
||||
});
|
||||
}
|
||||
@@ -17,9 +17,9 @@ export async function POST(req: NextRequest) {
|
||||
if (g instanceof NextResponse) return g;
|
||||
const adminId = g.user.objid || g.user.userId;
|
||||
|
||||
const { procObjid, vendorObjid, whObjid, inboundDate, lines, memo } = await req.json() as {
|
||||
const { procObjid, vendorObjid, whObjid, inboundDate, lines, memo, expiryDate, completedBy } = await req.json() as {
|
||||
procObjid?: string; vendorObjid?: string; whObjid: string; inboundDate?: string;
|
||||
lines: Line[]; memo?: string;
|
||||
lines: Line[]; memo?: string; expiryDate?: string; completedBy?: string;
|
||||
};
|
||||
if (!whObjid || !Array.isArray(lines) || lines.length === 0) {
|
||||
return NextResponse.json({ success: false, message: "창고와 입고 라인이 필요합니다." }, { status: 400 });
|
||||
@@ -34,9 +34,11 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
await client.query(
|
||||
`INSERT INTO momo_inbounds (objid, inbound_no, proc_objid, vendor_objid, wh_objid, inbound_date, status, total_amount, memo, regdate, regid)
|
||||
VALUES ($1,$2,$3,$4,$5,COALESCE($6::date, CURRENT_DATE),'COMPLETED',$7,$8,NOW(),$9)`,
|
||||
[inboundObjid, inboundNo, procObjid ?? null, vendorObjid ?? null, whObjid, inboundDate ?? null, total, memo ?? null, adminId]
|
||||
`INSERT INTO momo_inbounds (objid, inbound_no, proc_objid, vendor_objid, wh_objid, inbound_date, status, total_amount, memo, expiry_date, completed_by, regdate, regid)
|
||||
VALUES ($1,$2,$3,$4,$5,COALESCE($6::date, CURRENT_DATE),'COMPLETED',$7,$8,$10::date,$11,NOW(),$9)`,
|
||||
[inboundObjid, inboundNo, procObjid ?? null, vendorObjid ?? null, whObjid, inboundDate ?? null, total, memo ?? null, adminId,
|
||||
expiryDate && expiryDate.trim() !== "" ? expiryDate : null,
|
||||
completedBy && completedBy.trim() !== "" ? completedBy : null]
|
||||
);
|
||||
|
||||
// 입고 한도 사전 검증 (procObjid 있을 때) — 발주수량 - 기존 누적 입고 ≥ 이번 입고
|
||||
|
||||
Reference in New Issue
Block a user