Files
goodgo-platform/apps/web/components/khu-cong-nghiep/listing-card.tsx
Ho Ngoc Hai 36a9b00cf1 feat(industrial): update TypeScript types for Float→Decimal USD field migration (GOO-27)
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>
2026-04-23 00:34:40 +07:00

96 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}