feat(daily-order-inventory): 단일 일자 → 기간(시작~종료) 조회 지원
- 페이지: date 두 개(시작/종료) + '오늘'·'최근 7일' 빠른 버튼
- API: targetDate 대신 startDate/endDate 사용 (단일 호환 유지),
판매가능 품목은 기간 겹침 조건, 발주수량은 order_date BETWEEN으로 합산
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,8 @@ const fmt = (n: number) => Number(n || 0).toLocaleString("ko-KR");
|
||||
const today = () => new Date().toISOString().slice(0, 10);
|
||||
|
||||
export default function DailyOrderInventoryPage() {
|
||||
const [targetDate, setTargetDate] = useState<string>(today());
|
||||
const [startDate, setStartDate] = useState<string>(today());
|
||||
const [endDate, setEndDate] = useState<string>(today());
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [warehouses, setWarehouses] = useState<Wh[]>([]);
|
||||
const [items, setItems] = useState<ItemRow[]>([]);
|
||||
@@ -37,7 +38,7 @@ export default function DailyOrderInventoryPage() {
|
||||
const res = await fetch("/api/m/admin/daily-order-inventory", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ targetDate, keyword }),
|
||||
body: JSON.stringify({ startDate, endDate, keyword }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const j = await res.json();
|
||||
@@ -50,7 +51,7 @@ export default function DailyOrderInventoryPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [targetDate, keyword]);
|
||||
}, [startDate, endDate, keyword]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
@@ -75,7 +76,8 @@ export default function DailyOrderInventoryPage() {
|
||||
{ header: "분류", key: "KIND" },
|
||||
...items.map((it) => ({ header: it.ITEM_NAME, key: it.ITEM_NAME })),
|
||||
];
|
||||
downloadXlsx(`일자별발주재고_${targetDate}`, data, cols);
|
||||
const range = startDate === endDate ? startDate : `${startDate}_${endDate}`;
|
||||
downloadXlsx(`일자별발주재고_${range}`, data, cols);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -87,23 +89,44 @@ export default function DailyOrderInventoryPage() {
|
||||
일자별 발주/재고 현황
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
선택일에 <b>판매 가능</b>한 품목을 헤더로, 각 창고를 좌측에 배치한 매트릭스.
|
||||
발주수량 = 그 날짜의 발주(REQUESTED는 거래처 default 창고로 가상 배정 / APPROVED 이후는 출고 창고). 재고수량 = 해당 창고 현재고.
|
||||
선택 기간에 <b>판매 가능</b>했던 품목을 헤더로, 각 창고를 좌측에 배치한 매트릭스.
|
||||
발주수량 = 기간 내 발주 합계(REQUESTED는 거래처 default 창고로 가상 배정 / APPROVED 이후는 출고 창고). 재고수량 = 해당 창고 현재고(기간 무관 현재 시점).
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<input
|
||||
type="date"
|
||||
value={targetDate}
|
||||
onChange={(e) => setTargetDate(e.target.value)}
|
||||
value={startDate}
|
||||
max={endDate || undefined}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="h-9 px-3 rounded border border-slate-300 text-sm"
|
||||
/>
|
||||
<span className="text-slate-400 text-sm">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
min={startDate || undefined}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="h-9 px-3 rounded border border-slate-300 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setTargetDate(today())}
|
||||
onClick={() => { const t = today(); setStartDate(t); setEndDate(t); }}
|
||||
className="h-9 px-3 rounded border border-slate-300 bg-white text-slate-700 text-xs font-semibold"
|
||||
>
|
||||
오늘
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setDate(end.getDate() - 6);
|
||||
setStartDate(start.toISOString().slice(0, 10));
|
||||
setEndDate(end.toISOString().slice(0, 10));
|
||||
}}
|
||||
className="h-9 px-3 rounded border border-slate-300 bg-white text-slate-700 text-xs font-semibold"
|
||||
>
|
||||
최근 7일
|
||||
</button>
|
||||
<div className="relative">
|
||||
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
|
||||
@@ -10,11 +10,14 @@ export async function POST(req: NextRequest) {
|
||||
if (g instanceof NextResponse) return g;
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const targetDate = (body.targetDate as string) || ""; // YYYY-MM-DD, 비우면 오늘
|
||||
// 신/구 호환: startDate/endDate 우선, 없으면 targetDate(단일)로 같은 날짜 사용
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const targetDate = (body.targetDate as string) || "";
|
||||
let startDate = (body.startDate as string) || targetDate || today;
|
||||
let endDate = (body.endDate as string) || targetDate || today;
|
||||
if (startDate > endDate) [startDate, endDate] = [endDate, startDate];
|
||||
const keyword = (body.keyword as string) || "";
|
||||
|
||||
const date = targetDate || new Date().toISOString().slice(0, 10);
|
||||
|
||||
// 1) 활성 창고
|
||||
const warehouses = await queryRows<{ OBJID: string; WH_CODE: string; WH_NAME: string }>(
|
||||
`SELECT objid AS "OBJID", wh_code AS "WH_CODE", wh_name AS "WH_NAME"
|
||||
@@ -23,18 +26,19 @@ export async function POST(req: NextRequest) {
|
||||
ORDER BY wh_code`
|
||||
);
|
||||
|
||||
// 2) 선택일에 판매가능한 품목
|
||||
// 2) 기간 내에 한 번이라도 판매가능했던 품목
|
||||
// 품목 판매기간 [sale_start_date, sale_end_date] 과 조회 기간 [startDate, endDate] 이 겹치면 포함
|
||||
const itemConds: string[] = [
|
||||
"COALESCE(I.is_del, 'N') != 'Y'",
|
||||
"COALESCE(I.is_hidden, 'N') != 'Y'",
|
||||
"UPPER(COALESCE(I.status, '')) = 'ACTIVE'",
|
||||
`(I.sale_start_date IS NULL OR $1::date >= I.sale_start_date::date)`,
|
||||
`(I.sale_end_date IS NULL OR $1::date <= I.sale_end_date::date)`,
|
||||
`(I.sale_start_date IS NULL OR I.sale_start_date::date <= $2::date)`,
|
||||
`(I.sale_end_date IS NULL OR I.sale_end_date::date >= $1::date)`,
|
||||
];
|
||||
const params: unknown[] = [date];
|
||||
const params: unknown[] = [startDate, endDate];
|
||||
if (keyword) {
|
||||
params.push(keyword);
|
||||
itemConds.push(`(I.item_name ILIKE '%' || $2 || '%' OR I.item_code ILIKE '%' || $2 || '%')`);
|
||||
itemConds.push(`(I.item_name ILIKE '%' || $3 || '%' OR I.item_code ILIKE '%' || $3 || '%')`);
|
||||
}
|
||||
|
||||
const items = await queryRows<{
|
||||
@@ -89,12 +93,12 @@ export async function POST(req: NextRequest) {
|
||||
WHERE SM.ref_type = 'ORDER'
|
||||
AND SM.move_type = 'OUT'
|
||||
AND COALESCE(W.is_del,'N') != 'Y'
|
||||
AND O.order_date = $1::date
|
||||
AND O.order_date BETWEEN $1::date AND $2::date
|
||||
AND O.status IN ('APPROVED','INVOICED','PAID')
|
||||
AND COALESCE(O.is_del,'N') != 'Y'
|
||||
AND SM.item_objid IN (${items.map((_, i) => `$${i + 2}`).join(",")})
|
||||
AND SM.item_objid IN (${items.map((_, i) => `$${i + 3}`).join(",")})
|
||||
GROUP BY SM.item_objid, W.wh_code`,
|
||||
[date, ...items.map((it) => it.OBJID)]
|
||||
[startDate, endDate, ...items.map((it) => it.OBJID)]
|
||||
);
|
||||
|
||||
// REQUESTED 상태(아직 출고 전) 발주는 거래처 default 창고로 배정 (fallback: WH001)
|
||||
@@ -109,10 +113,10 @@ export async function POST(req: NextRequest) {
|
||||
WHERE COALESCE(OI.kind, 'ITEM') = 'ITEM'
|
||||
AND COALESCE(O.is_del, 'N') != 'Y'
|
||||
AND O.status = 'REQUESTED'
|
||||
AND O.order_date = $1::date
|
||||
AND OI.item_objid IN (${items.map((_, i) => `$${i + 2}`).join(",")})
|
||||
AND O.order_date BETWEEN $1::date AND $2::date
|
||||
AND OI.item_objid IN (${items.map((_, i) => `$${i + 3}`).join(",")})
|
||||
GROUP BY OI.item_objid, W.wh_code`,
|
||||
[date, ...items.map((it) => it.OBJID)]
|
||||
[startDate, endDate, ...items.map((it) => it.OBJID)]
|
||||
);
|
||||
|
||||
// 매트릭스 조립 — 각 품목별 STOCK / ORDER 객체 (wh_code → qty)
|
||||
|
||||
Reference in New Issue
Block a user