# Audit Log Integration Guide for Frontend

## Overview

The Audit Log system tracks all entity changes (CREATE, UPDATE, DELETE) across the system. Frontend
can retrieve change history for any entity to display to users.

## API Endpoint

### Get Entity Change History

**Endpoint:** `GET /api/v1/audit-logs/entity-change-history`

**Authentication:** Required (Admin role)

**Query Parameters:**

| Parameter     | Type   | Required | Description                                        |
| ------------- | ------ | -------- | -------------------------------------------------- |
| `entity_type` | string | Yes      | Entity type (e.g., "Resident", "User", "Facility") |
| `entity_id`   | string | Yes      | UUID of the entity                                 |
| `page`        | number | No       | Page number (default: 1)                           |
| `pageSize`    | number | No       | Items per page (default: 10)                       |
| `sortField`   | string | No       | Sort field (default: "created_at")                 |
| `sortOrder`   | string | No       | Sort order: "ASC" or "DESC" (default: "DESC")      |

**Example Request:**

```bash
curl -X 'GET' \
  'http://localhost:3001/api/v1/audit-logs/entity-change-history?page=1&pageSize=10&entity_type=Resident&entity_id=015570cd-909a-47ff-852f-63899b640ee5' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer YOUR_TOKEN'
```

## Response Structure

### Success Response (200)

```json
{
  "success": true,
  "trace_id": null,
  "timestamp": "2026-05-14T09:16:41.262Z",
  "errors": null,
  "data": {
    "count": 3,
    "rows": [
      {
        "id": "9a358c36-3b53-4c1d-b23e-f17bb16d24c7",
        "outbox_event_id": "0eafaf97-a044-4e56-8357-89f9df9f0378",
        "request_id": "8a0ebc89-ed39-4765-9645-732af21e614f",
        "trace_id": "8a0ebc89-ed39-4765-9645-732af21e614f",
        "entity_type": "Resident",
        "entity_id": "015570cd-909a-47ff-852f-63899b640ee5",
        "actor_id": "887ecd75-0e15-4039-a1d4-953f298ed620",
        "actor_name": "meu_admin",
        "actor_email": "admin@meu-solutions.com",
        "change_type": "UPDATE",
        "payload": {
          "meta": {
            "source": "api",
            "version": 1,
            "timestamp": "2026-05-14T04:06:40.087Z"
          },
          "before": { ... },
          "after": { ... },
          "changed_fields": ["user_id", "full_name"],
          "context": {}
        },
        "payload_size": 1903,
        "reason": null,
        "event_version": 1,
        "created_at": "2026-05-14T04:06:40.087Z"
      }
    ],
    "page": 1,
    "pageSize": 10
  }
}
```

### Response Fields

| Field                    | Type     | Description                                     |
| ------------------------ | -------- | ----------------------------------------------- | ----------------------------------------------------------- |
| `id`                     | string   | Unique audit log ID                             |
| `entity_type`            | string   | Type of entity that was changed                 |
| `entity_id`              | string   | UUID of the entity                              |
| `actor_id`               | string   | ID of user who made the change                  |
| `actor_name`             | string   | Username of the actor                           |
| `actor_email`            | string   | Email of the actor                              |
| `change_type`            | string   | Type of change: "CREATE", "UPDATE", "DELETE"    |
| `payload.before`         | object   | State of entity before change (null for CREATE) |
| `payload.after`          | object   | State of entity after change (null for DELETE)  |
| `payload.changed_fields` | string[] | List of field names that changed                |
| `reason`                 | string   | null                                            | Optional custom reason (currently null, FE should generate) |
| `created_at`             | string   | ISO timestamp when change occurred              |

## Frontend Implementation

### 1. TypeScript Interfaces

```typescript
// types/audit.ts
export interface AuditLogEntry {
	id: string;
	entity_type: string;
	entity_id: string;
	actor_id: string;
	actor_name: string;
	actor_email: string;
	change_type: 'CREATE' | 'UPDATE' | 'DELETE';
	reason: string | null;
	created_at: string;
	payload: {
		meta: {
			source: string;
			version: number;
			timestamp: string;
		};
		before: any;
		after: any;
		changed_fields: string[];
		context: any;
	};
	payload_size: number;
	event_version: number;
}

export interface AuditLogResponse {
	success: boolean;
	data: {
		count: number;
		rows: AuditLogEntry[];
		page: number;
		pageSize: number;
	};
}

export interface DisplayAuditLog {
	id: string;
	title: string;
	actor: string;
	timestamp: string;
	description: string;
	changeType: string;
}
```

### 2. API Service

```typescript
// services/auditService.ts
import { AuditLogResponse } from '@/types/audit';

export async function getEntityChangeHistory(
	entityType: string,
	entityId: string,
	page = 1,
	pageSize = 10,
): Promise<AuditLogResponse> {
	const response = await fetch(
		`/api/v1/audit-logs/entity-change-history?entity_type=${entityType}&entity_id=${entityId}&page=${page}&pageSize=${pageSize}`,
		{
			headers: {
				accept: 'application/json',
				Authorization: `Bearer ${localStorage.getItem('token')}`,
			},
		},
	);

	if (!response.ok) {
		throw new Error('Failed to fetch audit history');
	}

	return response.json();
}
```

### 3. Audit Log Transformer

The backend returns raw data (English field names). Frontend should generate human-readable
descriptions in the user's language.

```typescript
// utils/auditTransformer.ts
import { AuditLogEntry, DisplayAuditLog } from '@/types/audit';

// Field label mapping for Vietnamese - entity-specific
const getFieldLabel = (field: string, entityType: string): string => {
	const commonLabels: Record<string, string> = {
		full_name: 'Họ tên',
		name: 'Tên',
		email: 'Email',
		phone: 'Số điện thoại',
		id_card: 'CMND/CCCD',
		date_of_birth: 'Ngày sinh',
		nationality: 'Quốc tịch',
		status: 'Trạng thái',
		user_id: 'Người dùng',
		created_at: 'Ngày tạo',
		updated_at: 'Ngày cập nhật',
	};

	const residentLabels: Record<string, string> = {
		job: 'Nghề nghiệp',
		bed_ids: 'Giường',
		move_in_at: 'Ngày chuyển vào',
		emergency_contact_name: 'Người liên hệ khẩn cấp',
		emergency_contact_phone: 'SĐT người liên hệ',
		emergency_contact_relationship: 'Quan hệ người liên hệ',
	};

	const userLabels: Record<string, string> = {
		username: 'Tên đăng nhập',
		role: 'Vai trò',
		first_name: 'Tên',
		last_name: 'Họ',
	};

	const labels: Record<string, string> = {
		...commonLabels,
		...(entityType === 'Resident' ? residentLabels : {}),
		...(entityType === 'User' ? userLabels : {}),
	};

	return labels[field] || field;
};

const changeTypeLabels: Record<string, string> = {
	CREATE: 'Tạo hồ sơ',
	UPDATE: 'Cập nhật',
	DELETE: 'Xóa',
};

const generateDescription = (entry: AuditLogEntry): string => {
	const { change_type, entity_type, payload } = entry;
	const changedFields = payload.changed_fields || [];
	const after = payload.after;

	const entityTranslations: Record<string, string> = {
		Resident: 'cư dân',
		User: 'người dùng',
		Facility: 'cơ sở vật chất',
	};
	const translatedEntity = entityTranslations[entity_type] || entity_type.toLowerCase();

	switch (change_type) {
		case 'CREATE': {
			const entityName = after?.full_name || after?.name || after?.email || '';
			return entityName ? `Tạo ${translatedEntity}: ${entityName}` : `Tạo ${translatedEntity} mới`;
		}

		case 'UPDATE': {
			if (changedFields.length === 0) {
				return `Cập nhật ${translatedEntity}`;
			}
			if (changedFields.length === 1) {
				return `Cập nhật ${getFieldLabel(changedFields[0], entity_type)}`;
			}
			if (changedFields.length <= 3) {
				const translatedFields = changedFields.map((f) => getFieldLabel(f, entity_type));
				return `Cập nhật các trường: ${translatedFields.join(', ')}`;
			}
			const translatedFields = changedFields.slice(0, 3).map((f) => getFieldLabel(f, entity_type));
			return `Cập nhật ${changedFields.length} trường: ${translatedFields.join(', ')}...`;
		}

		case 'DELETE': {
			const deletedName = after?.full_name || after?.name || after?.email || '';
			return deletedName ? `Xóa ${translatedEntity}: ${deletedName}` : `Xóa ${translatedEntity}`;
		}

		default:
			return `${change_type} ${translatedEntity}`;
	}
};

export function transformAuditLog(entry: AuditLogEntry): DisplayAuditLog {
	const formatDate = (isoString: string) => {
		const date = new Date(isoString);
		return date.toLocaleString('vi-VN', {
			day: '2-digit',
			month: '2-digit',
			year: 'numeric',
			hour: '2-digit',
			minute: '2-digit',
		});
	};

	return {
		id: entry.id,
		title: changeTypeLabels[entry.change_type] || entry.change_type,
		actor: entry.actor_name,
		timestamp: formatDate(entry.created_at),
		description: generateDescription(entry),
		changeType: entry.change_type,
	};
}
```

### 4. React Component Example

```typescript
// components/EntityChangeHistory.tsx
'use client';

import { useEffect, useState } from 'react';
import { getEntityChangeHistory } from '@/services/auditService';
import { transformAuditLog, DisplayAuditLog } from '@/utils/auditTransformer';

interface EntityChangeHistoryProps {
  entityType: string;
  entityId: string;
}

export default function EntityChangeHistory({ entityType, entityId }: EntityChangeHistoryProps) {
  const [logs, setLogs] = useState<DisplayAuditLog[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchLogs() {
      try {
        setLoading(true);
        const response = await getEntityChangeHistory(entityType, entityId);
        const transformedLogs = response.data.rows.map(transformAuditLog);
        setLogs(transformedLogs);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to load history');
      } finally {
        setLoading(false);
      }
    }

    fetchLogs();
  }, [entityType, entityId]);

  if (loading) return <div>Đang tải...</div>;
  if (error) return <div>Lỗi: {error}</div>;

  return (
    <div className="space-y-4">
      <h3 className="text-lg font-semibold">Lịch sử thay đổi hồ sơ</h3>

      {logs.length === 0 ? (
        <p className="text-gray-500">Chưa có lịch sử thay đổi</p>
      ) : (
        <div className="space-y-3">
          {logs.map((log) => (
            <div
              key={log.id}
              className="border rounded-lg p-4 bg-white shadow-sm"
            >
              <div className="flex items-start justify-between">
                <div className="flex-1">
                  <div className="flex items-center gap-2 mb-1">
                    <span className="font-medium text-blue-600">{log.title}</span>
                    <span className="text-gray-400">·</span>
                    <span className="text-sm text-gray-600">{log.actor}</span>
                  </div>
                  <p className="text-sm text-gray-700">{log.description}</p>
                </div>
                <div className="text-sm text-gray-500 whitespace-nowrap ml-4">
                  {log.timestamp}
                </div>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
```

### 5. Usage Example

```typescript
// pages/resident/[id].tsx
import EntityChangeHistory from '@/components/EntityChangeHistory';

export default function ResidentDetailPage({ params }: { params: { id: string } }) {
  return (
    <div>
      {/* Resident details */}

      <EntityChangeHistory
        entityType="Resident"
        entityId={params.id}
      />
    </div>
  );
}
```

## Architecture Notes

### Why Frontend Generates Descriptions?

The backend returns raw data (field names in English) instead of pre-formatted descriptions because:

1. **Internationalization (i18n)**: Different users may need descriptions in different languages
   (Vietnamese, English, etc.)
2. **Entity-specific**: Different entities have different fields - frontend can provide
   context-specific labels
3. **Flexibility**: Frontend can change display format without backend deployment
4. **Single source of truth**: Localization logic stays in the UI layer where it belongs

### Data Flow

```
Backend (raw data) → API → Frontend (transform & localize) → Display
```

## Common Entity Types

| Entity Type | Vietnamese Translation |
| ----------- | ---------------------- |
| Resident    | cư dân                 |
| User        | người dùng             |
| Facility    | cơ sở vật chất         |
| Room        | phòng                  |
| Bed         | giường                 |

## Best Practices

1. **Cache audit logs**: History doesn't change frequently, consider caching
2. **Lazy load**: Load audit logs only when user opens the history section
3. **Pagination**: Use pagination for entities with many changes
4. **Error handling**: Gracefully handle cases where audit history is unavailable
5. **Loading states**: Show loading indicators while fetching history
6. **Empty states**: Display appropriate message when no history exists

## Security

- This endpoint requires **Admin role** authentication
- Sensitive fields (like `id_card`) are masked in the payload as `***MASKED***`
- Always include the Authorization header with a valid token

## Troubleshooting

### No audit logs returned

- Verify the entity has been modified after audit logging was implemented
- Check if the user has Admin role
- Ensure the `entity_type` and `entity_id` are correct

### Field labels not translated

- Add the field to the appropriate label mapping in `getFieldLabel()`
- Ensure the `entityType` matches the case in the mapping

### Timestamp format issues

- Use the `formatDate()` helper function or customize for your locale
- Ensure timezone handling matches your requirements
