fix(web): include ward in address display across card views

- property-card.tsx: add ward between address and district in both
  card (line 189) and list (line 95) layouts
- transfer-listing-card.tsx: conditionally prepend ward to
  district/city when ward is non-null
- property-card.spec.tsx: update address test to assert ward is shown,
  add list-layout ward regression test (21/21 pass)

Standard format: {address}, {ward}, {district}, {city}
Compact (project-card, industrial-listing-card): district/city only —
intentional; ProjectSummary has no ward field.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-24 11:57:09 +07:00
parent d7c5b1ca2c
commit fbe28102a1
11 changed files with 118 additions and 17 deletions

View File

@@ -56,7 +56,7 @@ export function TransferListingCard({ listing }: TransferListingCardProps) {
{/* 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">{listing.district}, {listing.city}</span>
<span className="line-clamp-1">{listing.ward ? `${listing.ward}, ` : ''}{listing.district}, {listing.city}</span>
</div>
{/* Price */}

View File

@@ -13,6 +13,7 @@ import {
} from '@/components/ui/dialog';
import { useMarkInquiryRead } from '@/lib/hooks/use-inquiries';
import type { InquiryReadDto } from '@/lib/inquiries-api';
import { formatPhone, zaloHref } from '@/lib/phone';
interface InquiryDetailDialogProps {
inquiry: InquiryReadDto | null;
@@ -42,6 +43,8 @@ export function InquiryDetailDialog({ inquiry, open, onOpenChange }: InquiryDeta
minute: '2-digit',
});
const phone = inquiry.phone ?? inquiry.userPhone;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md sm:max-w-lg">
@@ -60,7 +63,7 @@ export function InquiryDetailDialog({ inquiry, open, onOpenChange }: InquiryDeta
<InquiryStatusBadge isRead={inquiry.isRead} />
</div>
<div className="space-y-1 text-sm text-muted-foreground">
<p>SĐT: {inquiry.phone ?? inquiry.userPhone}</p>
<p>SĐT: {formatPhone(phone)}</p>
<p>Ngày gửi: {formattedDate}</p>
</div>
</div>
@@ -78,13 +81,13 @@ export function InquiryDetailDialog({ inquiry, open, onOpenChange }: InquiryDeta
<h4 className="text-sm font-medium">Liên hệ nhanh</h4>
<div className="flex flex-wrap gap-2">
<a
href={`tel:${inquiry.phone ?? inquiry.userPhone}`}
href={`tel:${phone}`}
className="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition-colors hover:bg-accent"
>
<Phone className="h-4 w-4" aria-hidden="true" /> Gọi điện
</a>
<a
href={`https://zalo.me/${(inquiry.phone ?? inquiry.userPhone).replace(/^0/, '84')}`}
href={zaloHref(phone)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition-colors hover:bg-accent"

View File

@@ -15,6 +15,7 @@ import {
import { Select } from '@/components/ui/select';
import { useDeleteLead, useUpdateLeadStatus } from '@/lib/hooks/use-leads';
import { LEAD_STATUSES, LEAD_SOURCES, type LeadReadDto, type LeadStatus } from '@/lib/leads-api';
import { formatPhone, zaloHref } from '@/lib/phone';
interface LeadDetailDialogProps {
lead: LeadReadDto | null;
@@ -96,7 +97,7 @@ export function LeadDetailDialog({ lead, open, onOpenChange }: LeadDetailDialogP
<LeadStatusBadge status={lead.status} />
</div>
<div className="space-y-1 text-sm text-muted-foreground">
<p>SĐT: {lead.phone}</p>
<p>SĐT: {formatPhone(lead.phone)}</p>
{lead.email && <p>Email: {lead.email}</p>}
<p>Nguồn: {getSourceLabel(lead.source)}</p>
{lead.score !== null && <p>Điểm: {lead.score}/100</p>}
@@ -163,7 +164,7 @@ export function LeadDetailDialog({ lead, open, onOpenChange }: LeadDetailDialogP
</a>
)}
<a
href={`https://zalo.me/${lead.phone.replace(/^0/, '84')}`}
href={zaloHref(lead.phone)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm transition-colors hover:bg-accent"

View File

@@ -106,9 +106,15 @@ describe('PropertyCard', () => {
expect(screen.getByText(/3\.5 tỷ/)).toBeInTheDocument();
});
it('renders property address', () => {
it('renders property address including ward', () => {
render(<PropertyCard listing={makeListing()} />);
expect(screen.getByText(/208 Nguyễn Hữu Cảnh/)).toBeInTheDocument();
expect(screen.getByText(/Phường 22/)).toBeInTheDocument();
});
it('renders ward in list layout', () => {
render(<PropertyCard listing={makeListing()} layout="list" />);
expect(screen.getByText(/Phường 22/)).toBeInTheDocument();
});
it('renders transaction type badge for SALE', () => {

View File

@@ -92,7 +92,7 @@ export function PropertyCard({ listing, compact, layout = 'card' }: PropertyCard
{listing.property.title}
</h3>
<p className="mt-0.5 line-clamp-1 text-sm text-muted-foreground">
{listing.property.address}, {listing.property.district}, {listing.property.city}
{listing.property.address}, {listing.property.ward}, {listing.property.district}, {listing.property.city}
</p>
</div>
<p className="shrink-0 text-lg font-bold text-primary">
@@ -186,7 +186,7 @@ export function PropertyCard({ listing, compact, layout = 'card' }: PropertyCard
</p>
<h3 className="mt-1 line-clamp-1 font-medium">{listing.property.title}</h3>
<p className="mt-1 line-clamp-1 text-sm text-muted-foreground">
{listing.property.address}, {listing.property.district}, {listing.property.city}
{listing.property.address}, {listing.property.ward}, {listing.property.district}, {listing.property.city}
</p>
<ul className="mt-3 flex flex-wrap gap-1.5" aria-label="Thông tin bất động sản">
<li>