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 */}
+
+
+ {/* 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
+
+
+
+
+
+ |
+ Tiêu chí
+ |
+ {compareData.map((park, i) => (
+
+ {park.name}
+ |
+ ))}
+
+
+
+ (
+
+ {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}
+ )) ?? '—'}
+
+ )} />
+
+
+
+
+
+ ) : 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}` : ''}`,
+ );
+ },
};