diff --git a/apps/web/app/[locale]/(public)/khu-cong-nghiep/cho-thue/page.tsx b/apps/web/app/[locale]/(public)/khu-cong-nghiep/cho-thue/page.tsx new file mode 100644 index 0000000..29647d4 --- /dev/null +++ b/apps/web/app/[locale]/(public)/khu-cong-nghiep/cho-thue/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next'; +import { ListingSearchClient } from '@/components/khu-cong-nghiep/listing-search-client'; + +export const metadata: Metadata = { + title: 'Cho Thuê Bất Động Sản Công Nghiệp — GoodGo', + description: 'Tìm kiếm nhà xưởng, kho bãi, đất công nghiệp cho thuê tại các khu công nghiệp Việt Nam.', +}; + +export default function IndustrialListingsPage() { + return ; +} diff --git a/apps/web/app/[locale]/(public)/khu-cong-nghiep/page.tsx b/apps/web/app/[locale]/(public)/khu-cong-nghiep/page.tsx index bd76e54..51451a6 100644 --- a/apps/web/app/[locale]/(public)/khu-cong-nghiep/page.tsx +++ b/apps/web/app/[locale]/(public)/khu-cong-nghiep/page.tsx @@ -1,9 +1,10 @@ 'use client'; -import { Factory } from 'lucide-react'; +import { Factory, Map } from 'lucide-react'; import * as React from 'react'; import { ParkCard } from '@/components/khu-cong-nghiep/park-card'; import { ParkFilterBar } from '@/components/khu-cong-nghiep/park-filter-bar'; +import { ParkMap } from '@/components/khu-cong-nghiep/park-map'; import { Button } from '@/components/ui/button'; import { useIndustrialParksSearch } from '@/lib/hooks/use-khu-cong-nghiep'; import type { SearchIndustrialParksParams } from '@/lib/khu-cong-nghiep-api'; @@ -15,6 +16,7 @@ export default function KhuCongNghiepPage() { page: 1, limit: PAGE_SIZE, }); + const [showMap, setShowMap] = React.useState(false); const { data, isLoading, isError } = useIndustrialParksSearch(filters); @@ -41,6 +43,26 @@ export default function KhuCongNghiepPage() { {/* Filters */} + {/* Map toggle */} +
+ +
+ + {/* Park Map */} + {showMap && data && data.data.length > 0 && ( +
+ +
+ )} + {/* Results */}
{isLoading ? ( diff --git a/apps/web/app/[locale]/(public)/khu-cong-nghiep/so-sanh/page.tsx b/apps/web/app/[locale]/(public)/khu-cong-nghiep/so-sanh/page.tsx new file mode 100644 index 0000000..7835a6e --- /dev/null +++ b/apps/web/app/[locale]/(public)/khu-cong-nghiep/so-sanh/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next'; +import { ParkCompareClient } from '@/components/khu-cong-nghiep/park-compare-client'; + +export const metadata: Metadata = { + title: 'So Sánh Khu Công Nghiệp — GoodGo', + description: 'So sánh chi tiết giữa các khu công nghiệp tại Việt Nam: diện tích, giá thuê, tỷ lệ lấp đầy, hạ tầng và kết nối.', +}; + +export default function ParkComparePage() { + return ; +} diff --git a/apps/web/components/khu-cong-nghiep/listing-card.tsx b/apps/web/components/khu-cong-nghiep/listing-card.tsx new file mode 100644 index 0000000..f87fe35 --- /dev/null +++ b/apps/web/components/khu-cong-nghiep/listing-card.tsx @@ -0,0 +1,95 @@ +'use client'; + +import { Calendar, Eye, MapPin, Ruler } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent } from '@/components/ui/card'; +import { + type IndustrialListingItem, + LEASE_TYPE_LABELS, + PROPERTY_TYPE_LABELS, +} from '@/lib/khu-cong-nghiep-api'; + +interface ListingCardProps { + listing: IndustrialListingItem; +} + +export function IndustrialListingCard({ listing }: ListingCardProps) { + const priceText = listing.priceUsdM2 + ? `$${listing.priceUsdM2}/${listing.pricingUnit ?? 'm²/tháng'}` + : listing.totalLeasePrice + ? `$${listing.totalLeasePrice.toLocaleString()}` + : 'Liên hệ'; + + const leaseTermText = + listing.minLeaseYears && listing.maxLeaseYears + ? `${listing.minLeaseYears}–${listing.maxLeaseYears} năm` + : listing.minLeaseYears + ? `Từ ${listing.minLeaseYears} năm` + : null; + + return ( + + + {/* Header badges */} +
+ + {PROPERTY_TYPE_LABELS[listing.propertyType]} + + + {LEASE_TYPE_LABELS[listing.leaseType]} + +
+ + {/* Title */} +

+ {listing.title} +

+ + {/* Park location */} + + + {/* Stats grid */} +
+
+
+ + Diện tích +
+
{listing.areaM2.toLocaleString()} m²
+
+
+
Giá thuê
+
{priceText}
+
+
+ + {/* Additional info */} +
+ {listing.ceilingHeightM && ( + Cao trần: {listing.ceilingHeightM}m + )} + {leaseTermText && ( + + + {leaseTermText} + + )} + {listing.viewCount > 0 && ( + + + {listing.viewCount} + + )} +
+
+
+ ); +} diff --git a/apps/web/components/khu-cong-nghiep/listing-search-client.tsx b/apps/web/components/khu-cong-nghiep/listing-search-client.tsx new file mode 100644 index 0000000..6394a42 --- /dev/null +++ b/apps/web/components/khu-cong-nghiep/listing-search-client.tsx @@ -0,0 +1,231 @@ +'use client'; + +import { ArrowLeft, Factory, Search, X } from 'lucide-react'; +import * as React from 'react'; +import { IndustrialListingCard } from '@/components/khu-cong-nghiep/listing-card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Link } from '@/i18n/navigation'; +import { useIndustrialListingsSearch } from '@/lib/hooks/use-khu-cong-nghiep'; +import { + type IndustrialLeaseType, + type IndustrialPropertyType, + type SearchIndustrialListingsParams, + LEASE_TYPE_LABELS, + PROPERTY_TYPE_LABELS, +} from '@/lib/khu-cong-nghiep-api'; + +const PAGE_SIZE = 12; + +export function ListingSearchClient() { + const [filters, setFilters] = React.useState({ + page: 1, + limit: PAGE_SIZE, + }); + const [searchInput, setSearchInput] = React.useState(''); + + const { data, isLoading, isError } = useIndustrialListingsSearch(filters); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + setFilters((prev) => ({ ...prev, q: searchInput.trim() || undefined, page: 1 })); + }; + + const updateFilter = (key: keyof SearchIndustrialListingsParams, value: string) => { + setFilters((prev) => ({ ...prev, [key]: value || undefined, page: 1 })); + }; + + const updateNumericFilter = (key: keyof SearchIndustrialListingsParams, value: string) => { + const num = value ? Number(value) : undefined; + setFilters((prev) => ({ ...prev, [key]: num, page: 1 })); + }; + + const handlePageChange = (page: number) => { + setFilters((prev) => ({ ...prev, page })); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const handleClear = () => { + setSearchInput(''); + setFilters({ page: 1, limit: PAGE_SIZE }); + }; + + const hasFilters = filters.q || filters.propertyType || filters.leaseType || + filters.minAreaM2 || filters.maxAreaM2 || filters.minPriceUsdM2 || filters.maxPriceUsdM2; + + return ( +
+ {/* Header */} +
+ + + +
+

Cho Thuê Bất Động Sản Công Nghiệp

+

+ Tìm nhà xưởng, kho bãi, đất công nghiệp cho thuê tại các KCN +

+
+
+ + {/* Search bar */} +
+
+ + setSearchInput(e.target.value)} + className="pl-9" + /> +
+ +
+ + {/* Filters */} +
+ + + + + updateNumericFilter('minAreaM2', e.target.value)} + className="w-36 text-sm" + aria-label="Diện tích tối thiểu" + /> + + updateNumericFilter('maxAreaM2', e.target.value)} + className="w-36 text-sm" + aria-label="Diện tích tối đa" + /> + + updateNumericFilter('minPriceUsdM2', e.target.value)} + className="w-36 text-sm" + aria-label="Giá tối thiểu" + /> + + updateNumericFilter('maxPriceUsdM2', e.target.value)} + className="w-36 text-sm" + aria-label="Giá tối đa" + /> + + {hasFilters && ( + + )} +
+ + {/* Results */} +
+ {isLoading ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ ) : isError ? ( +
+

+ Không thể tải danh sách tin cho thuê. Vui lòng thử lại. +

+ +
+ ) : data && data.data.length > 0 ? ( + <> +

+ {data.total} tin cho thuê được tìm thấy +

+ +
+ {data.data.map((listing) => ( + + ))} +
+ + {/* Pagination */} + {data.totalPages > 1 && ( +
+ + + Trang {data.page} / {data.totalPages} + + +
+ )} + + ) : ( +
+ +

Không tìm thấy tin cho thuê

+

+ Thử thay đổi bộ lọc để tìm kiếm nhiều hơn +

+
+ )} +
+
+ ); +} diff --git a/apps/web/components/khu-cong-nghiep/park-compare-client.tsx b/apps/web/components/khu-cong-nghiep/park-compare-client.tsx new file mode 100644 index 0000000..1584977 --- /dev/null +++ b/apps/web/components/khu-cong-nghiep/park-compare-client.tsx @@ -0,0 +1,339 @@ +'use client'; + +import { ArrowLeft, Factory, Plus, X } from 'lucide-react'; +import * as React from 'react'; +import { + Radar, + RadarChart, + PolarGrid, + PolarAngleAxis, + PolarRadiusAxis, + ResponsiveContainer, + Legend, + Tooltip, +} from 'recharts'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Link } from '@/i18n/navigation'; +import { + useIndustrialCompare, + useIndustrialParksSearch, +} from '@/lib/hooks/use-khu-cong-nghiep'; +import { + type IndustrialParkDetail, + type IndustrialParkListItem, + PARK_STATUS_COLORS, + PARK_STATUS_LABELS, + REGION_LABELS, +} from '@/lib/khu-cong-nghiep-api'; + +const CHART_COLORS = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b']; + +const RADAR_METRICS = [ + { key: 'occupancy', label: 'Lấp đầy' }, + { key: 'area', label: 'Diện tích' }, + { key: 'rent', label: 'Giá thuê' }, + { key: 'infrastructure', label: 'Hạ tầng' }, + { key: 'connectivity', label: 'Kết nối' }, +] as const; + +function normalizeScore(park: IndustrialParkDetail, metric: string): number { + switch (metric) { + case 'occupancy': + return park.occupancyRate; + case 'area': + return Math.min((park.totalAreaHa / 1000) * 100, 100); + case 'rent': { + const rent = park.landRentUsdM2Year ?? 0; + return rent > 0 ? Math.min((rent / 150) * 100, 100) : 0; + } + case 'infrastructure': { + const count = park.infrastructure ? Object.keys(park.infrastructure).length : 0; + return Math.min((count / 10) * 100, 100); + } + case 'connectivity': { + const conns = park.connectivity ? Object.keys(park.connectivity).length : 0; + return Math.min((conns / 8) * 100, 100); + } + default: + return 0; + } +} + +function buildRadarData(parks: IndustrialParkDetail[]) { + return RADAR_METRICS.map((metric) => { + const entry: Record = { metric: metric.label }; + parks.forEach((park, i) => { + entry[`park${i}`] = Math.round(normalizeScore(park, metric.key)); + }); + return entry; + }); +} + +export function ParkCompareClient() { + const [selectedIds, setSelectedIds] = React.useState([]); + const [searchQuery, setSearchQuery] = React.useState(''); + const [showPicker, setShowPicker] = React.useState(false); + + const { data: searchResults } = useIndustrialParksSearch({ + q: searchQuery || undefined, + limit: 20, + }); + const { data: compareData, isLoading } = useIndustrialCompare(selectedIds); + + const addPark = (park: IndustrialParkListItem) => { + if (selectedIds.length >= 4) return; + if (selectedIds.includes(park.id)) return; + setSelectedIds((prev) => [...prev, park.id]); + setShowPicker(false); + setSearchQuery(''); + }; + + const removePark = (id: string) => { + setSelectedIds((prev) => prev.filter((pid) => pid !== id)); + }; + + const radarData = compareData ? buildRadarData(compareData) : []; + + return ( +
+ {/* Header */} +
+ + + +
+

So Sánh Khu Công Nghiệp

+

+ Chọn 2–4 KCN để so sánh chi tiết +

+
+
+ + {/* Park selection */} + + +
+ {selectedIds.map((id, i) => { + const park = compareData?.find((p) => p.id === id); + return ( + + {park?.name ?? 'Đang tải...'} + + + ); + })} + + {selectedIds.length < 4 && ( +
+ + + {showPicker && ( +
+
+ setSearchQuery(e.target.value)} + className="w-full rounded-md border bg-background px-3 py-2 text-sm" + autoFocus + /> +
+
+ {searchResults?.data + .filter((p) => !selectedIds.includes(p.id)) + .map((park) => ( + + ))} + {searchResults?.data.length === 0 && ( +

+ Không tìm thấy KCN +

+ )} +
+
+ )} +
+ )} +
+
+
+ + {/* Content */} + {selectedIds.length < 2 ? ( +
+ +

Chọn ít nhất 2 KCN để so sánh

+

+ Sử dụng nút “Thêm KCN” ở trên để bắt đầu +

+
+ ) : isLoading ? ( +
+ {Array.from({ length: 2 }).map((_, i) => ( +
+ ))} +
+ ) : compareData ? ( +
+ {/* Radar Chart */} + + + Biểu đồ radar so sánh + + + + + + + + {compareData.map((park, i) => ( + + ))} + + + + + + + + {/* Comparison Table */} + + + Chi tiết so sánh + + + + + + + {compareData.map((park, i) => ( + + ))} + + + + ( + + {PARK_STATUS_LABELS[p.status]} + + )} /> + REGION_LABELS[p.region]} /> + p.province} /> + `${p.totalAreaHa.toLocaleString()} ha`} /> + `${p.leasableAreaHa.toLocaleString()} ha`} /> + `${p.occupancyRate}%`} /> + `${p.remainingAreaHa.toLocaleString()} ha`} /> + String(p.tenantCount)} /> + p.establishedYear ? String(p.establishedYear) : '—'} /> + p.landRentUsdM2Year ? `$${p.landRentUsdM2Year}` : '—'} /> + p.rbfRentUsdM2Month ? `$${p.rbfRentUsdM2Month}` : '—'} /> + p.managementFeeUsd ? `$${p.managementFeeUsd}` : '—'} /> + p.developer} /> + ( +
+ {p.targetIndustries.slice(0, 4).map((ind) => ( + {ind} + ))} + {p.targetIndustries.length > 4 && ( + +{p.targetIndustries.length - 4} + )} +
+ )} /> + ( +
+ {p.certifications?.map((cert) => ( + {cert} + )) ?? '—'} +
+ )} /> +
+
+ Tiêu chí + + {park.name} +
+
+
+
+ ) : null} +
+ ); +} + +function CompareRow({ + label, + parks, + render, +}: { + label: string; + parks: IndustrialParkDetail[]; + render: (park: IndustrialParkDetail) => React.ReactNode; +}) { + return ( + + {label} + {parks.map((park) => ( + {render(park)} + ))} + + ); +} diff --git a/apps/web/components/khu-cong-nghiep/park-map.tsx b/apps/web/components/khu-cong-nghiep/park-map.tsx new file mode 100644 index 0000000..042fd4e --- /dev/null +++ b/apps/web/components/khu-cong-nghiep/park-map.tsx @@ -0,0 +1,159 @@ +'use client'; + +/* eslint-disable import-x/no-named-as-default-member */ +import mapboxgl from 'mapbox-gl'; +import * as React from 'react'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import { + type IndustrialParkListItem, + PARK_STATUS_LABELS, + PARK_STATUS_COLORS, +} from '@/lib/khu-cong-nghiep-api'; + +interface ParkMapProps { + parks: IndustrialParkListItem[]; + className?: string; +} + +const DEFAULT_CENTER: [number, number] = [106.6297, 10.8231]; // HCMC +const DEFAULT_ZOOM = 6; + +export function ParkMap({ parks, className }: ParkMapProps) { + const mapContainerRef = React.useRef(null); + const mapRef = React.useRef(null); + const markersRef = React.useRef([]); + + const geoParks = React.useMemo( + () => parks.filter((p) => p.latitude != null && p.longitude != null), + [parks], + ); + + React.useEffect(() => { + if (!mapContainerRef.current) return; + + const token = process.env['NEXT_PUBLIC_MAPBOX_TOKEN']; + if (!token) return; + + mapboxgl.accessToken = token; + + const map = new mapboxgl.Map({ + container: mapContainerRef.current, + style: 'mapbox://styles/mapbox/streets-v12', + center: DEFAULT_CENTER, + zoom: DEFAULT_ZOOM, + attributionControl: false, + }); + + map.addControl(new mapboxgl.NavigationControl(), 'top-right'); + map.addControl(new mapboxgl.AttributionControl({ compact: true }), 'bottom-right'); + + mapRef.current = map; + + return () => { + map.remove(); + mapRef.current = null; + }; + }, []); + + React.useEffect(() => { + const map = mapRef.current; + if (!map) return; + + markersRef.current.forEach((m) => m.remove()); + markersRef.current = []; + + if (geoParks.length === 0) return; + + const bounds = new mapboxgl.LngLatBounds(); + + geoParks.forEach((park) => { + const el = document.createElement('div'); + el.className = 'park-map-marker'; + el.style.cssText = ` + background: white; + border-radius: 8px; + padding: 4px 8px; + font-size: 11px; + font-weight: 600; + box-shadow: 0 2px 6px rgba(0,0,0,0.15); + white-space: nowrap; + cursor: pointer; + border-left: 3px solid hsl(221.2, 83.2%, 53.3%); + transition: transform 0.15s; + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + `; + el.textContent = park.name; + el.addEventListener('mouseenter', () => { + el.style.transform = 'scale(1.05)'; + }); + el.addEventListener('mouseleave', () => { + el.style.transform = 'scale(1)'; + }); + + const statusLabel = PARK_STATUS_LABELS[park.status]; + const statusColorClass = PARK_STATUS_COLORS[park.status]; + const bgColor = statusColorClass.includes('green') ? '#dcfce7' : + statusColorClass.includes('amber') ? '#fef3c7' : + statusColorClass.includes('red') ? '#fee2e2' : + '#dbeafe'; + const textColor = statusColorClass.includes('green') ? '#166534' : + statusColorClass.includes('amber') ? '#92400e' : + statusColorClass.includes('red') ? '#991b1b' : + '#1e40af'; + + const rentText = park.landRentUsdM2Year + ? `$${park.landRentUsdM2Year}/m²/năm` + : 'Liên hệ'; + + const popup = new mapboxgl.Popup({ offset: 15, maxWidth: '260px', closeButton: false }) + .setHTML( + `
+

${park.name}

+

${park.province} · ${park.totalAreaHa.toLocaleString()} ha

+

+ ${statusLabel} + ${rentText} +

+

Lấp đầy: ${park.occupancyRate}% · ${park.tenantCount} DN

+ Xem chi tiết → +
`, + ); + + const marker = new mapboxgl.Marker({ element: el, anchor: 'left' }) + .setLngLat([park.longitude, park.latitude]) + .setPopup(popup) + .addTo(map); + + markersRef.current.push(marker); + bounds.extend([park.longitude, park.latitude]); + }); + + if (geoParks.length > 1) { + map.fitBounds(bounds, { padding: 60, maxZoom: 13 }); + } else { + map.flyTo({ center: [geoParks[0]!.longitude, geoParks[0]!.latitude], zoom: 14 }); + } + }, [geoParks]); + + const hasToken = typeof process !== 'undefined' && process.env['NEXT_PUBLIC_MAPBOX_TOKEN']; + + return ( +
+
+ + {!hasToken && ( +
+

+ Thiết lập NEXT_PUBLIC_MAPBOX_TOKEN để hiển thị bản đồ +

+
+ )} + +
+ {geoParks.length} KCN trên bản đồ +
+
+ ); +} diff --git a/apps/web/lib/hooks/use-khu-cong-nghiep.ts b/apps/web/lib/hooks/use-khu-cong-nghiep.ts index 5045daa..4e4684e 100644 --- a/apps/web/lib/hooks/use-khu-cong-nghiep.ts +++ b/apps/web/lib/hooks/use-khu-cong-nghiep.ts @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { industrialApi, + type SearchIndustrialListingsParams, type SearchIndustrialParksParams, } from '@/lib/khu-cong-nghiep-api'; @@ -11,6 +12,7 @@ export const industrialKeys = { stats: () => ['industrial', 'stats'] as const, market: () => ['industrial', 'market'] as const, compare: (ids: string[]) => ['industrial', 'compare', ids] as const, + listings: (params: SearchIndustrialListingsParams) => ['industrial', 'listings', params] as const, }; export function useIndustrialParksSearch(params: SearchIndustrialParksParams = {}) { @@ -51,3 +53,10 @@ export function useIndustrialCompare(ids: string[]) { enabled: ids.length >= 2, }); } + +export function useIndustrialListingsSearch(params: SearchIndustrialListingsParams = {}) { + return useQuery({ + queryKey: industrialKeys.listings(params), + queryFn: () => industrialApi.searchListings(params), + }); +} diff --git a/apps/web/lib/khu-cong-nghiep-api.ts b/apps/web/lib/khu-cong-nghiep-api.ts index cabfc29..88ce754 100644 --- a/apps/web/lib/khu-cong-nghiep-api.ts +++ b/apps/web/lib/khu-cong-nghiep-api.ts @@ -90,6 +90,65 @@ export interface IndustrialMarketData { rentByProvince: { province: string; avgLandRent: number | null; avgRbfRent: number | null; parkCount: number }[]; } +// ─── Industrial Listing Types ─────────────────────────── + +export type IndustrialPropertyType = + | 'INDUSTRIAL_LAND' + | 'READY_BUILT_FACTORY' + | 'READY_BUILT_WAREHOUSE' + | 'LOGISTICS_CENTER' + | 'OFFICE_IN_PARK' + | 'DATA_CENTER'; + +export type IndustrialLeaseType = + | 'LAND_LEASE' + | 'FACTORY_LEASE' + | 'WAREHOUSE_LEASE' + | 'SUBLEASE'; + +export type IndustrialListingStatus = + | 'DRAFT' + | 'ACTIVE' + | 'RESERVED' + | 'LEASED' + | 'EXPIRED'; + +export interface IndustrialListingItem { + id: string; + parkId: string; + parkName: string; + parkSlug: string; + propertyType: IndustrialPropertyType; + leaseType: IndustrialLeaseType; + status: IndustrialListingStatus; + title: string; + description: string | null; + areaM2: number; + ceilingHeightM: number | null; + priceUsdM2: number | null; + pricingUnit: string | null; + totalLeasePrice: number | null; + minLeaseYears: number | null; + maxLeaseYears: number | null; + availableFrom: string | null; + media: { url: string; type: string; caption?: string }[] | null; + viewCount: number; + publishedAt: string | null; +} + +export interface SearchIndustrialListingsParams { + parkId?: string; + propertyType?: IndustrialPropertyType; + leaseType?: IndustrialLeaseType; + minAreaM2?: number; + maxAreaM2?: number; + minPriceUsdM2?: number; + maxPriceUsdM2?: number; + q?: string; + page?: number; + limit?: number; +} + export interface PaginatedResult { data: T[]; total: number; @@ -132,6 +191,22 @@ export const REGION_LABELS: Record = { SOUTH: 'Miền Nam', }; +export const PROPERTY_TYPE_LABELS: Record = { + INDUSTRIAL_LAND: 'Đất công nghiệp', + READY_BUILT_FACTORY: 'Nhà xưởng xây sẵn', + READY_BUILT_WAREHOUSE: 'Kho bãi xây sẵn', + LOGISTICS_CENTER: 'Trung tâm logistics', + OFFICE_IN_PARK: 'Văn phòng trong KCN', + DATA_CENTER: 'Trung tâm dữ liệu', +}; + +export const LEASE_TYPE_LABELS: Record = { + LAND_LEASE: 'Thuê đất', + FACTORY_LEASE: 'Thuê nhà xưởng', + WAREHOUSE_LEASE: 'Thuê kho bãi', + SUBLEASE: 'Cho thuê lại', +}; + // ─── API Functions ────────────────────────────────────── export const industrialApi = { @@ -157,4 +232,15 @@ export const industrialApi = { getMarket: () => apiClient.get('/industrial/market'), + + searchListings: (params: SearchIndustrialListingsParams = {}) => { + const query = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== '') query.append(key, String(value)); + }); + const qs = query.toString(); + return apiClient.get>( + `/industrial/listings${qs ? `?${qs}` : ''}`, + ); + }, };