feat(login_action):login is working but register with email still error

parent 2b1aba07
......@@ -4,7 +4,7 @@
NODE_ENV=development
PORT=3001
BACKEND_URL=http://localhost:3001
FRONTEND_URL=http://localhost:3000,http://localhost:3001
FRONTEND_URL=http://localhost:4001
PROJECT_NAME=SSO VietProDev
PROJECT_VERSION=1.0.0
......
# Authors
Nguyen Thi Nguyet Que <quentn0620@gmail.com> - Original Author and Creator
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -7,12 +7,18 @@ services:
image: postgres:17-alpine
container_name: sso-postgres
restart: unless-stopped
ports:
- '${POSTGRES_PORT:-5432}:5432'
# ⚠️ Port 5432 is used by local Windows PostgreSQL (vietprodev_sso).
# The SSO server connects to local PostgreSQL, NOT to this container.
# Remove the port mapping to avoid conflict:
# ports:
# - '${POSTGRES_PORT:-5432}:5432'
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-sso}
# Container creates its own database named after POSTGRES_DB.
# This is SEPARATE from local PostgreSQL's vietprodev_sso.
# Migrations should run against the local PostgreSQL (pnpm db:setup).
POSTGRES_DB: ${DB_NAME:-vietprodev_sso}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
......@@ -23,6 +29,31 @@ services:
timeout: 5s
retries: 5
# ─────────────────────────────────────────────
# postgres-backup: Full database backup instance on a SEPARATE port.
# Port 5433 avoids conflict with local Windows PostgreSQL (5432).
# This container is started MANUALLY: docker compose up -d postgres-backup
# ─────────────────────────────────────────────
postgres-backup:
image: postgres:17-alpine
container_name: sso-postgres-backup
restart: unless-stopped
ports:
- '5433:5432' # Exposed on 5433 (pgAdmin default), not 5432
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-vietprodev_sso}_backup
volumes:
- postgres_backup_data:/var/lib/postgresql/data
networks:
- sso-network
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-postgres}']
interval: 10s
timeout: 5s
retries: 5
mongo:
image: mongo:7
container_name: sso-mongo
......@@ -198,6 +229,7 @@ networks:
volumes:
postgres_data:
postgres_backup_data:
mongo_data:
redis_data:
minio_data:
......@@ -170,14 +170,16 @@ Folder names use **singular kebab-case**: `user`, `role`, `file`, `role-permissi
```
src/controllers/api/v1/
├── auth/ # Login, register, refresh, logout
├── user/ # CRUD with role management
├── role/ # RBAC role management
├── permission/ # Permission definitions
├── auth/ # Login, register, refresh, logout, email verification
├── users/ # CRUD with role management
├── roles/ # RBAC role management
├── permissions/ # Permission definitions
├── role-permission/ # Role ↔ Permission mapping
├── user-role/ # User ↔ Role assignment
├── file/ # File upload and storage
└── notifications/ # Push, email, in-app notifications
├── files/ # File upload and storage
├── notifications/ # Push, email, in-app notifications
├── audit-logs/ # System audit logs
└── audit-dead-letters/ # Failed audit event DLQ
```
Each module has: Controller · Service · Provider · Validator
......
......@@ -77,7 +77,7 @@ DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=your_password
DB_NAME=bekind
DB_NAME=vietprodev_sso
```
### D3. Enable Redis
......
# BEKIND Backend — Coding Conventions
# SSO VietProDev Backend — Coding Conventions
Author: Nguyen Thi Nguyet Que Created on: May 2, 2026
......@@ -66,38 +66,39 @@ Architecture (Controller → Service → Provider → Model)
```
src/
constants/
controllers/api/v1/
auth/
file/
notifications/
permission/
role/
role-permission/
user/
user-role/
interfaces/
error/
handler/
violations/
middlewares/
mod/
validators/
models/
providers/
services/auth/
services/data/
services/database/sequelize/
services/file-system/
services/notification/
helpers/
services/scheduler/
jobs/
services/storage/
templates/base/
templates/email/
templates/swagger/
utils/
├── constants/ # Enums, error codes, roles, statuses
├── config/ # Env config with Zod validation
├── controllers/
│ ├── admin/ # Admin API (clients, users)
│ └── api/v1/ # REST API (auto-mounted by express-automatic-routes)
│ ├── auth/ # Login, register, verify-email, resend-verification
│ ├── users/
│ ├── roles/
│ ├── permissions/
│ ├── role-permission/
│ ├── user-role/
│ ├── files/
│ ├── notifications/
│ ├── audit-logs/
│ └── audit-dead-letters/
├── contracts/ # Zod schemas + OpenAPI paths
├── dto/ # Data transfer objects
├── interfaces/ # Shared TypeScript types
│ └── error/
├── middlewares/ # Auth, validators, rate-limiter, CSP
├── models/ # Sequelize models (auto-generated)
├── oidc/ # OIDC/OAuth2 provider + interactions
├── providers/ # Data access layer (CRUD per model)
├── services/
│ ├── auth/ # Auth, token, password, email verification
│ ├── database/ # Sequelize, multi-pool, Redis adapters
│ ├── file-system/ # Logging
│ ├── notification/ # Email, push, in-app notifications
│ └── scheduler/ # BullMQ jobs
├── templates/
│ ├── base/ # Base classes (Provider, Controller)
│ └── email/ # Email HTML templates
└── utils/ # Pure helpers, Logger, auth utilities
```
---
......@@ -236,14 +237,15 @@ Use tsconfig paths — no relative imports beyond sibling files.
## 7. Error Handling
Use `GenericError` from `#interfaces/error/generic`:
```typescript
throw new MeUError(404, 'DB', `Record not found`);
throw new MeUError(409, 'DB', { duplicateFields });
throw GenericError.create({ vi: 'Không tìm thấy', en: 'Not found' }, 'NOT_FOUND', 404);
throw GenericError.create({ vi: 'Email đã tồn tại', en: 'Email already exists' }, 'CONFLICT', 409, { field: 'email' });
```
- `errorCode`: numeric (404, 409, -999)
- `errorType`: `'API'` | `'DB'`
- `errorData`: optional context object
- Never return raw error objects or stack traces
- Use `res.error(error)` in controllers — never `res.json({ error: error.stack })`
---
......
This diff is collapsed.
......@@ -32,7 +32,7 @@ DB_HOST=localhost
DB_PORT=5432
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_NAME=bekind
DB_NAME=vietprodev_sso
REDIS_HOST=localhost
REDIS_PORT=6379
......@@ -70,7 +70,7 @@ See `.env.example` for the full list (email, storage, notifications, virus scan,
PostgreSQL must be running before this step.
```bash
psql -U postgres -c "CREATE DATABASE bekind;"
psql -U postgres -c "CREATE DATABASE vietprodev_sso;"
```
---
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Bills API — Hướng dẫn chi tiết cho Frontend
Tài liệu này mô tả API `bills` (hóa đơn) để đội frontend hiểu cách gọi, cấu trúc dữ liệu, quy tắc validate, và logic nghiệp vụ liên quan.
**Base URL**: /api/v1/bills
---
## Tổng quan
- Mục đích: quản lý hóa đơn (tạo, liệt kê, xem chi tiết, cập nhật, xóa) gắn với `room``resident`.
- Yêu cầu chung: endpoint yêu cầu authentication; nhiều route yêu cầu quyền `admin` (middleware `requireAdmin`).
- Định dạng ngày:
- `billing_period`: `YYYY-MM` (ví dụ `2026-05`)
- `due_date`: `YYYY-MM-DD` (DATE)
- Các thời gian trả về (`created_at`, `updated_at`) dùng ISO 8601 datetime string
---
## Các trường chính (tổng hợp)
- `id` (UUID): id của hóa đơn
- `code` (string): mã hóa đơn sinh tự động
- `room_id` (UUID): id phòng
- `resident_id` (UUID): id cư dân (người liên quan đến hóa đơn)
- `billing_period` (string): kỳ thu, format `YYYY-MM`
- `due_date` (string): hạn trả, `YYYY-MM-DD`
- `payment_method` (string|null)
- `notes` (string|null)
- `total_amount` (number|null)
- `status` (string): `draft` | `paid` | `pending` | `overdue`
- `created_by` (UUID|null)
- `created_at`, `updated_at`, `deleted_at` (datetime strings)
### bill_items
Mỗi `bill` có một mảng `bill_items` mô tả các khoản phí thuộc hóa đơn.
- Trường trả về (`BillItem`):
- `id` (UUID)
- `bill_id` (UUID)
- `fee_name` (string)
- `quantity` (integer)
- `unit_price` (number)
- `created_at` (datetime)
- Trường gửi khi tạo (`BillItemCreate`):
- `fee_name`: required string
- `quantity`: optional integer (default 1)
- `unit_price`: required number >= 0
---
## Endpoint: POST /api/v1/bills — Tạo hóa đơn mới
- Authentication: có (middleware `verify`), thường yêu cầu `requireAdmin`.
- Body: `application/json` theo `BillCreateBody`.
### Request body (example)
```json
{
"room_id": "466256c1-0cac-4784-af74-8a16b5576853",
"resident_id": "77862ec4-3c2b-4e95-8c90-dce5226e76f1",
"billing_period": "2026-05",
"due_date": "2026-05-30",
"payment_method": "cash",
"notes": "Thanh toán tiền phòng tháng 5",
"total_amount": 5000000,
"status": "pending",
"bill_items": [
{ "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000 },
{ "fee_name": "Điện", "quantity": 1, "unit_price": 500000 },
{ "fee_name": "Nước", "quantity": 1, "unit_price": 500000 }
]
}
```
### Validation rules
- `room_id`, `resident_id`: UUID required
- `billing_period`: regex `^\d{4}-\d{2}$` (YYYY-MM)
- `due_date`: string date (no strict format enforced in validation beyond being a string; API expects `YYYY-MM-DD`)
- `status`: one of `draft`, `paid`, `pending`, `overdue` (default `pending`)
- `bill_items`: non-empty array minimum 1 item; each item must follow `BillItemCreate` schema
- `fee_name`: non-empty string, max 255
- `quantity`: integer >= 1 (default 1)
- `unit_price`: number >= 0
Nếu payload không hợp lệ, API trả `422` với hình thức lỗi:
```json
{
"type": "VALIDATION_ERROR",
"httpStatus": 422,
"messages": { "vi": "Dữ liệu không hợp lệ", "en": "Invalid data" },
"additionalData": { "errors": [ { "field": "bill_items.0.unit_price", "code": "INVALID_NUMBER_TYPE", "message": { ... } } ] }
}
```
### Business logic khi tạo
- Server sinh `code` tự động (`generateBillCode()`) nếu không truyền
- Tạo `bills` record trong transaction
- Nếu có `bill_items`, provider sẽ bulk-create các `bill_items` với `bill_id` gắn với `bill` vừa tạo
- `total_amount` nếu không truyền sẽ mặc định 0; tuy nhiên frontend nên tính tổng từ `bill_items` và gửi chính xác nếu muốn hiển thị ngay
- Ghi `created_by` từ `req.user.id` khi tạo
- Audit log sẽ được tạo (AuditLogService)
### Response
- 200 (hoặc success wrapper): dữ liệu `bill` đã tạo (kèm `bill_items` trả từ DB, có `id`, timestamps)
- Ví dụ:
```json
{
"success": true,
"data": {
"id": "...",
"code": "BILL-202605-0001",
"room_id": "...",
"resident_id": "...",
"billing_period": "2026-05",
"due_date": "2026-05-30",
"total_amount": 5000000,
"status": "pending",
"bill_items": [ { "id": "...", "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000, "created_at": "..." }, ... ],
"created_at": "...",
"updated_at": "..."
}
}
```
---
## Endpoint: GET /api/v1/bills — Danh sách (pagination, filters)
- Query params hỗ trợ bởi `QueryParamsSchema` (chung): `page`, `pageSize`, `sortField`, `sortOrder`, `filters`.
- Backend cung cấp `getAllOptimized` với join để trả `room_name`, `apartment_name`, `building_name`, `resident_full_name`, `resident_email`, `resident_phone`.
- Default pageSize: 20, max 100.
Ví dụ request:
```
GET /api/v1/bills?page=1&pageSize=20&sortField=created_at&sortOrder=desc
```
Response: { count, rows, page, pageSize }
Filter có thể truyền qua `filters` object (thực tế code áp dụng `...(options?.filters || {})` khi build `where`).
---
## Endpoint: GET /api/v1/bills/:id — Chi tiết hóa đơn
- Trả chi tiết bill kèm `bill_items`.
- Nếu không tìm thấy trả 404 (hoặc `null`) — tuỳ cách controller xử lý.
---
## Endpoint: PUT/PATCH /api/v1/bills/:id — Cập nhật
- Body theo `BillUpdateBody` schema (không cho cập nhật `bill_items` qua endpoint này; nếu cần update items, backend có provider/endpoint riêng hoặc cần triển khai).
- Validation tương tự `BillCreateBody` nhưng fields optional.
- Business rule: cập nhật `status` là supported. Nếu thay đổi `total_amount` frontend nên đồng bộ với `bill_items` service.
---
## Xử lý lỗi phổ biến
- 422 VALIDATION_ERROR: payload không hợp lệ (thường lỗi field types/format)
- 500 DATABASE / Sequelize errors: lỗi quan hệ/association—nếu gặp `SequelizeEagerLoadingError`, báo backend xem lại `init-models` associations
- 401/403: thiếu auth hoặc quyền
- 400 Bad JSON: body không phải JSON hợp lệ (ví dụ thiếu quote/comma) — kiểm tra payload JSON trước khi gửi
---
## Gợi ý UI / FE
- Khi tạo hóa đơn từ UI: tính `total_amount` client-side từ `bill_items` và gửi kèm để backend lưu, tránh mismatch
- Hiển thị trạng thái `pending`/`paid`/`overdue` rõ màu sắc
- Khi show form tạo: validate client-side trùng với server (billing_period format, due_date required, unit_price >= 0)
- Sau tạo: cập nhật danh sách bằng refetch `GET /api/v1/bills` hoặc realtime update nếu dùng websockets
---
## Các chú ý kỹ thuật cho FE-devs
- Authorization header: gửi token như bình thường (bearer) — backend sử dụng `verify` middleware.
- Request size: `bill_items` thường nhỏ; nếu tạo nhiều item > 100, cân nhắc batch API
- Định dạng JSON: ensure `due_date` là string `YYYY-MM-DD` khi gửi
- Nếu cần trường mở rộng (discount, tax), thỏa thuận trước với backend — hiện schema không support
---
## Ví dụ đầy đủ (curl)
```bash
curl -X 'POST' \
'http://localhost:3001/api/v1/bills' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <TOKEN>' \
-d '{
"room_id": "466256c1-0cac-4784-af74-8a16b5576853",
"resident_id": "77862ec4-3c2b-4e95-8c90-dce5226e76f1",
"billing_period": "2026-05",
"due_date": "2026-05-30",
"payment_method": "cash",
"notes": "Thanh toán tiền phòng tháng 5",
"total_amount": 5000000,
"status": "pending",
"bill_items": [
{ "fee_name": "Tiền phòng", "quantity": 1, "unit_price": 4000000 },
{ "fee_name": "Điện", "quantity": 1, "unit_price": 500000 },
{ "fee_name": "Nước", "quantity": 1, "unit_price": 500000 }
]
}'
```
---
Nếu bạn muốn mình thêm:
- tệp OpenAPI/Swagger đoạn cho `bills` (examples + response schemas),
- hoặc tạo một mini-PR mẫu ở frontend (fetch helpers) để gọi endpoint này,
hãy nói mình biết ưu tiên nào nhé.
\ No newline at end of file
# Contract & Contract Types API Documentation
## Overview
The Contract API has been refactored to normalize `contract_type` from an ENUM into a separate
`contract_types` table. This allows for dynamic management of contract types without requiring
database schema changes.
**Key Change:** `contract_type` (string ENUM) → `contract_type_id` (UUID foreign key)
## Architecture
### Before (v1 - Deprecated)
```
contracts table:
- contract_type: ENUM('residential_lease', 'room_rental', 'service_agreement', 'other')
```
### After (v2 - Current)
```
contracts table:
- contract_type_id: UUID (FK → contract_types.id)
contract_types table:
- id: UUID (Primary Key)
- code: VARCHAR(50) - Code identifier (residential_lease, room_rental, etc.)
- name_vi: VARCHAR(100) - Vietnamese name
- name_en: VARCHAR(100) - English name
- description: TEXT - Detailed description
- created_at, updated_at, created_by, updated_by: Audit fields
- deleted_at, deleted_by: Soft delete fields
```
## ContractType Schema
### Response Format
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"code": "residential_lease",
"name_vi": "Thuê nhà/căn hộ",
"name_en": "Residential Lease",
"description": "Hợp đồng thuê toàn bộ nhà hoặc căn hộ",
"created_at": "2024-01-15T08:30:00Z",
"updated_at": "2024-01-15T08:30:00Z"
}
```
## Default Contract Types
The system auto-seeds 4 default contract types:
| Code | Vietnamese | English | Description |
| ------------------- | ---------------- | ----------------- | ------------------------------------- |
| `residential_lease` | Thuê nhà/căn hộ | Residential Lease | Hợp đồng thuê toàn bộ nhà hoặc căn hộ |
| `room_rental` | Thuê phòng | Room Rental | Hợp đồng thuê phòng đơn lẻ |
| `service_agreement` | Hợp đồng dịch vụ | Service Agreement | Hợp đồng cung cấp dịch vụ |
| `other` | Khác | Other | Các loại hợp đồng khác |
## Contract API
### Create Contract
**Endpoint:** `POST /api/v1/contracts`
**Request Body:**
```json
{
"resident_id": "456e7890-abcd-1234-efgh-567890123456",
"contract_number": "HĐ-2024-001",
"contract_name": "Hợp đồng thuê phòng A101",
"contract_type_id": "123e4567-e89b-12d3-a456-426614174000",
"description": "Hợp đồng thuê phòng tại tòa nhà A",
"rent_amount": 5000000,
"deposit_amount": 10000000,
"payment_day": 15,
"signed_date": "2024-01-15",
"expiry_date": "2025-01-15"
}
```
**Response:**
```json
{
"success": true,
"data": {
"id": "789abcde-f012-3456-ghij-789012345678",
"resident_id": "456e7890-abcd-1234-efgh-567890123456",
"contract_number": "HĐ-2024-001",
"contract_name": "Hợp đồng thuê phòng A101",
"contract_type_id": "123e4567-e89b-12d3-a456-426614174000",
"description": "Hợp đồng thuê phòng tại tòa nhà A",
"rent_amount": 5000000,
"deposit_amount": 10000000,
"payment_day": 15,
"signed_date": "2024-01-15",
"expiry_date": "2025-01-15",
"created_by": "user-id-here",
"updated_by": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"deleted_at": null
}
}
```
### Update Contract
**Endpoint:** `PUT /api/v1/contracts/{id}`
**Request Body (all fields optional):**
```json
{
"contract_number": "HĐ-2024-002",
"contract_name": "Updated name",
"contract_type_id": "different-uuid-here",
"description": "Updated description",
"rent_amount": 6000000,
"deposit_amount": 12000000,
"payment_day": 20,
"signed_date": "2024-02-01",
"expiry_date": "2025-02-01"
}
```
### Get Contract by ID
**Endpoint:** `GET /api/v1/contracts/{id}`
**Response:**
```json
{
"success": true,
"data": {
"id": "789abcde-f012-3456-ghij-789012345678",
"resident_id": "456e7890-abcd-1234-efgh-567890123456",
"contract_number": "HĐ-2024-001",
"contract_name": "Hợp đồng thuê phòng A101",
"contract_type_id": "123e4567-e89b-12d3-a456-426614174000",
...
}
}
```
### List Contracts
**Endpoint:** `GET /api/v1/contracts`
**Query Parameters:**
```
?page=1&limit=20&orderBy=created_at&orderDir=DESC
```
**Response:**
```json
{
"success": true,
"data": {
"items": [
{
"id": "789abcde-f012-3456-ghij-789012345678",
"resident_id": "456e7890-abcd-1234-efgh-567890123456",
"contract_number": "HĐ-2024-001",
"contract_type_id": "123e4567-e89b-12d3-a456-426614174000",
...
}
],
"total": 42,
"page": 1,
"limit": 20
}
}
```
## Frontend Implementation Guide
### 1. Fetch Contract Types (on page load)
```typescript
// API Call to get all contract types
async function fetchContractTypes() {
const response = await fetch('/api/v1/contract-types', {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch contract types');
}
const data = await response.json();
return data.data; // Array of contract types
}
// Store in state/context
const [contractTypes, setContractTypes] = useState([]);
useEffect(() => {
fetchContractTypes().then(setContractTypes);
}, []);
```
### 2. Display Contract Type Dropdown
```tsx
<select
name="contract_type_id"
required
onChange={(e) => setFormData({ ...formData, contract_type_id: e.target.value })}
value={formData.contract_type_id}
>
<option value="">-- Chọn loại hợp đồng --</option>
{contractTypes.map((type) => (
<option key={type.id} value={type.id}>
{type.name_vi} ({type.code})
</option>
))}
</select>
```
### 3. Create Contract
```typescript
async function createContract(contractData) {
const response = await fetch('/api/v1/contracts', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
resident_id: contractData.resident_id,
contract_number: contractData.contract_number,
contract_name: contractData.contract_name,
contract_type_id: contractData.contract_type_id, // UUID
description: contractData.description,
rent_amount: contractData.rent_amount,
deposit_amount: contractData.deposit_amount,
payment_day: contractData.payment_day,
signed_date: contractData.signed_date,
expiry_date: contractData.expiry_date,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return await response.json();
}
```
### 4. Display Contract with Type Name
```tsx
// After fetching contract and contract types
function ContractCard({ contract, contractTypes }) {
const contractType = contractTypes.find((t) => t.id === contract.contract_type_id);
return (
<div>
<h3>{contract.contract_name}</h3>
<p>
Loại HĐ: <strong>{contractType?.name_vi}</strong>
</p>
<p>Số HĐ: {contract.contract_number}</p>
<p>Giá thuê: {contract.rent_amount?.toLocaleString()} đ</p>
<p>Ngày hết hạn: {new Date(contract.expiry_date).toLocaleDateString('vi-VN')}</p>
</div>
);
}
```
### 5. Update Contract
```typescript
async function updateContract(contractId, updates) {
// Updates can include contract_type_id
const response = await fetch(`/api/v1/contracts/${contractId}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updates),
});
if (!response.ok) {
throw new Error('Failed to update contract');
}
return await response.json();
}
```
## Important Notes
### ✅ What Changed
- Request/Response now uses `contract_type_id` (UUID) instead of `contract_type` (string)
- `contract_type_id` is a **required field** when creating contracts
- You must select an ID from the `contract_types` table
### ⚠️ Migration Checklist for Frontend
- [ ] Update form inputs to use `contract_type_id` instead of `contract_type`
- [ ] Fetch contract types on app initialization
- [ ] Store contract types in state/context for reuse
- [ ] Update dropdowns/selects to show `name_vi` but send `id`
- [ ] Update display logic to lookup type name from contract types array
- [ ] Update validation - ensure `contract_type_id` is UUID format
- [ ] Test create/update/read flows
### 🔍 Debugging Tips
**To see all available contract types:**
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:3000/api/v1/contract-types
```
**To verify contract_type_id in a contract:**
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:3000/api/v1/contracts/{contractId}
```
Look for the `contract_type_id` field - it should be a UUID.
## Error Handling
### Invalid contract_type_id
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid contract_type_id",
"details": {
"field": "contract_type_id",
"reason": "Contract type not found"
}
}
}
```
### Missing contract_type_id
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "contract_type_id is required"
}
}
```
## Related API Endpoints
- `GET /api/v1/contract-types` - List all contract types
- `POST /api/v1/contracts` - Create contract
- `GET /api/v1/contracts` - List contracts
- `GET /api/v1/contracts/{id}` - Get contract by ID
- `PUT /api/v1/contracts/{id}` - Update contract
- `DELETE /api/v1/contracts/{id}` - Delete contract (soft delete)
## Support
For questions or issues with the API, please refer to:
- [API Development Guide](./api-development.md)
- Backend team documentation
- Database schema: [Migration 016](../sql/migrations/016-add-contract-types-table.sql)
# Bulk create — Link files to Contract and Incident
Mục đích
- Cho phép liên kết nhiều file đã upload với một `contract_id` hoặc `incident_id` trong một lần gọi API.
Endpoints
- `POST /api/v1/contract-files` — Link nhiều file với một hợp đồng.
- `POST /api/v1/incident-files` — Link nhiều file với một sự cố.
Xác thực & quyền
- `POST /api/v1/contract-files`: yêu cầu `Bearer` token (middleware: `verify`).
- `POST /api/v1/incident-files`: yêu cầu `Bearer` token và quyền Admin (middleware: `verify`, `requireAdmin`).
Request body (JSON)
- Contract:
- `contract_id` (string, uuid) — Bắt buộc.
- `file_ids` (array[string, uuid]) — Bắt buộc, tối thiểu 1 phần tử.
- Incident:
- `incident_id` (string) — Bắt buộc.
- `file_ids` (array[string]) — Bắt buộc, tối thiểu 1 phần tử.
Ví dụ request — Contract
```json
{
"contract_id": "9fbbf51b-50a3-1aea-baa0-18596c0eebdf/i",
"file_ids": [
"9d9f1450-dd41-79d9-31f4-5b3903429ac3/i",
"f47ac10b-58cc-4372-a567-0e02b2c3d480/i"
]
}
```
Ví dụ request — Incident
```json
{
"incident_id": "9fbbf51b-50a3-1aea-baa0-18596c0eebdf/i",
"file_ids": [
"9d9f1450-dd41-79d9-31f4-5b3903429ac3/i"
]
}
```
Hành vi (Behavior)
- Với mỗi `file_id` trong `file_ids`, API sẽ tạo một bản ghi `ContractFile` hoặc `IncidentFile` tương ứng.
- Trường `created_by` được gán từ `req.user?.id` (người gọi API).
- Toàn bộ thao tác được thực hiện bằng `bulkCreate(...)` trong provider tương ứng (`ContractFileProvider.bulkCreate` hoặc `IncidentFileProvider.bulkCreate`) và chạy trong giao dịch — nếu một phần thất bại thì rollback cả nhóm (atomic).
- Controller trả về mảng các bản ghi vừa tạo (HTTP 201 cho contract-files, HTTP 200 cho incident-files theo controller hiện tại).
Ví dụ response
```json
{
"data": [
{
"id": "bb5ea6d5-fcd7-d0ca-d7ad-8f25a94bedd3",
"contract_id": "9fbbf51b-50a3-1aea-baa0-18596c0eebdf/i",
"file_id": "9d9f1450-dd41-79d9-31f4-5b3903429ac3/i",
"created_by": "user-uuid",
"created_at": "2026-05-18T12:34:56.000Z",
"updated_at": null,
"deleted_at": null
}
]
}
```
Schema validator & controller
- Contract schema: [src/contracts/contract-file/schema.ts](src/contracts/contract-file/schema.ts)
- Contract controller: [src/controllers/api/v1/contract-files/index.ts](src/controllers/api/v1/contract-files/index.ts)
- Incident schema: [src/contracts/incident-file/schema.ts](src/contracts/incident-file/schema.ts)
- Incident controller: [src/controllers/api/v1/incident-files/index.ts](src/controllers/api/v1/incident-files/index.ts)
Lưu ý & khuyến nghị
- `file_ids` là trường bắt buộc; `file_id` (đơn lẻ) không còn được dùng.
- Nếu cần tránh bản ghi trùng (duplicate) nên thực hiện dedupe trước khi gọi API hoặc thêm kiểm tra trong provider.
- Nếu database có ràng buộc khóa ngoại (FK) hoặc ràng buộc unique, lỗi có thể xuất hiện và toàn giao dịch sẽ rollback.
- Nếu muốn thay đổi mã trạng thái trả về cho consistency, cân nhắc trả `201 Created` cho cả hai endpoints.
Hành động tiếp theo (tuỳ chọn)
- Dọn dẹp: xóa hai file tài liệu cũ `contract-files-bulk-create.md``incident-files-bulk-create.md` nếu không cần giữ bản riêng.
- Commit thay đổi tài liệu vào repo.
This diff is collapsed.
**Incident API — Thay Đổi & Lưu Ý cho Frontend**
- **Tổng quan:**
- Backend đã di chuyển thông tin liên hệ của kỹ thuật viên (`full_name`, `phone`, `email`) từ bảng `technicians` sang bảng `users` (qua `technician.user_id`).
- Đã thêm hai trường trả về ở endpoint danh sách: `technician_phone``technician_email`.
**Endpoints & khác biệt chính**
- **GET /api/v1/incidents** (list) :
- Mục đích: trả về danh sách rút gọn (table/list view).
- Trường liên quan (quan trọng cho FE):
- `id`, `title`, `code`, `type_name`, `room_name`, `apartment_name`, `building_name`
- `reporter_name` (chuỗi), `technician_name` (chuỗi)
- `technician_phone` (string|null)
- `technician_email` (string|null)
- `priority`, `status`, `deadline`, `created_at`, `updated_at`
- Lưu ý: **Không có** trường `description` trong kết quả của endpoint này.
- Ví dụ (tóm tắt một item):
```json
{
"id": "...",
"title": "Bóng đèn hỏng",
"code": "INC-0001",
"technician_name": "Nguyễn Văn A",
"technician_phone": "0912345678",
"technician_email": "a@example.com",
"reporter_name": "Trần Thị B",
"priority": "high",
"status": "open"
}
```
- **GET /api/v1/incidents/:id** (detail) :
- Mục đích: trả về chi tiết đầy đủ của một incident.
- Trả về `description` (mô tả chi tiết) — đây là điểm khác biệt chính so với `GET /api/v1/incidents`.
- `technician` là đối tượng lồng (object) với cấu trúc:
- `technician.id`, `technician.name`, `technician.phone`, `technician.email` (tất cả có thể là `null` nếu không có user liên kết).
- `reporter` cũng trả về như một object: `id`, `name`, `phone`, `email`.
- Ví dụ (tóm tắt):
```json
{
"id": "...",
"title": "Bóng đèn hỏng",
"code": "INC-0001",
"description": "Đèn hành lang tầng 3 cháy",
"technician": {
"id": "tech-uuid",
"name": "Nguyễn Văn A",
"phone": "0912345678",
"email": "a@example.com"
},
"reporter": {
"id": "user-uuid",
"name": "Trần Thị B",
"phone": "0987654321",
"email": "b@example.com"
}
}
```
**Nguồn dữ liệu & mapping**
- Technician contact data hiện lấy từ bảng `users` thông qua quan hệ `technician.user_id`.
- Mapping FE cần cập nhật:
- List view: dùng `row.technician_name`, `row.technician_phone`, `row.technician_email`.
- Detail view: dùng `response.technician.name`, `response.technician.phone`, `response.technician.email`.
- Nếu giá trị là `null`, FE nên hiển thị fallback (ví dụ: `-` hoặc `Chưa có`).
**Tương thích ngược / Ghi chú triển khai**
- Trước đây một số code backend trả `technician.full_name`, `technician.phone`, `technician.email` trực tiếp từ bảng `technicians`. Hiện trường `full_name/phone/email` đã bị xóa khỏi bảng `technicians`.
- Đã giữ tên trường `technician_name` cho list (kết quả xây bằng CONCAT của `user.first_name` + `user.last_name`) để giảm tác động tới FE. Tuy nhiên:
- List: `technician_name` (chuỗi), `technician_phone`, `technician_email` (hai trường bổ sung mới).
- Detail: `technician` là object, **có cấu trúc khác** so với list — FE cần đọc theo object.
**Tham chiếu mã nguồn**
- Logic lấy dữ liệu và tên/trường mới ở: [src/providers/IncidentProvider.ts](src/providers/IncidentProvider.ts)
- Route list tạo ở: [src/controllers/api/v1/incidents/index.ts](src/controllers/api/v1/incidents/index.ts)
Nếu FE cần mình viết helper mapping (ví dụ: `normalizeIncidentForList` / `normalizeIncidentForDetail`) hoặc muốn ví dụ TypeScript interfaces, mình có thể thêm nhanh. Nếu có chỗ FE đang bị lỗi hiển thị, gửi ví dụ response hiện tại, mình sẽ kiểm tra.
This diff is collapsed.
This diff is collapsed.
# Posts API — Luồng chi tiết cho Frontend
Tài liệu này mô tả chi tiết các endpoint liên quan đến module **Posts** dành cho team Frontend: đường dẫn, quy tắc hiển thị theo vai trò, body/response mẫu, query params (pagination / filters) và các lưu ý khi triển khai UI.
> Tham khảo schema & đường dẫn backend: [src/contracts/post/schema.ts](src/contracts/post/schema.ts), [src/contracts/post/paths.ts](src/contracts/post/paths.ts)
---
## Tổng quan endpoints
- **GET** `/api/v1/posts` — Lấy danh sách (role-filtered).
- Mô tả: trả về danh sách phân trang; server sẽ lọc theo quyền/địa điểm người dùng.
- Quyền: token Bearer (Authorization) được sử dụng để xác định role; nếu không có token server sẽ xử lý như unauthenticated (xem phần Luồng truy cập).
- **POST** `/api/v1/posts` — Tạo bài viết (Admin only).
- **GET** `/api/v1/posts/{id}` — Lấy chi tiết một bài.
- **PUT** `/api/v1/posts/{id}` — Cập nhật bài (Admin only).
- **DELETE** `/api/v1/posts/{id}` — Soft-delete bài (Admin only).
---
## Luồng truy cập (visibility rules)
- Role **admin** / **system_admin**: có thể xem bài ở mọi `status` (draft, published, archived). Khi frontend dùng token của admin, server sẽ không ép filter status = 'published'.
- Người dùng bình thường (resident): chỉ thấy bài `status = 'published'` (mặc định) nhưng được lọc thêm theo chuỗi vị trí: user → resident → resident_stays → bed → room → apartment → building. Server sẽ trả các bài: `target_type = 'all'` hoặc các bài target trùng với building/apartment/room của resident.
- Unauthenticated (không có token): server sẽ trả **chỉ** các bài `target_type = 'all'` và (mặc định) `status = 'published'`.
Ghi chú đặc biệt:
- `target_type = 'floor'` được xử lý như `apartment` ở backend (xem provider logic).
- Để admin hiển thị draft/archived, FE chỉ cần gửi Authorization token của admin; không cần thêm parameter đặc biệt.
---
## Query params (pagination / filtering)
Các query param chung (theo `QueryParamsSchema`):
- `page` (number, default 1)
- `pageSize` (number, default 10, max 100)
- `sortField` (string)
- `sortOrder` (`asc` | `desc`)
- `filters` (string) — filter theo syntax của `sequelize-api-paginate`.
Ví dụ filter:
- `filters=status==published`
- `filters=status==draft` (admin only)
- `filters=created_at>=2024-01-01T00:00:00Z,created_at<=2024-12-31T23:59:59Z`
Ví dụ request list (published, trang 1, 20 item, sắp xếp theo published_at desc):
```bash
curl -X GET "https://api.example.com/api/v1/posts?filters=status==published&page=1&pageSize=20&sortField=published_at&sortOrder=desc" \
-H "Authorization: Bearer $TOKEN"
```
Lưu ý: FE thường không cần (và không nên) tự filter theo target_ids cho resident — backend sẽ trả các bài phù hợp với user dựa trên token.
---
## Body — Create / Update
Request create (`POST /api/v1/posts`) — tất cả trường theo `PostCreateBodySchema` (example):
```json
{
"title": "Thông báo bảo trì hệ thống",
"content": "Hệ thống sẽ bảo trì vào 22:00 - 23:00",
"targetType": "all",
"targetIds": [],
"isFeatured": false,
"status": "published",
"publishedAt": "2026-05-21T22:00:00Z"
}
```
Rules quan trọng:
- Nếu `targetType !== 'all'` thì `targetIds` phải khác rỗng (validated server-side).
- `status` có thể là: `draft`, `published`, `archived`.
Update (`PUT /api/v1/posts/{id}`): các field là optional; `content` có thể null.
---
## Response format (common)
All responses dùng envelope theo `ApiResponseSchema`:
```json
{
"success": true,
"data": { /* payload */ },
"trace_id": null,
"timestamp": "2026-05-21T10:00:00Z",
"errors": null
}
```
- List (`GET /api/v1/posts`) -> `data` shape:
```json
{
"count": 123,
"rows": [ /* PostListItem */ ],
"page": 1,
"pageSize": 20
}
```
- Post list item fields (`PostListItem`): `id`, `title`, `target_type`, `status`, `is_featured`, `author_id`, `author_name`, `published_at`, `created_at`, `updated_at`.
- Detail (`GET /api/v1/posts/{id}`) -> `PostDetail`: includes `content`, `target_ids` (array), `author` (object with `id`, `name`, `email`).
Ví dụ detail response `data`:
```json
{
"id":"...",
"title":"...",
"content":"...",
"target_type":"building",
"target_ids":["uuid-1","uuid-2"],
"status":"published",
"is_featured":false,
"author_id":"...",
"author":{"id":"...","name":"Nguyễn A","email":"a@example.com"},
"published_at":"2026-05-21T...",
"created_at":"...",
"updated_at":"..."
}
```
---
## Các luồng UI (Frontend)
1. Feed công khai (visitor):
- Gọi `GET /api/v1/posts` **không có token** → backend trả `target_type == 'all'` & `status == 'published'`.
- Hiển thị title, excerpt, author_name, published_at.
2. Resident logged-in (thường dùng app resident):
- Gọi `GET /api/v1/posts` với `Authorization` header.
- Backend sẽ tự resolve resident → room/apartment/building và trả những bài target phù hợp (kết hợp `post_targets`), chỉ `status = 'published'`.
3. Admin panel (create/update/delete/manage):
- Gọi `GET /api/v1/posts` với admin token → backend không ép status = 'published' nên admin có thể thấy draft/archived.
- Tạo bài: `POST /api/v1/posts` (admin only). FE form: chọn `targetType` và khi chọn `'building'|'apartment'|'floor'|'room'` thì show multi-select để pick `targetIds`.
- Update: `PUT /api/v1/posts/{id}`. Nếu thay đổi `targetType` từ 'all' → non-all cần cập nhật `targetIds`.
- Delete: `DELETE /api/v1/posts/{id}` sẽ soft-delete (FE có thể gọi và sau đó refresh list).
---
## Ví dụ cURL
- Lấy danh sách (visitor)
```bash
curl "https://api.example.com/api/v1/posts?page=1&pageSize=10"
```
- Lấy danh sách (admin xem draft)
```bash
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.example.com/api/v1/posts?filters=status==draft&page=1&pageSize=20"
```
- Tạo bài (admin)
```bash
curl -X POST "https://api.example.com/api/v1/posts" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Tít test","targetType":"all","targetIds":[],"status":"draft"}'
```
---
## Lưu ý kỹ thuật / Implementation tips
- Backend lưu `post_targets` trong bảng `post_targets` (mapping). Khi tạo/update, server thực hiện trong transaction nên FE không cần retry đặc biệt.
- Soft-delete: `DELETE /api/v1/posts/{id}` chỉ set `deleted_at` + `deleted_by`. Để khôi phục không có endpoint công khai — phải check bên backend nếu cần.
- Khi cần lọc theo `target_id` ở client (nếu có use-case đặc biệt), trao đổi với backend để hỗ trợ query phù hợp vì việc join `post_targets` đã được backend xử lý trong visibility logic.
---
Nếu muốn, mình có thể mở rộng phần này bằng các ví dụ React/Redux (service calls + typings), hoặc tạo Postman collection / Swagger UI snippet để FE import trực tiếp. Muốn mình làm phần nào tiếp theo?
# Post — Thumbnail (`thumbnail_id`)
Tổng quan
-------
Thêm cột `thumbnail_id` (UUID) vào bảng `posts` để tham chiếu tới bản ghi trong bảng `files` dùng làm thumbnail / ảnh đại diện cho bài viết.
API (FE hướng dẫn)
------------------
1) Create post (POST /api/v1/posts)
- Request body: thêm trường `thumbnailId` (UUID | null)
Ví dụ:
```json
{
"title": "posttest",
"content": "posttest",
"thumbnailId": "af62cea2-e329-45f3-bf55-b07eb2ee5618",
"targetType": "all",
"targetIds": [],
"isFeatured": false,
"status": "published"
}
```
2) Update post (PUT /api/v1/posts/{id})
- Gửi `thumbnailId` để thay đổi, hoặc `null` để xóa thumbnail.
Ví dụ (clear thumbnail):
```json
{ "thumbnailId": null }
```
3) Responses
- List (`GET /api/v1/posts`): mỗi item có `thumbnail_id` (UUID|null).
- Detail (`GET /api/v1/posts/{id}`): trả `thumbnail_id` và một object `thumbnail` (nếu file tồn tại, chưa bị xóa) với metadata theo `File` schema.
Ví dụ snippet (detail):
```json
{
"id": "...",
"title": "...",
"thumbnail_id": "af62cea2-e329-45f3-bf55-b07eb2ee5618",
"thumbnail": {
"id": "af62cea2-e329-45f3-bf55-b07eb2ee5618",
"path": "uploads/2026/05/abc.webp",
"name": "abc.jpg",
"mime": "image/jpeg",
"type": "image",
"size": 12345,
"compress_info": null,
"title": "...",
"description": null,
"note": null,
"is_library": false,
"created_at": "2026-05-29T...Z",
"updated_at": null,
"created_by": "...",
"updated_by": null
}
}
```
Lấy URL hiển thị (Presigned URL)
---------------------------------
Ghi chú quan trọng: trường `thumbnail` chỉ chứa metadata của file — *không* trả presigned URL trực tiếp trong object `thumbnail`.
Để hiển thị ảnh, FE cần lấy link truy cập:
- Single link: `GET /api/v1/files/{id}/link?mode=view&expires=3600` → trả link tạm thời.
- Batch link (khuyến nghị cho list): `POST /api/v1/files/batch-link` với body `{ "file_ids": ["<id1>","<id2>"] , "mode": "view" }` để lấy nhiều link trong 1 request.
Gợi ý triển khai FE
-------------------
- Khi render trang danh sách bài (có nhiều post), thu thập tất cả `thumbnail_id` không-null, gọi `POST /api/v1/files/batch-link` một lần để lấy URL cho các thumbnail, sau đó map `file_id``url` và gán `thumbnail_url` cho từng post.
- Khi render chi tiết bài, nếu `thumbnail` tồn tại có thể gọi `GET /api/v1/files/{id}/link` để lấy URL.
- Nếu `thumbnail_id``null` hoặc backend trả `thumbnail: null`, hiển thị placeholder (ví dụ: ảnh mặc định hoặc biểu tượng).
- Lưu cache URL ngắn hạn theo `expires_in` trả về; refresh khi hết hạn.
Edge cases / Behaviour
----------------------
- Nếu file bị xóa (soft-delete) hoặc không tồn tại, `thumbnail` sẽ không được trả (backend dùng `where: { deleted_at: null }`).
- Migration đặt `ON DELETE SET NULL` nên nếu file bị xóa cứng thumbnail_id sẽ tự động trở thành NULL.
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Hướng dẫn FE — Import / Export Residents
**Tổng quan**
- Mục tiêu: FE cần hỗ trợ tải file mẫu, upload file Excel để import nhiều resident, và export dữ liệu residents hiện tại ra Excel.
**Endpoints**
- `GET /api/v1/residents/import/template` : Tải file mẫu Excel (`.xlsx`).
- Auth: Bearer token.
- Response: Excel file (Vietnamese headers, column width 25, tất cả ô là text).
- `POST /api/v1/residents/import` : Upload file Excel để import.
- Auth: Bearer token.
- Content-Type: `multipart/form-data` với field `file` chứa file `.xlsx`.
- Response codes:
- `200 OK` — tất cả rows import thành công; body là kết quả thành công (mảng resident tạo ra).
- `207 Multi-Status` — một số row thành công, một số row lỗi. Body: `{ success: true, data: { successful: [...], failed: [{ row, error }, ...] } }`.
- `422 Unprocessable Entity` — tất cả rows lỗi; body: `{ success: false, errors: [...] }`.
- `GET /api/v1/residents/export` : Xuất Excel từ dữ liệu residents theo cùng query params với `GET /api/v1/residents`.
- Auth: Bearer token.
- Query params: giống `GET /api/v1/residents` (filters, sortField, sortOrder, page, pageSize). Nếu muốn xuất toàn bộ, set `pageSize` lớn (ví dụ `pageSize=10000`).
- Response: attachment Excel file (`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`). File dùng tiêu đề tiếng Việt, width 25, và tất cả ô là text; header được highlight.
**Import — chi tiết trường (header mapping)**
- FE có thể dùng file mẫu từ endpoint `GET /api/v1/residents/import/template` để đảm bảo định dạng. Import chấp nhận cả tiêu đề tiếng Việt và English (ví dụ `Họ và tên` hoặc `full_name`).
- Mapping (Tiếng Việt → trường nội bộ):
- `Họ và tên``full_name` (recommended)
- `Email``email`
- `Số điện thoại``phone`
- `Số CCCD/CMND``id_card`
- `Ngày sinh``date_of_birth` (ISO `YYYY-MM-DD` hoặc Excel date cell)
- `Quốc tịch``nationality`
- `Nghề nghiệp``job`
- `Người liên hệ khẩn cấp``emergency_contact_name`
- `Số điện thoại liên hệ khẩn cấp``emergency_contact_phone`
- `Mối quan hệ với người liên hệ khẩn cấp``emergency_contact_relationship`
- `Trạng thái``status` (tuỳ chọn, mặc định `active`)
- `Ngày vào ở``move_in_at` (ISO string)
- `Mã giường` (hoặc `Mã giường(s)`) → `bed_code` / `bed_codes` (bắt buộc): có thể truyền nhiều mã giường trong một ô.
- Lưu ý về `bed_code(s)`:
- Hỗ trợ nhiều mã cách nhau bằng `,`, `;`, `|`, `/` hoặc newline.
- Import sẽ tìm `beds.code` tương ứng. Nếu một hoặc nhiều mã không tồn tại, row đó sẽ fail với lỗi `BED_NOT_FOUND`.
**File format / template**
- File mẫu trả về từ `GET /api/v1/residents/import/template` có:
- Tiêu đề tiếng Việt như trên.
- Độ rộng cột `wch = 25`.
- Tất cả ô được ép kiểu text (để Excel không tự format số/ngày).
- FE nên cung cấp 1 button `Download template` gọi endpoint mẫu để người dùng tải file đúng định dạng.
**Import — ví dụ upload bằng fetch**
```js
const fd = new FormData();
fd.append('file', fileInput.files[0]);
const res = await fetch('/api/v1/residents/import', {
method: 'POST',
headers: { Authorization: 'Bearer ' + token },
body: fd,
});
if (res.status === 207) {
const json = await res.json();
// json.data.successful and json.data.failed
} else if (res.status === 200) {
const json = await res.json();
// all successful
} else {
const json = await res.json();
// handle errors
}
```
**Export — ví dụ download bằng fetch**
```js
const params = new URLSearchParams({ filters: currentFilters, page: '1', pageSize: '10000' });
const res = await fetch(`/api/v1/residents/export?${params.toString()}`, {
method: 'GET',
headers: { Authorization: 'Bearer ' + token },
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'residents.xlsx';
a.click();
URL.revokeObjectURL(url);
```
**Response handling & UI suggestions**
- Import:
- Nếu `207` (partial), show two lists: `successful` (số lượng) và `failed` (danh sách lỗi theo row). FE có thể cho phép user tải file lỗi (export failed rows) hoặc hiển thị từng `row` + `error` để chỉnh sửa.
- Nếu `422`, show error messages (tập trung vào lỗi validation chung).
- Export:
- Vì export dùng cùng query params như listing, FE nên gửi chính xác `filters/sort/page/pageSize` tương ứng với view hiện tại.
- Để export toàn bộ, set `pageSize` lớn hoặc implement server-side export-all (nếu backend hỗ trợ sau này).
**Edge cases & notes**
- Dates: importer chấp nhận Excel date cells; nhưng tốt nhất FE ghi hướng dẫn ở UI yêu cầu `YYYY-MM-DD` hoặc dùng mẫu.
- Case/whitespace: importer sẽ trim và so sánh mã giường case-insensitive; tuy nhiên nên đảm bảo mã giường hợp lệ trên UI.
- Kích thước file: hiện backend xử lý đồng bộ; nếu người dùng upload file rất lớn, UX nên cảnh báo và có thể giới hạn kích thước phía FE.
- Xác thực: mọi endpoint yêu cầu Bearer token.
**Checklist để tích hợp nhanh (FE)**
- [ ] Thêm button `Download template` gọi `GET /api/v1/residents/import/template`.
- [ ] Thêm UI `Upload` nhận file `.xlsx`, gửi `multipart/form-data` đến `POST /api/v1/residents/import`.
- [ ] Xử lý response `200/207/422` và hiển thị kết quả/ lỗi theo `row`.
- [ ] Xuất file hiện tại bằng `GET /api/v1/residents/export` với cùng query params như listing (đảm bảo `pageSize` phù hợp).
---
Nếu bạn muốn mình tạo thêm một component React sample (upload form + error table) hoặc tệp markdown được lưu trong repo, mình có thể tạo sẵn.
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
-- =============================================================================
-- BeKind Backend — Auth Schema v3
-- SSO VietProDev Backend — Auth Schema v3
-- Tables: users, user_auth, roles, permissions,
-- user_roles, role_permissions, user_sessions, password_reset_tokens, email_verify_tokens,
-- auth_audit_logs, refresh_token_audit_logs
......
-- =============================================================================
-- BeKind Backend — File Schema
-- SSO VietProDev Backend — File Schema
-- Tables: files, file_variants
-- Depends on: 001-auth-schema.sql (users, update_updated_at_column)
-- =============================================================================
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
-- Change billing_period from DATE to VARCHAR(7) to store as yyyy-mm format
ALTER TABLE bills ALTER COLUMN billing_period TYPE VARCHAR(7);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment