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:
@@ -128,6 +128,59 @@ export interface PaginatedResult<T> {
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface CreateProjectPayload {
|
||||
name: string;
|
||||
slug: string;
|
||||
developer: string;
|
||||
developerLogo?: string;
|
||||
totalUnits: number;
|
||||
status: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
ward: string;
|
||||
district: string;
|
||||
city: string;
|
||||
description?: string;
|
||||
amenities?: Record<string, unknown>;
|
||||
masterPlanUrl?: string;
|
||||
minPrice?: string;
|
||||
maxPrice?: string;
|
||||
pricePerM2Range?: Record<string, unknown>;
|
||||
totalArea?: number;
|
||||
buildingCount?: number;
|
||||
floorCount?: number;
|
||||
unitTypes?: Record<string, unknown>;
|
||||
tags?: string[];
|
||||
startDate?: string;
|
||||
completionDate?: string;
|
||||
}
|
||||
|
||||
export interface UpdateProjectPayload {
|
||||
name?: string;
|
||||
developer?: string;
|
||||
developerLogo?: string | null;
|
||||
totalUnits?: number;
|
||||
completedUnits?: number;
|
||||
status?: string;
|
||||
description?: string | null;
|
||||
amenities?: Record<string, unknown> | null;
|
||||
masterPlanUrl?: string | null;
|
||||
minPrice?: string | null;
|
||||
maxPrice?: string | null;
|
||||
pricePerM2Range?: Record<string, unknown> | null;
|
||||
totalArea?: number | null;
|
||||
buildingCount?: number | null;
|
||||
floorCount?: number | null;
|
||||
unitTypes?: Record<string, unknown> | null;
|
||||
media?: Record<string, unknown>[] | null;
|
||||
documents?: Record<string, unknown>[] | null;
|
||||
tags?: string[];
|
||||
isVerified?: boolean;
|
||||
startDate?: string | null;
|
||||
completionDate?: string | null;
|
||||
}
|
||||
|
||||
export interface SearchProjectsParams {
|
||||
city?: string;
|
||||
district?: string;
|
||||
@@ -196,4 +249,13 @@ export const duAnApi = {
|
||||
|
||||
submitInquiry: (projectId: string, data: { name: string; phone: string; message: string }) =>
|
||||
apiClient.post<{ inquiryId: string }>(`/projects/${projectId}/inquiries`, data),
|
||||
|
||||
create: (payload: CreateProjectPayload) =>
|
||||
apiClient.post<{ id: string; slug: string }>('/projects', payload),
|
||||
|
||||
update: (id: string, payload: UpdateProjectPayload) =>
|
||||
apiClient.patch<ProjectDetail>(`/projects/${id}`, payload),
|
||||
|
||||
delete: (id: string) =>
|
||||
apiClient.delete<{ success: boolean }>(`/projects/${id}`),
|
||||
};
|
||||
|
||||
@@ -149,6 +149,59 @@ export interface SearchIndustrialListingsParams {
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface CreateIndustrialParkPayload {
|
||||
name: string;
|
||||
nameEn?: string;
|
||||
slug: string;
|
||||
developer: string;
|
||||
operator?: string;
|
||||
status: IndustrialParkStatus;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
address: string;
|
||||
district: string;
|
||||
province: string;
|
||||
region: VietnamRegion;
|
||||
totalAreaHa: number;
|
||||
leasableAreaHa: number;
|
||||
occupancyRate: number;
|
||||
remainingAreaHa: number;
|
||||
tenantCount?: number;
|
||||
establishedYear?: number;
|
||||
landRentUsdM2Year?: number;
|
||||
rbfRentUsdM2Month?: number;
|
||||
rbwRentUsdM2Month?: number;
|
||||
managementFeeUsd?: number;
|
||||
infrastructure?: Record<string, unknown>;
|
||||
connectivity?: Record<string, unknown>;
|
||||
incentives?: Record<string, unknown>;
|
||||
targetIndustries: string[];
|
||||
description?: string;
|
||||
descriptionEn?: string;
|
||||
}
|
||||
|
||||
export interface UpdateIndustrialParkPayload {
|
||||
name?: string;
|
||||
nameEn?: string;
|
||||
developer?: string;
|
||||
operator?: string;
|
||||
status?: IndustrialParkStatus;
|
||||
occupancyRate?: number;
|
||||
remainingAreaHa?: number;
|
||||
tenantCount?: number;
|
||||
landRentUsdM2Year?: number;
|
||||
rbfRentUsdM2Month?: number;
|
||||
rbwRentUsdM2Month?: number;
|
||||
managementFeeUsd?: number;
|
||||
infrastructure?: Record<string, unknown>;
|
||||
connectivity?: Record<string, unknown>;
|
||||
incentives?: Record<string, unknown>;
|
||||
targetIndustries?: string[];
|
||||
description?: string;
|
||||
descriptionEn?: string;
|
||||
isVerified?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
@@ -243,4 +296,13 @@ export const industrialApi = {
|
||||
`/industrial/listings${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
},
|
||||
|
||||
createPark: (payload: CreateIndustrialParkPayload) =>
|
||||
apiClient.post<IndustrialParkDetail>('/industrial/parks', payload),
|
||||
|
||||
updatePark: (id: string, payload: UpdateIndustrialParkPayload) =>
|
||||
apiClient.patch<IndustrialParkDetail>(`/industrial/parks/${id}`, payload),
|
||||
|
||||
deletePark: (id: string) =>
|
||||
apiClient.delete<{ success: boolean }>(`/industrial/parks/${id}`),
|
||||
};
|
||||
|
||||
@@ -165,6 +165,9 @@ export const listingsApi = {
|
||||
data,
|
||||
),
|
||||
|
||||
delete: (id: string) =>
|
||||
apiClient.delete<{ success: boolean }>(`/listings/${id}`),
|
||||
|
||||
getById: (id: string) => apiClient.get<ListingDetail>(`/listings/${id}`),
|
||||
|
||||
search: (params: SearchListingsParams = {}) => {
|
||||
|
||||
Reference in New Issue
Block a user