Some checks failed
Build and Push Images / build-and-push (push) Failing after 1m30s
- POP 전용 39개 파일 추가 (홈/입고/출고/생산) - 백엔드 INSERT에 id gen_random_uuid 추가 (5개 파일) - POP 전용 API 7개 추가 (창고/위치/입고/동기화) - PC 코드 구조/순서/로직 변경 없음 (AppLayout, UserDropdown 미수정)
529 lines
22 KiB
TypeScript
529 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { InspectionModal, type InspectionResult } from "./InspectionModal";
|
|
import type { PackageEntry } from "./NumberPadModal";
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Warehouse type */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
interface Warehouse {
|
|
warehouse_code: string;
|
|
warehouse_name: string;
|
|
warehouse_type?: string;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Types */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export interface CartItem {
|
|
id: string;
|
|
/** cart_items 테이블의 PK (UUID) — DB 삭제용 */
|
|
dbId?: string;
|
|
/** purchase_detail or purchase_order_mng */
|
|
source_table: string;
|
|
/** PK of the source row */
|
|
source_id: string;
|
|
purchase_no: string;
|
|
item_code: string;
|
|
item_name: string;
|
|
spec: string;
|
|
material: string;
|
|
order_qty: number;
|
|
remain_qty: number;
|
|
/** User-entered quantity */
|
|
inbound_qty: number;
|
|
unit_price: number;
|
|
supplier_code: string;
|
|
supplier_name: string;
|
|
order_date: string;
|
|
inspection_required?: boolean;
|
|
inspection_type?: "self" | "request" | null;
|
|
packages?: PackageEntry[];
|
|
inspectionResult?: InspectionResult | null;
|
|
}
|
|
|
|
interface InboundCartProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
items: CartItem[];
|
|
onUpdateQty: (id: string, qty: number) => void;
|
|
onRemove: (id: string) => void;
|
|
onClear: () => void;
|
|
supplierName?: string;
|
|
onUpdateItems?: (items: CartItem[]) => void;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Component */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
export function InboundCart({
|
|
open,
|
|
onClose,
|
|
items,
|
|
onUpdateQty,
|
|
onRemove,
|
|
onClear,
|
|
supplierName,
|
|
onUpdateItems,
|
|
}: InboundCartProps) {
|
|
const router = useRouter();
|
|
const [confirming, setConfirming] = useState(false);
|
|
const [resultMsg, setResultMsg] = useState<string | null>(null);
|
|
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
|
const [inspectionModalOpen, setInspectionModalOpen] = useState(false);
|
|
const [inspectionTarget, setInspectionTarget] = useState<CartItem | null>(null);
|
|
|
|
/* Warehouse state */
|
|
const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
|
|
const [selectedWarehouse, setSelectedWarehouse] = useState<string>("");
|
|
|
|
/* Fetch warehouses on mount */
|
|
const fetchWarehouses = useCallback(async () => {
|
|
try {
|
|
const res = await apiClient.get("/receiving/warehouses");
|
|
const data: Warehouse[] = res.data?.data ?? [];
|
|
setWarehouses(data);
|
|
if (data.length > 0 && !selectedWarehouse) {
|
|
setSelectedWarehouse(data[0].warehouse_code);
|
|
}
|
|
} catch {
|
|
// Keep empty - user can still confirm without warehouse
|
|
}
|
|
}, [selectedWarehouse]);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
fetchWarehouses();
|
|
}
|
|
}, [open, fetchWarehouses]);
|
|
|
|
const totalQty = items.reduce((s, i) => s + i.inbound_qty, 0);
|
|
const totalAmount = items.reduce((s, i) => s + i.inbound_qty * i.unit_price, 0);
|
|
|
|
/* Toggle select */
|
|
const toggleSelect = (id: string) => {
|
|
setSelectedItems((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const toggleSelectAll = () => {
|
|
if (selectedItems.size === items.length) {
|
|
setSelectedItems(new Set());
|
|
} else {
|
|
setSelectedItems(new Set(items.map((i) => i.id)));
|
|
}
|
|
};
|
|
|
|
/* Open inspection modal */
|
|
const openInspection = (item: CartItem) => {
|
|
setInspectionTarget(item);
|
|
setInspectionModalOpen(true);
|
|
};
|
|
|
|
/* Handle inspection complete */
|
|
const handleInspectionComplete = (result: InspectionResult) => {
|
|
if (!inspectionTarget || !onUpdateItems) return;
|
|
const updated = items.map((item) =>
|
|
item.id === inspectionTarget.id
|
|
? { ...item, inspectionResult: result }
|
|
: item
|
|
);
|
|
onUpdateItems(updated);
|
|
setInspectionTarget(null);
|
|
};
|
|
|
|
/* Confirm inbound — PC receivingController.create 와 동일한 body 구조 */
|
|
const handleConfirm = async () => {
|
|
if (items.length === 0) return;
|
|
if (!selectedWarehouse) {
|
|
setResultMsg("오류: 입고 창고를 선택해주세요.");
|
|
return;
|
|
}
|
|
setConfirming(true);
|
|
setResultMsg(null);
|
|
|
|
try {
|
|
// 1. 입고번호 채번 (RCV-YYYY-XXXX)
|
|
let inboundNumber: string | undefined;
|
|
try {
|
|
const numRes = await apiClient.get("/receiving/generate-number");
|
|
if (numRes.data?.success && numRes.data?.data) {
|
|
inboundNumber = numRes.data.data;
|
|
}
|
|
} catch {
|
|
// 채번 실패 시 백엔드가 처리
|
|
}
|
|
|
|
// 2. POST /api/receiving — PC create 와 동일한 payload
|
|
const payload = {
|
|
inbound_number: inboundNumber,
|
|
inbound_date: new Date().toISOString().slice(0, 10),
|
|
warehouse_code: selectedWarehouse,
|
|
inbound_type: "구매입고",
|
|
items: items.map((item, idx) => ({
|
|
inbound_type: "구매입고",
|
|
item_number: item.item_code,
|
|
item_name: item.item_name,
|
|
spec: item.spec || "",
|
|
material: item.material || "",
|
|
unit: "EA",
|
|
inbound_qty: String(item.inbound_qty),
|
|
unit_price: String(item.unit_price || 0),
|
|
total_amount: String((item.inbound_qty || 0) * (item.unit_price || 0)),
|
|
reference_number: item.purchase_no,
|
|
supplier_code: item.supplier_code,
|
|
supplier_name: item.supplier_name,
|
|
inspection_status: item.inspectionResult?.completed
|
|
? "검사완료"
|
|
: item.inspection_required
|
|
? "검사대기"
|
|
: "합격",
|
|
source_table: item.source_table,
|
|
source_id: item.source_id || item.id,
|
|
seq_no: idx + 1,
|
|
})),
|
|
};
|
|
|
|
const res = await apiClient.post("/receiving", payload);
|
|
|
|
if (res.data?.success) {
|
|
// 3. cart_items DB 정리 (백그라운드, 논블로킹)
|
|
// cart_items.row_key 로 삭제 (row_key = source_id 로 저장됨)
|
|
const rowKeys = items.map((item) => item.source_id || item.id).filter(Boolean);
|
|
if (rowKeys.length > 0) {
|
|
apiClient.post("/pop/execute-action", {
|
|
tasks: [{ type: "cart-save" }],
|
|
cartChanges: {
|
|
toDelete: rowKeys,
|
|
},
|
|
}).catch(() => {
|
|
// cart cleanup 실패 시 무시
|
|
});
|
|
}
|
|
|
|
const inboundNo = res.data?.data?.header?.inbound_number || inboundNumber || "";
|
|
setResultMsg(`${items.length}건 입고 등록 완료! (${inboundNo})`);
|
|
setTimeout(() => {
|
|
onClear();
|
|
onClose();
|
|
router.push("/pop/inbound");
|
|
}, 1500);
|
|
} else {
|
|
setResultMsg(`오류: ${res.data?.message || "입고 등록에 실패했습니다."}`);
|
|
}
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : "입고 등록에 실패했습니다.";
|
|
setResultMsg(`오류: ${msg}`);
|
|
} finally {
|
|
setConfirming(false);
|
|
}
|
|
};
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
|
{/* Overlay */}
|
|
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
|
|
{/* Panel */}
|
|
<div className="relative bg-white w-full sm:max-w-lg sm:rounded-2xl rounded-t-2xl max-h-[90vh] flex flex-col shadow-2xl z-10 overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 rounded-xl bg-blue-500 flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-gray-900">입고 장바구니</h3>
|
|
{supplierName && (
|
|
<p className="text-xs text-gray-400">{supplierName}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500 hover:bg-gray-200 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Select all bar */}
|
|
{items.length > 0 && (
|
|
<div className="flex items-center gap-3 px-5 py-2 border-b border-gray-50 bg-gray-50/50">
|
|
<button
|
|
onClick={toggleSelectAll}
|
|
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all shrink-0 ${
|
|
selectedItems.size === items.length
|
|
? "bg-blue-500 border-blue-500"
|
|
: "border-gray-300 hover:border-blue-400"
|
|
}`}
|
|
>
|
|
{selectedItems.size === items.length && (
|
|
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth={3} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
<span className="text-xs text-gray-500">
|
|
전체 선택 ({selectedItems.size}/{items.length})
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Items */}
|
|
<div className="flex-1 overflow-y-auto px-5 py-3">
|
|
{items.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center py-12 text-gray-400">
|
|
<svg className="w-12 h-12 mb-3 opacity-30" fill="none" stroke="currentColor" strokeWidth={1} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
|
</svg>
|
|
<p className="text-sm">담은 품목이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-3">
|
|
{items.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="bg-gray-50 rounded-xl p-3 border border-gray-100"
|
|
>
|
|
{/* Top row: checkbox + name + delete */}
|
|
<div className="flex items-start gap-2.5 mb-2">
|
|
{/* Checkbox */}
|
|
<button
|
|
onClick={() => toggleSelect(item.id)}
|
|
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all shrink-0 mt-0.5 ${
|
|
selectedItems.has(item.id)
|
|
? "bg-blue-500 border-blue-500"
|
|
: "border-gray-300 hover:border-blue-400"
|
|
}`}
|
|
>
|
|
{selectedItems.has(item.id) && (
|
|
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth={3} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-semibold text-gray-900 truncate">{item.item_name}</p>
|
|
<p className="text-[11px] text-gray-400 mt-0.5">
|
|
{item.item_code} | {item.purchase_no}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Delete button */}
|
|
<button
|
|
onClick={() => onRemove(item.id)}
|
|
className="w-7 h-7 rounded-lg flex items-center justify-center text-white bg-red-400 hover:bg-red-500 transition-colors shrink-0"
|
|
>
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Spec row */}
|
|
{(item.spec || item.material) && (
|
|
<p className="text-[11px] text-gray-400 mb-2 ml-[30px]">
|
|
{[item.spec, item.material].filter(Boolean).join(" | ")}
|
|
</p>
|
|
)}
|
|
|
|
{/* Package info */}
|
|
{item.packages && item.packages.length > 0 && (
|
|
<div className="ml-[30px] mb-2 px-2.5 py-2 bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-lg">
|
|
<div className="flex items-center gap-2 mb-0.5">
|
|
<span className="text-[10px] font-bold text-white bg-green-500 px-1.5 py-0.5 rounded-full">
|
|
포장완료
|
|
</span>
|
|
<span className="text-[10px] text-green-600 font-semibold">
|
|
{"\uD83D\uDCE6"} {item.packages.map(p =>
|
|
`${p.count}${p.unit.label} x ${p.qtyPerUnit.toLocaleString()} = ${(p.count * p.qtyPerUnit).toLocaleString()}EA`
|
|
).join(", ")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Inspection row */}
|
|
{(item.inspection_type === "self" || item.inspection_type === "request") && (
|
|
<div className="ml-[30px] mb-2">
|
|
<button
|
|
onClick={() => openInspection(item)}
|
|
className={`flex items-center gap-2 w-full px-3 py-2 rounded-md border text-left transition-all ${
|
|
item.inspectionResult?.completed
|
|
? "bg-green-50 border-green-300"
|
|
: item.inspection_type === "self"
|
|
? "bg-blue-50 border-blue-200 hover:bg-blue-100"
|
|
: "bg-amber-50 border-amber-200 hover:bg-amber-100"
|
|
}`}
|
|
>
|
|
<span className="text-[13px] font-semibold">
|
|
{item.inspection_type === "self" ? "검사" : "검사의뢰"}
|
|
</span>
|
|
<span className={`text-[11px] px-1.5 py-0.5 rounded ${
|
|
item.inspection_required
|
|
? "bg-red-100 text-red-600"
|
|
: "bg-blue-100 text-blue-600"
|
|
}`}>
|
|
{item.inspection_required ? "필수" : "선택"}
|
|
</span>
|
|
<span className={`ml-auto text-[12px] font-semibold ${
|
|
item.inspectionResult?.completed
|
|
? "text-green-600"
|
|
: "text-gray-400"
|
|
}`}>
|
|
{item.inspectionResult?.completed ? "완료" : "대기"}
|
|
</span>
|
|
<svg className="w-4 h-4 text-gray-400 shrink-0" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Qty controls */}
|
|
<div className="flex items-center justify-between ml-[30px]">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-gray-400">미입고: {item.remain_qty.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<button
|
|
onClick={() => onUpdateQty(item.id, Math.max(1, item.inbound_qty - 1))}
|
|
className="w-8 h-8 rounded-lg bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 active:scale-95 transition-all"
|
|
>
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 12h-15" />
|
|
</svg>
|
|
</button>
|
|
<input
|
|
type="number"
|
|
value={item.inbound_qty}
|
|
onChange={(e) => {
|
|
const v = parseInt(e.target.value, 10);
|
|
if (!isNaN(v) && v >= 0) onUpdateQty(item.id, Math.min(v, item.remain_qty));
|
|
}}
|
|
className="w-16 h-8 text-center text-sm font-semibold border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-100"
|
|
style={{ fontVariantNumeric: "tabular-nums" }}
|
|
/>
|
|
<button
|
|
onClick={() => onUpdateQty(item.id, Math.min(item.remain_qty, item.inbound_qty + 1))}
|
|
className="w-8 h-8 rounded-lg bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 active:scale-95 transition-all"
|
|
>
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer summary + confirm */}
|
|
{items.length > 0 && (
|
|
<div className="border-t border-gray-100 px-5 py-4">
|
|
{/* Result message */}
|
|
{resultMsg && (
|
|
<div className={`mb-3 p-3 rounded-xl text-sm font-medium ${
|
|
resultMsg.startsWith("오류") ? "bg-red-50 text-red-700" : "bg-green-50 text-green-700"
|
|
}`}>
|
|
{resultMsg}
|
|
</div>
|
|
)}
|
|
|
|
{/* Warehouse selection */}
|
|
<div className="mb-3">
|
|
<label className="text-xs font-semibold text-gray-500 mb-1 block">입고 창고</label>
|
|
<select
|
|
value={selectedWarehouse}
|
|
onChange={(e) => setSelectedWarehouse(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-100 bg-white"
|
|
>
|
|
{warehouses.length === 0 ? (
|
|
<option value="">창고 정보 없음</option>
|
|
) : (
|
|
warehouses.map((wh) => (
|
|
<option key={wh.warehouse_code} value={wh.warehouse_code}>
|
|
{wh.warehouse_name} ({wh.warehouse_code})
|
|
</option>
|
|
))
|
|
)}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
<div className="flex items-center justify-between mb-3 text-sm">
|
|
<span className="text-gray-500">
|
|
총 <span className="font-bold text-gray-900">{items.length}</span>건
|
|
</span>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-gray-500">
|
|
합계 수량: <span className="font-bold text-blue-600">{totalQty.toLocaleString()}</span>
|
|
</span>
|
|
{totalAmount > 0 && (
|
|
<span className="text-gray-400 text-xs">
|
|
({totalAmount.toLocaleString()}원)
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Buttons */}
|
|
<div className="flex gap-3">
|
|
<button
|
|
onClick={() => { onClear(); }}
|
|
className="flex-1 h-12 rounded-xl border border-gray-200 text-sm font-semibold text-gray-600 hover:bg-gray-50 active:scale-[0.98] transition-all"
|
|
>
|
|
전체 삭제
|
|
</button>
|
|
<button
|
|
onClick={handleConfirm}
|
|
disabled={confirming || items.length === 0}
|
|
className="flex-[2] h-12 rounded-xl text-sm font-bold text-white active:scale-[0.98] transition-all disabled:opacity-50"
|
|
style={{
|
|
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
|
boxShadow: "0 4px 12px rgba(59,130,246,.3)",
|
|
}}
|
|
>
|
|
{confirming ? "처리 중..." : `입고 확정 (${items.length}건)`}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Inspection Modal */}
|
|
{inspectionTarget && (
|
|
<InspectionModal
|
|
open={inspectionModalOpen}
|
|
onClose={() => { setInspectionModalOpen(false); setInspectionTarget(null); }}
|
|
onComplete={handleInspectionComplete}
|
|
itemCode={inspectionTarget.item_code}
|
|
itemName={inspectionTarget.item_name}
|
|
totalQty={inspectionTarget.inbound_qty}
|
|
initialResult={inspectionTarget.inspectionResult}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|