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:
@@ -0,0 +1,7 @@
|
||||
export class DeleteListingCommand {
|
||||
constructor(
|
||||
public readonly listingId: string,
|
||||
public readonly userId: string,
|
||||
public readonly userRole?: string,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { ForbiddenException, NotFoundException } from '@modules/shared';
|
||||
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
|
||||
import { DeleteListingCommand } from './delete-listing.command';
|
||||
|
||||
@CommandHandler(DeleteListingCommand)
|
||||
export class DeleteListingHandler implements ICommandHandler<DeleteListingCommand> {
|
||||
constructor(
|
||||
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteListingCommand): Promise<void> {
|
||||
const listing = await this.listingRepo.findById(command.listingId);
|
||||
if (!listing) {
|
||||
throw new NotFoundException('Listing', command.listingId);
|
||||
}
|
||||
|
||||
const isOwner = listing.sellerId === command.userId;
|
||||
const isAdmin = command.userRole === 'ADMIN';
|
||||
if (!isOwner && !isAdmin) {
|
||||
throw new ForbiddenException(
|
||||
'Chỉ người bán hoặc quản trị viên mới có thể xóa tin đăng',
|
||||
);
|
||||
}
|
||||
|
||||
await this.listingRepo.delete(command.listingId);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export interface IListingRepository {
|
||||
findByIdWithProperty(id: string): Promise<ListingDetailData | null>;
|
||||
save(listing: ListingEntity): Promise<void>;
|
||||
update(listing: ListingEntity): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
search(params: ListingSearchParams): Promise<PaginatedResult<ListingSearchItem>>;
|
||||
findByStatus(status: ListingStatus, page: number, limit: number): Promise<PaginatedResult<ListingSearchItem>>;
|
||||
findBySellerId(sellerId: string, page: number, limit: number): Promise<PaginatedResult<ListingSellerItem>>;
|
||||
|
||||
@@ -47,6 +47,10 @@ export class PrismaListingRepository implements IListingRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.prisma.listing.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async update(entity: ListingEntity): Promise<void> {
|
||||
await this.prisma.listing.update({
|
||||
where: { id: entity.id },
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { MulterModule } from '@nestjs/platform-express';
|
||||
import { AdminFeatureListingHandler } from './application/commands/admin-feature-listing/admin-feature-listing.handler';
|
||||
import { CreateListingHandler } from './application/commands/create-listing/create-listing.handler';
|
||||
import { DeleteListingHandler } from './application/commands/delete-listing/delete-listing.handler';
|
||||
import { FeatureListingHandler } from './application/commands/feature-listing/feature-listing.handler';
|
||||
import { ModerateListingHandler } from './application/commands/moderate-listing/moderate-listing.handler';
|
||||
import { PromoteFeaturedListingHandler } from './application/commands/promote-featured-listing/promote-featured-listing.handler';
|
||||
@@ -36,6 +37,7 @@ const CommandHandlers = [
|
||||
UpdateListingStatusHandler,
|
||||
UploadMediaHandler,
|
||||
ModerateListingHandler,
|
||||
DeleteListingHandler,
|
||||
];
|
||||
|
||||
const QueryHandlers = [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Ip,
|
||||
Param,
|
||||
@@ -31,6 +32,7 @@ import { NotFoundException, EndpointRateLimit, EndpointRateLimitGuard, FileValid
|
||||
import { RequireQuota, QuotaGuard } from '@modules/subscriptions';
|
||||
import { CreateListingCommand } from '../../application/commands/create-listing/create-listing.command';
|
||||
import type { CreateListingResult } from '../../application/commands/create-listing/create-listing.handler';
|
||||
import { DeleteListingCommand } from '../../application/commands/delete-listing/delete-listing.command';
|
||||
import { FeatureListingCommand } from '../../application/commands/feature-listing/feature-listing.command';
|
||||
import type { FeatureListingResult } from '../../application/commands/feature-listing/feature-listing.handler';
|
||||
import { ModerateListingCommand } from '../../application/commands/moderate-listing/moderate-listing.command';
|
||||
@@ -352,6 +354,23 @@ export class ListingsController {
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({ summary: 'Delete a listing (owner or admin)' })
|
||||
@ApiParam({ name: 'id', description: 'Listing UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Listing deleted successfully' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
@ApiResponse({ status: 403, description: 'Forbidden — not the seller or admin' })
|
||||
@ApiResponse({ status: 404, description: 'Listing not found' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':id')
|
||||
async deleteListing(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<{ success: true }> {
|
||||
await this.commandBus.execute(new DeleteListingCommand(id, user.sub, user.role));
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({
|
||||
summary: 'Promote a listing via subscription entitlement (no payment)',
|
||||
|
||||
Reference in New Issue
Block a user