feat(web): add khu-cong-nghiep, chuyen-nhuong, and reports pages
Add three new frontend page sections: - Industrial parks (khu-cong-nghiep): listing, detail, filter bar - Transfer listings (chuyen-nhuong): search, category tabs, detail - AI reports dashboard: list, create, viewer with TOC Includes components, API clients, hooks, server helpers, i18n keys, navigation links in public and dashboard layouts, and lint fixes. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
105
apps/web/components/khu-cong-nghiep/park-card.tsx
Normal file
105
apps/web/components/khu-cong-nghiep/park-card.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { Building2, MapPin } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import { type IndustrialParkListItem, PARK_STATUS_COLORS, PARK_STATUS_LABELS, REGION_LABELS } from '@/lib/khu-cong-nghiep-api';
|
||||
|
||||
interface ParkCardProps {
|
||||
park: IndustrialParkListItem;
|
||||
}
|
||||
|
||||
export function ParkCard({ park }: ParkCardProps) {
|
||||
const occupancyColor =
|
||||
park.occupancyRate >= 90 ? 'text-red-600' :
|
||||
park.occupancyRate >= 70 ? 'text-amber-600' :
|
||||
'text-green-600';
|
||||
|
||||
return (
|
||||
<Link href={`/khu-cong-nghiep/${park.slug}`}>
|
||||
<Card className="group h-full transition-shadow hover:shadow-lg">
|
||||
<CardContent className="p-5">
|
||||
{/* Header */}
|
||||
<div className="mb-3 flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="line-clamp-1 font-semibold text-foreground group-hover:text-primary">
|
||||
{park.name}
|
||||
</h3>
|
||||
{park.nameEn && (
|
||||
<p className="line-clamp-1 text-xs text-muted-foreground">{park.nameEn}</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge className={PARK_STATUS_COLORS[park.status]} variant="secondary">
|
||||
{PARK_STATUS_LABELS[park.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
<span className="line-clamp-1">{park.province} · {REGION_LABELS[park.region]}</span>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="mb-3 grid grid-cols-2 gap-3">
|
||||
<div className="rounded-md bg-muted p-2">
|
||||
<div className="text-xs text-muted-foreground">Diện tích</div>
|
||||
<div className="font-semibold">{park.totalAreaHa.toLocaleString()} ha</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-muted p-2">
|
||||
<div className="text-xs text-muted-foreground">Lấp đầy</div>
|
||||
<div className={`font-semibold ${occupancyColor}`}>{park.occupancyRate}%</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-muted p-2">
|
||||
<div className="text-xs text-muted-foreground">Còn trống</div>
|
||||
<div className="font-semibold">{park.remainingAreaHa.toLocaleString()} ha</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-muted p-2">
|
||||
<div className="text-xs text-muted-foreground">Doanh nghiệp</div>
|
||||
<div className="font-semibold">{park.tenantCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rent info */}
|
||||
{park.landRentUsdM2Year && (
|
||||
<div className="mb-3 flex items-center gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Thuê đất: </span>
|
||||
<span className="font-medium text-primary">${park.landRentUsdM2Year}/m²/năm</span>
|
||||
</div>
|
||||
{park.rbfRentUsdM2Month && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">NX: </span>
|
||||
<span className="font-medium">${park.rbfRentUsdM2Month}/m²/th</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Industries */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{park.targetIndustries.slice(0, 3).map((industry) => (
|
||||
<Badge key={industry} variant="outline" className="text-xs">
|
||||
{industry}
|
||||
</Badge>
|
||||
))}
|
||||
{park.targetIndustries.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{park.targetIndustries.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-3 flex items-center gap-3 border-t pt-3 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Building2 className="h-3 w-3" />
|
||||
{park.developer}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user