- Add @@unique([subscriptionId, metric, periodStart, periodEnd]) constraint
to UsageRecord model with corresponding migration
- Replace racy findFirst+update/create pattern with Prisma upsert using
INSERT ON CONFLICT DO UPDATE SET count = count + delta
- Fix CheckQuotaHandler to use period-scoped findUnique instead of
unscoped findFirst, preventing stale cross-period reads
- Update tests to reflect atomic upsert pattern
Closes GOO-4
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Foundation for Phase E (AI advisor / AI valuation on listing detail).
An admin sets the Anthropic Claude credentials once in the new
"/admin/settings/ai" page; downstream features read them via
SystemSettingsService.
Database
--------
- New Prisma model SystemSetting { key @id, value Text, valueType,
isSecret, updatedAt, updatedBy }. db:push applied cleanly.
Backend
-------
- SystemSettingsService — canonical getter/setter for
ai.api_url / ai.api_key / ai.model. maskApiKey() returns the last 4
chars prefixed with "sk-ant-...". Exposes unmasked getAiSettings()
for server-side consumers (AI advisor handlers).
- GET /admin/settings/ai — returns { apiUrl, apiKeyMasked, model,
hasApiKey, updatedAt }. Never emits the raw key.
- PATCH /admin/settings/ai — body accepts partial { apiUrl, apiKey,
model }. apiKey sentinel "__UNCHANGED__" preserves the stored value;
empty string clears it; any other value overwrites.
- CQRS: get-ai-settings query + update-ai-settings command. Registered
in admin.module.ts; service exported via modules/admin/index.ts so
Phase E can inject it.
Frontend
--------
- adminApi.getAiSettings() / updateAiSettings() added to
lib/admin-api.ts with shared AiSettings + UpdateAiSettingsPayload
types.
- New Lucide-only nav entry "Cài đặt AI" (Sparkles) in admin layout.
- /admin/settings/ai/page.tsx — Card with API URL input, masked API
key input with Eye/EyeOff toggle, "Xoá key" button, model Select
(claude-opus-4-5 / sonnet-4-5 / haiku-4-5 + custom input), save
button with inline success/error banners, "last updated" timestamp.
- i18n keys adminNav.settings + adminNav.aiSettings in vi.json/en.json.
Constraints
-----------
- No new packages. Runtime imports for NestJS-DI classes preserved.
- Key NOT encrypted at rest (MVP); documented in service comment as
future hardening.
- Page inherits existing admin auth guard via (admin) layout.
Verification
------------
- API typecheck clean.
- Web typecheck clean in touched files.
- API suite: 1975 / 1975 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three asks after a walk-through of the dashboard:
1. Dashboard navigation was missing direct entry points to the two
catalog surfaces (Dự án, Khu Công Nghiệp) even though both exist at
/du-an and /khu-cong-nghiep. Users landing in the dashboard had to
go back out to the public header to reach them.
2. The "Tin đăng" (dashboard listings) page defaulted to a 3-column
grid which shows only a handful of properties per viewport. Scanning
many listings at once is easier as a vertical list of horizontal
rows.
3. The public /search results used the same 3-column grid via
PropertyCard. Asked to flip to list there too.
Changes
- (dashboard)/layout.tsx: new `catalogs` nav group with Building2 +
Factory icons pointing at /du-an and /khu-cong-nghiep. Primary
desktop nav also exposes both so they're reachable without opening
the hamburger. Uses existing `nav.projects` / `nav.industrialParks`
i18n keys plus a new `dashboard.catalogs` label in vi/en.
- (dashboard)/listings/page.tsx: default viewMode flipped from 'grid'
to 'list'. The list mode renders a horizontal row per listing
(thumbnail + title/location + price + badges + engagement counters)
inside an <ul>. Toggle button relabelled "Danh sách".
- components/search/search-results.tsx + property-card.tsx: add a
`layout?: 'card' | 'list'` prop to PropertyCard. When `list`, the
card renders as a horizontal row with 224px thumbnail on sm+,
stacked on mobile. SearchResults wraps items in a <ul><li> and asks
for list layout. Default card layout preserved so other callers
(compare, related, etc.) keep their vertical card view.
No API / DB changes. Typecheck clean for the touched surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mobile menu's logged-in footer previously showed just the user's
full name and a generic "Bảng điều khiển" button that always pointed
at /dashboard, even for ADMIN users whose real console lives at
/admin. The desktop header had the same ADMIN mis-route.
Mobile menu footer — now a compact account card:
- Avatar (avatarUrl) or initials fallback in a primary-tinted circle
(getInitials handles single-word and multi-word names).
- Full name (truncated).
- Secondary line: email if present, otherwise phone.
- Role badge via ROLE_LABELS (Quản trị viên / Đại lý / Người bán /
Người mua) — skipped when the role string isn't in the map.
- Primary CTA: routes to /admin for ADMIN, /dashboard otherwise.
Button label flips to "Admin" vs "Bảng điều khiển" accordingly.
- Secondary CTA: /dashboard/profile with UserIcon.
- Tertiary: destructive-styled Đăng xuất button that calls the
auth-store logout() action then router.push('/').
Desktop header: Dashboard button on the right now uses the same
dashboardHref + label computation so ADMIN users land on /admin.
i18n: added common.profile in both vi.json and en.json.
Verified at 375×812 (mobile preset): card + 3 buttons render within
viewport, initials bubble shows "HH" in red, role badge reads
"Quản trị viên".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Click-to-fill panel above the login form showing 4 seeded accounts
(ADMIN/AGENT/SELLER/BUYER) with role badges. Clicking an account
populates phone + shared demo password into the form, letting
stakeholders try each role without memorizing credentials. Panel is
collapsible and labeled "(MVP)" so it's obvious this is demo-only
scaffolding to remove before production.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add "Giải pháp GoodGo" section after hero with 4 feature cards
linking to the platform's core products: Dự án, Khu công nghiệp,
Chuyển nhượng, Định giá BĐS.
- Convert "Tin đăng nổi bật" from residential-only 3-column grid into a
tabbed section with one tab per core feature. Items render as a
vertical list of horizontal cards (image left, title/location/meta
right, price + arrow). Valuation tab shows a highlight CTA since it's
a tool, not a listing type.
- Remove "Khu vực nổi bật" district quick-links block (didn't fit the
platform's multi-product positioning).
- Fix invisible "Tìm kiếm ngay" button on CTA section — outline variant
defaulted to bg-background (white) masking text-primary-foreground
(white) on the primary background.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Auto-fix 862 lint errors: convert value imports used only as types to
`import type`, fix import group ordering in seed.ts and du-an-api.ts,
remove unused imports in auth controller, and clean up stale eslint-disable
comments referencing non-existent rules.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Build a complete property comparison feature at /compare:
- Zustand store with localStorage persistence for selected listings (2-5)
- Side-by-side comparison table (price, area, price/m², amenities, location, etc.)
- Summary statistics banner (price range, area range, price/m² range)
- "Add to Compare" button on property cards and detail pages
- Floating comparison bar for quick access when listings are selected
- Bilingual i18n support (Vietnamese + English)
- 18 unit tests for store logic and comparison stats computation
- Mobile-responsive layout with horizontal scroll on comparison table
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Complete public pricing page showing all 4 subscription plans (FREE,
AGENT_PRO, INVESTOR, ENTERPRISE) with billing cycle toggle, feature
comparison table, VND formatting, and Vietnamese/English i18n support.
Also adds pricing link to public navigation header and footer.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Add missing auth and search translation namespaces to vi.json and en.json
that are required by login/register pages and search filter-bar component.
Update filter-bar with useTranslations('search'), aria-labels, and
role="search" for WCAG 2.1 AA compliance.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Document all API endpoints (auth, listings, payments, search)
- Include DTOs and request/response body shapes
- Document authentication methods and rate limits
- Provide database and environment configuration
- Include existing test setup (Playwright, Vitest)
- Detail CI/CD pipeline structure
- Recommend K6 endpoints and test patterns
- Provide file location references for quick lookup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>