Files
goodgo-platform/apps/web/lib/chuyen-nhuong-api.ts
Ho Ngoc Hai 7ce651fce5 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>
2026-04-16 09:07:45 +07:00

180 lines
5.3 KiB
TypeScript

import { apiClient } from './api-client';
// ─── Types ──────────────────────────────────────────────
export type TransferCategory = 'FURNITURE' | 'APPLIANCE' | 'OFFICE_EQUIPMENT' | 'KITCHEN' | 'PREMISES' | 'FULL_UNIT';
export type TransferCondition = 'NEW' | 'LIKE_NEW' | 'GOOD' | 'FAIR' | 'WORN';
export type TransferListingStatus = 'DRAFT' | 'PENDING_REVIEW' | 'ACTIVE' | 'RESERVED' | 'SOLD' | 'EXPIRED' | 'REJECTED';
export type TransferPricingSource = 'MANUAL' | 'AI_ESTIMATED' | 'NEGOTIABLE';
export interface TransferListingListItem {
id: string;
sellerId: string;
category: TransferCategory;
status: TransferListingStatus;
title: string;
address: string;
district: string;
city: string;
latitude: number;
longitude: number;
askingPriceVND: string; // BigInt serialized as string
aiEstimatePriceVND: string | null;
pricingSource: TransferPricingSource;
isNegotiable: boolean;
areaM2: number | null;
media: { url: string; type: string; order: number; caption?: string }[] | null;
viewCount: number;
inquiryCount: number;
publishedAt: string | null;
itemCount: number;
}
export interface TransferItemData {
id: string;
name: string;
brand: string | null;
modelName: string | null;
category: TransferCategory;
condition: TransferCondition;
purchaseYear: number | null;
originalPriceVND: string | null;
askingPriceVND: string;
aiEstimatePriceVND: string | null;
aiConfidence: number | null;
quantity: number;
dimensions: { widthCm?: number; heightCm?: number; depthCm?: number; weightKg?: number } | null;
media: { url: string; type: string; order: number }[] | null;
notes: string | null;
}
export interface TransferListingDetail {
id: string;
sellerId: string;
category: TransferCategory;
status: TransferListingStatus;
title: string;
description: string | null;
address: string;
ward: string | null;
district: string;
city: string;
latitude: number;
longitude: number;
askingPriceVND: string;
aiEstimatePriceVND: string | null;
aiConfidence: number | null;
pricingSource: TransferPricingSource;
isNegotiable: boolean;
areaM2: number | null;
monthlyRentVND: string | null;
depositMonths: number | null;
remainingLeaseMo: number | null;
businessType: string | null;
footTraffic: string | null;
media: { url: string; type: string; order: number; caption?: string }[] | null;
viewCount: number;
saveCount: number;
inquiryCount: number;
contactPhone: string | null;
contactName: string | null;
items: TransferItemData[];
createdAt: string;
updatedAt: string;
}
export interface TransferStats {
totalListings: number;
totalValue: string;
byCategory: { category: string; count: number; avgPrice: number }[];
byDistrict: { district: string; count: number; avgPrice: number }[];
byStatus: { status: string; count: number }[];
}
export interface PaginatedResult<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface SearchTransferListingsParams {
q?: string;
category?: TransferCategory;
status?: TransferListingStatus;
district?: string;
city?: string;
minPrice?: number;
maxPrice?: number;
page?: number;
limit?: number;
}
// ─── Labels ─────────────────────────────────────────────
export const CATEGORY_LABELS: Record<TransferCategory, string> = {
FURNITURE: 'Nội thất',
APPLIANCE: 'Thiết bị gia dụng',
OFFICE_EQUIPMENT: 'Thiết bị văn phòng',
KITCHEN: 'Bếp & thiết bị',
PREMISES: 'Mặt bằng',
FULL_UNIT: 'Trọn bộ',
};
export const CATEGORY_ICONS: Record<TransferCategory, string> = {
FURNITURE: '🛋️',
APPLIANCE: '🧊',
OFFICE_EQUIPMENT: '🖥️',
KITCHEN: '🍳',
PREMISES: '🏪',
FULL_UNIT: '🏠',
};
export const CONDITION_LABELS: Record<TransferCondition, string> = {
NEW: 'Mới',
LIKE_NEW: 'Như mới',
GOOD: 'Tốt',
FAIR: 'Khá',
WORN: 'Cũ',
};
export const CONDITION_COLORS: Record<TransferCondition, string> = {
NEW: 'bg-green-100 text-green-800',
LIKE_NEW: 'bg-emerald-100 text-emerald-800',
GOOD: 'bg-blue-100 text-blue-800',
FAIR: 'bg-amber-100 text-amber-800',
WORN: 'bg-red-100 text-red-800',
};
export const STATUS_LABELS: Record<TransferListingStatus, string> = {
DRAFT: 'Nháp',
PENDING_REVIEW: 'Chờ duyệt',
ACTIVE: 'Đang đăng',
RESERVED: 'Đã giữ',
SOLD: 'Đã bán',
EXPIRED: 'Hết hạn',
REJECTED: 'Từ chối',
};
// ─── API Functions ──────────────────────────────────────
export const transferApi = {
search: (params: SearchTransferListingsParams = {}) => {
const query = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== '') query.append(key, String(value));
});
const qs = query.toString();
return apiClient.get<PaginatedResult<TransferListingListItem>>(
`/transfer/listings${qs ? `?${qs}` : ''}`,
);
},
getById: (id: string) =>
apiClient.get<TransferListingDetail>(`/transfer/listings/${id}`),
getStats: () =>
apiClient.get<TransferStats>('/transfer/stats'),
};