import { apiClient } from './api-client'; // ─── Types ────────────────────────────────────────────── export type IndustrialParkStatus = | 'PLANNING' | 'UNDER_CONSTRUCTION' | 'OPERATIONAL' | 'FULL'; export type VietnamRegion = 'NORTH' | 'CENTRAL' | 'SOUTH'; export interface IndustrialParkListItem { id: string; name: string; nameEn: string | null; slug: string; developer: string; status: IndustrialParkStatus; province: string; region: VietnamRegion; totalAreaHa: number; occupancyRate: number; remainingAreaHa: number; tenantCount: number; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ landRentUsdM2Year: string | null; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ rbfRentUsdM2Month: string | null; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ rbwRentUsdM2Month: string | null; targetIndustries: string[]; latitude: number; longitude: number; } export interface IndustrialParkDetail { id: string; name: string; nameEn: string | null; slug: string; developer: string; operator: string | null; status: IndustrialParkStatus; latitude: number; longitude: number; address: string; district: string; province: string; region: VietnamRegion; totalAreaHa: number; leasableAreaHa: number; occupancyRate: number; remainingAreaHa: number; tenantCount: number; establishedYear: number | null; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ landRentUsdM2Year: string | null; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ rbfRentUsdM2Month: string | null; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ rbwRentUsdM2Month: string | null; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ managementFeeUsd: string | null; infrastructure: Record | null; connectivity: Record | null; incentives: Record | null; targetIndustries: string[]; existingTenants: { name: string; country: string; industry: string }[] | null; certifications: string[] | null; media: { url: string; type: string; caption?: string }[] | null; documents: { url: string; name: string }[] | null; description: string | null; descriptionEn: string | null; isVerified: boolean; listingCount: number; createdAt: string; updatedAt: string; } export interface IndustrialParkStats { totalParks: number; totalAreaHa: number; avgOccupancyRate: number; totalTenants: number; byRegion: { region: string; count: number; avgOccupancy: number }[]; byStatus: { status: string; count: number }[]; topProvinces: { province: string; count: number; avgRent: number | null }[]; } export interface IndustrialMarketData { totalParks: number; avgOccupancyRate: number; /** AVG(numeric) serialised as string by PostgreSQL. */ avgLandRentUsdM2: string | null; /** AVG(numeric) serialised as string by PostgreSQL. */ avgRbfRentUsdM2: string | null; rentByRegion: { region: string; avgLandRent: string | null; avgRbfRent: string | null; parkCount: number }[]; rentByProvince: { province: string; avgLandRent: string | null; avgRbfRent: string | 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; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ priceUsdM2: string | null; pricingUnit: string | null; /** Decimal(18,4) serialised as string. Use parseFloat() for arithmetic. */ totalLeasePrice: string | 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 CreateIndustrialParkPayload { name: string; nameEn?: string; slug: string; developer: string; operator?: string; status: IndustrialParkStatus; latitude: number; longitude: number; address: string; district: string; province: string; region: VietnamRegion; totalAreaHa: number; leasableAreaHa: number; occupancyRate: number; remainingAreaHa: number; tenantCount?: number; establishedYear?: number; landRentUsdM2Year?: number; rbfRentUsdM2Month?: number; rbwRentUsdM2Month?: number; managementFeeUsd?: number; infrastructure?: Record; connectivity?: Record; incentives?: Record; targetIndustries: string[]; description?: string; descriptionEn?: string; } export interface UpdateIndustrialParkPayload { name?: string; nameEn?: string; developer?: string; operator?: string; status?: IndustrialParkStatus; occupancyRate?: number; remainingAreaHa?: number; tenantCount?: number; landRentUsdM2Year?: number; rbfRentUsdM2Month?: number; rbwRentUsdM2Month?: number; managementFeeUsd?: number; infrastructure?: Record; connectivity?: Record; incentives?: Record; targetIndustries?: string[]; description?: string; descriptionEn?: string; isVerified?: boolean; } export interface PaginatedResult { data: T[]; total: number; page: number; limit: number; totalPages: number; } export interface SearchIndustrialParksParams { q?: string; province?: string; region?: VietnamRegion; status?: IndustrialParkStatus; minAreaHa?: number; maxRentUsdM2?: number; targetIndustry?: string; page?: number; limit?: number; } // ─── OSM Admin Types ──────────────────────────────────── export interface OsmPendingItem { id: string; slug: string; name: string; nameEn: string | null; province: string; district: string; region: string; status: string; /** OSM relation/way/node id, serialised as string (BigInt). */ osmId: string; osmType: 'NODE' | 'WAY' | 'RELATION' | null; /** Raw OSM tags object — varies wildly per row. */ osmTags: Record | null; totalAreaHa: number; developer: string; operator: string | null; osmLocked: boolean; lastSyncedAt: string | null; latitude: number | null; longitude: number | null; } export interface OsmPendingResult { data: OsmPendingItem[]; total: number; page: number; limit: number; totalPages: number; } export interface ListOsmPendingParams { page?: number; limit?: number; q?: string; province?: string; /** Diện tích tối thiểu (ha). Default backend = 50 để lọc bớt nhà máy lẻ. */ minAreaHa?: number; region?: VietnamRegion; } // ─── Labels ───────────────────────────────────────────── export const PARK_STATUS_LABELS: Record = { PLANNING: 'Quy hoạch', UNDER_CONSTRUCTION: 'Đang xây dựng', OPERATIONAL: 'Đang hoạt động', FULL: 'Đã lấp đầy', }; export const PARK_STATUS_COLORS: Record = { PLANNING: 'bg-blue-100 text-blue-800', UNDER_CONSTRUCTION: 'bg-amber-100 text-amber-800', OPERATIONAL: 'bg-green-100 text-green-800', FULL: 'bg-red-100 text-red-800', }; export const REGION_LABELS: Record = { NORTH: 'Miền Bắc', CENTRAL: 'Miền Trung', 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 = { search: (params: SearchIndustrialParksParams = {}) => { 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/parks${qs ? `?${qs}` : ''}`, ); }, /** PARK_OPERATOR / ADMIN only — returns KCN owned by the current user. */ searchMine: (params: SearchIndustrialParksParams = {}) => { 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/parks/mine/list${qs ? `?${qs}` : ''}`, ); }, getBySlug: (slug: string) => apiClient.get(`/industrial/parks/${slug}`), compare: (ids: string[]) => apiClient.post('/industrial/parks/compare', { ids }), getStats: () => apiClient.get('/industrial/parks/stats'), 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}` : ''}`, ); }, createPark: (payload: CreateIndustrialParkPayload) => apiClient.post('/industrial/parks', payload), updatePark: (id: string, payload: UpdateIndustrialParkPayload) => apiClient.patch(`/industrial/parks/${id}`, payload), deletePark: (id: string) => apiClient.delete<{ success: boolean }>(`/industrial/parks/${id}`), // ─── OSM admin endpoints (ADMIN role only) ─────────── listOsmPending: (params: ListOsmPendingParams = {}) => { 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/parks/osm/pending${qs ? `?${qs}` : ''}`, ); }, /** Promote OSM row → public OSM_PROMOTED. Optionally lock fields the admin * just edited so the next sync run leaves them alone. */ promoteOsm: (id: string, lockFields: string[] = []) => apiClient.post<{ id: string }>(`/industrial/parks/${id}/osm/promote`, { lockFields, }), /** Toggle the row-level OSM lock. When `true`, sync skips this row entirely. */ lockOsm: (id: string, locked: boolean) => apiClient.post<{ id: string; locked: boolean }>( `/industrial/parks/${id}/osm/lock`, { locked }, ), };