feat: dashboard CRUD for Projects + Industrial Parks, listings delete, BĐS homepage card
Some checks failed
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m15s
Deploy / Build API Image (push) Failing after 20s
Deploy / Build AI Services Image (push) Failing after 12s
E2E Tests / Playwright E2E (push) Failing after 16s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 35s
Security Scanning / Trivy Filesystem Scan (push) Failing after 30s
Backup Verification / Backup Restore Verification (push) Failing after 14m37s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m4s
Security Scanning / Trivy Scan — Web Image (push) Failing after 36s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 11m6s
Deploy / Build Web Image (push) Failing after 12s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 8s
CI / E2E Tests (push) Has been skipped
Security Scanning / Security Gate (push) Has been cancelled
Some checks failed
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m15s
Deploy / Build API Image (push) Failing after 20s
Deploy / Build AI Services Image (push) Failing after 12s
E2E Tests / Playwright E2E (push) Failing after 16s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 35s
Security Scanning / Trivy Filesystem Scan (push) Failing after 30s
Backup Verification / Backup Restore Verification (push) Failing after 14m37s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m4s
Security Scanning / Trivy Scan — Web Image (push) Failing after 36s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 11m6s
Deploy / Build Web Image (push) Failing after 12s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 8s
CI / E2E Tests (push) Has been skipped
Security Scanning / Security Gate (push) Has been cancelled
Backend — DELETE endpoints (hard delete, ADMIN or owner):
- DELETE /projects/:id (Admin) — new DeleteProjectCommand/Handler,
repository.delete() adapter, module wiring.
- DELETE /industrial/parks/:id (Admin) — same pattern.
- DELETE /listings/:id (JWT + owner-or-Admin check in handler).
Frontend — API clients:
- lib/du-an-api.ts: add create/update/delete + CreateProjectPayload,
UpdateProjectPayload types.
- lib/khu-cong-nghiep-api.ts: add createPark/updatePark/deletePark +
Create/Update payload types.
- lib/listings-api.ts: add delete().
Dashboard pages — new:
- /projects (Quản lý dự án): list with filters + edit/delete actions,
/projects/new form (sectioned Cards, zod-validated), /projects/[id]/edit
with danger-zone delete.
- /industrial-parks (Quản lý KCN): same triad. Fix occupancy-rate display
(percentage already 0-100, no need to *100).
Dashboard listings page:
- Add Edit/Delete row actions with confirm + useMutation; error banner
on mutation failure. Table view gains a "Thao tác" column; list view
gains a footer action bar below each card.
Dashboard nav:
- Catalog group: /du-an → /projects (Quản lý dự án), /khu-cong-nghiep
→ /industrial-parks (Quản lý KCN). Desktop primaryNav updated too.
Public homepage:
- Add "Bất động sản" as a 5th feature card/tab → /search, using
listingsApi for the "Featured listings" section.
- Bump grid to lg:grid-cols-5, update features subtitle copy ("Năm/Five
core services").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,10 +22,12 @@ import { Link, useRouter } from '@/i18n/navigation';
|
||||
import { transferApi, type TransferListingListItem } from '@/lib/chuyen-nhuong-api';
|
||||
import { duAnApi, type ProjectSummary } from '@/lib/du-an-api';
|
||||
import { industrialApi, type IndustrialParkListItem } from '@/lib/khu-cong-nghiep-api';
|
||||
import { listingsApi, type ListingDetail } from '@/lib/listings-api';
|
||||
|
||||
type FeatureKey = 'projects' | 'industrial' | 'transfer' | 'valuation';
|
||||
type FeatureKey = 'listings' | 'projects' | 'industrial' | 'transfer' | 'valuation';
|
||||
|
||||
const FEATURES: { key: FeatureKey; href: string; icon: LucideIcon }[] = [
|
||||
{ key: 'listings', href: '/search', icon: Home },
|
||||
{ key: 'projects', href: '/du-an', icon: Building2 },
|
||||
{ key: 'industrial', href: '/khu-cong-nghiep', icon: Factory },
|
||||
{ key: 'transfer', href: '/chuyen-nhuong', icon: ArrowRightLeft },
|
||||
@@ -56,6 +58,7 @@ type FeaturedItem = {
|
||||
};
|
||||
|
||||
const VIEW_ALL_HREFS: Record<FeatureKey, string> = {
|
||||
listings: '/search',
|
||||
projects: '/du-an',
|
||||
industrial: '/khu-cong-nghiep',
|
||||
transfer: '/chuyen-nhuong',
|
||||
@@ -81,6 +84,7 @@ export default function LandingPage() {
|
||||
const [projects, setProjects] = React.useState<ProjectSummary[]>([]);
|
||||
const [parks, setParks] = React.useState<IndustrialParkListItem[]>([]);
|
||||
const [transfers, setTransfers] = React.useState<TransferListingListItem[]>([]);
|
||||
const [listings, setListings] = React.useState<ListingDetail[]>([]);
|
||||
const [loadingFeatured, setLoadingFeatured] = React.useState(true);
|
||||
const [featuredError, setFeaturedError] = React.useState(false);
|
||||
|
||||
@@ -93,11 +97,13 @@ export default function LandingPage() {
|
||||
setLoadingFeatured(true);
|
||||
setFeaturedError(false);
|
||||
const request =
|
||||
feature === 'projects'
|
||||
? duAnApi.search({ limit: 4 }).then((res) => setProjects(res.data))
|
||||
: feature === 'industrial'
|
||||
? industrialApi.search({ limit: 4 }).then((res) => setParks(res.data))
|
||||
: transferApi.search({ limit: 4 }).then((res) => setTransfers(res.data));
|
||||
feature === 'listings'
|
||||
? listingsApi.search({ limit: 4, status: 'ACTIVE' }).then((res) => setListings(res.data))
|
||||
: feature === 'projects'
|
||||
? duAnApi.search({ limit: 4 }).then((res) => setProjects(res.data))
|
||||
: feature === 'industrial'
|
||||
? industrialApi.search({ limit: 4 }).then((res) => setParks(res.data))
|
||||
: transferApi.search({ limit: 4 }).then((res) => setTransfers(res.data));
|
||||
request
|
||||
.catch(() => setFeaturedError(true))
|
||||
.finally(() => setLoadingFeatured(false));
|
||||
@@ -108,6 +114,22 @@ export default function LandingPage() {
|
||||
}, [activeFeature, fetchFeatured]);
|
||||
|
||||
const featuredItems: FeaturedItem[] = React.useMemo(() => {
|
||||
if (activeFeature === 'listings') {
|
||||
return listings.map((l) => ({
|
||||
id: l.id,
|
||||
href: `/listings/${l.id}`,
|
||||
imageUrl: l.property.media?.[0]?.url ?? null,
|
||||
fallbackIcon: Home,
|
||||
title: l.property.title,
|
||||
location: `${l.property.district}, ${l.property.city}`,
|
||||
priceLabel: `${formatVND(l.priceVND)} VNĐ`,
|
||||
meta: [
|
||||
`${l.property.areaM2} m²`,
|
||||
l.property.bedrooms != null ? `${l.property.bedrooms} PN` : null,
|
||||
l.transactionType === 'SALE' ? 'Bán' : 'Cho thuê',
|
||||
].filter(Boolean) as string[],
|
||||
}));
|
||||
}
|
||||
if (activeFeature === 'projects') {
|
||||
return projects.map((p) => ({
|
||||
id: p.id,
|
||||
@@ -145,7 +167,7 @@ export default function LandingPage() {
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}, [activeFeature, projects, parks, transfers]);
|
||||
}, [activeFeature, projects, parks, transfers, listings]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -244,7 +266,7 @@ export default function LandingPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{FEATURES.map((feature) => (
|
||||
<Link key={feature.key} href={feature.href} className="group">
|
||||
<div className="flex h-full flex-col rounded-xl border bg-card p-6 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md">
|
||||
|
||||
Reference in New Issue
Block a user