Migration SQL (20260422120000_industrial_usd_to_decimal) and Prisma schema already reflected Decimal(18,4). This commit completes the TypeScript / frontend layer. API changes: - Domain repo interfaces (IndustrialListingListItem, IndustrialListingDetailData, IndustrialParkListItem, IndustrialParkDetailData, IndustrialMarketData): USD money fields changed from number|null → string|null (PostgreSQL numeric serialises as string in raw query results) - Raw DB interface types in Prisma repositories updated to string|null for Decimal columns - toDomain() mappers: parseFloat() added where entity props require number|null for business-logic arithmetic - estimate-industrial-rent handler: Number() cast on Prisma ORM Decimal objects before arithmetic and comparisons Web changes: - khu-cong-nghiep-api.ts: IndustrialParkListItem, IndustrialParkDetail, IndustrialListingItem, IndustrialMarketData USD fields → string|null with JSDoc - listing-card.tsx: parseFloat() wrapping for priceUsdM2/totalLeasePrice display - park-compare-client.tsx: parseFloat() for landRentUsdM2Year in radar score Note: pre-existing test failures in filter-bar/login/search specs are unrelated to this migration (confirmed present on branch before this change). Co-Authored-By: Paperclip <noreply@paperclip.ing>
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
'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
|
||
? `$${parseFloat(listing.priceUsdM2)}/${listing.pricingUnit ?? 'm²/tháng'}`
|
||
: listing.totalLeasePrice
|
||
? `$${parseFloat(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 (
|
||
<Card className="group h-full transition-shadow hover:shadow-lg">
|
||
<CardContent className="p-5">
|
||
{/* Header badges */}
|
||
<div className="mb-3 flex flex-wrap items-center gap-2">
|
||
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
|
||
{PROPERTY_TYPE_LABELS[listing.propertyType]}
|
||
</Badge>
|
||
<Badge variant="outline">
|
||
{LEASE_TYPE_LABELS[listing.leaseType]}
|
||
</Badge>
|
||
</div>
|
||
|
||
{/* Title */}
|
||
<h3 className="mb-2 line-clamp-2 font-semibold text-foreground group-hover:text-primary">
|
||
{listing.title}
|
||
</h3>
|
||
|
||
{/* Park location */}
|
||
<div className="mb-3 flex items-center gap-1 text-sm text-muted-foreground">
|
||
<MapPin className="h-3.5 w-3.5 shrink-0" />
|
||
<a
|
||
href={`/khu-cong-nghiep/${listing.parkSlug}`}
|
||
className="line-clamp-1 hover:text-primary hover:underline"
|
||
>
|
||
{listing.parkName}
|
||
</a>
|
||
</div>
|
||
|
||
{/* Stats grid */}
|
||
<div className="mb-3 grid grid-cols-2 gap-3">
|
||
<div className="rounded-md bg-muted p-2">
|
||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||
<Ruler className="h-3 w-3" />
|
||
Diện tích
|
||
</div>
|
||
<div className="font-semibold">{listing.areaM2.toLocaleString()} m²</div>
|
||
</div>
|
||
<div className="rounded-md bg-muted p-2">
|
||
<div className="text-xs text-muted-foreground">Giá thuê</div>
|
||
<div className="font-semibold text-primary">{priceText}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Additional info */}
|
||
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||
{listing.ceilingHeightM && (
|
||
<span>Cao trần: {listing.ceilingHeightM}m</span>
|
||
)}
|
||
{leaseTermText && (
|
||
<span className="flex items-center gap-1">
|
||
<Calendar className="h-3 w-3" />
|
||
{leaseTermText}
|
||
</span>
|
||
)}
|
||
{listing.viewCount > 0 && (
|
||
<span className="flex items-center gap-1">
|
||
<Eye className="h-3 w-3" />
|
||
{listing.viewCount}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|