From 043edff93a234092b538bdd0d17f849f114b0926 Mon Sep 17 00:00:00 2001 From: phaichayon Date: Sun, 26 Apr 2026 00:15:22 +0700 Subject: [PATCH] setup --- API_DOCUMENTATION.md | 561 ++- docs/API_REFERENCE.md | 1178 ++++++ KEYCLOAK_AUTH.md => docs/KEYCLOAK_AUTH.md | 0 docs/KEYCLOAK_ENV.md | 205 + docs/MODULES_SUMMARY.md | 334 ++ docs/PROJECT_SUMMARY.md | 428 +++ docs/api-documentation-summary.md | 497 +++ docs/checklist-phase1-database.md | 230 ++ docs/checklist-phase2-middleware.md | 298 ++ docs/checklist-phase3-keycloak.md | 381 ++ docs/checklist-phase4-services.md | 220 ++ docs/checklist-phase5-controllers.md | 278 ++ docs/checklist-phase6-models.md | 540 +++ docs/checklist-phase7-testing.md | 672 ++++ .../contact-sharing-implementation-summary.md | 443 +++ docs/quotation-checklist.md | 416 ++ drizzle.config.ts | 5 +- drizzle/0000_cultured_dreaming_celestial.sql | 419 ++ drizzle/0001_curvy_sunspot.sql | 54 + drizzle/meta/0000_snapshot.json | 3056 +++++++++++++++ drizzle/meta/0001_snapshot.json | 3370 +++++++++++++++++ drizzle/meta/_journal.json | 20 + .../0001_crm_refactor_uuid_multi_branch.sql | 590 +++ package-lock.json | 152 + package.json | 7 +- src/app/api/[[...slugs]]/route.ts | 9 + src/database/schema/audit-log.ts | 55 + src/database/schema/branches.ts | 31 + src/database/schema/contact-shares.ts | 44 + src/database/schema/customers.ts | 121 + src/database/schema/documents-sequences.ts | 40 + src/database/schema/index.ts | 9 + src/database/schema/industrialEstate.ts | 49 + src/database/schema/location.ts | 40 + src/database/schema/master-options.ts | 43 + src/database/schema/quotation-contacts.ts | 39 + src/database/schema/quotations.ts | 434 +++ src/lib/eden-helpers.ts | 492 +++ src/lib/eden.ts | 79 + src/lib/helpers/location-enrichment.ts | 155 + src/lib/helpers/user-enrichment.ts | 258 ++ src/lib/keycloak.ts | 329 +- src/lib/utils/file-upload.ts | 189 + src/middleware/branch.ts | 220 ++ src/modules/audit-logs/controller.ts | 380 ++ src/modules/audit-logs/index.ts | 1 + src/modules/audit-logs/model.ts | 30 + src/modules/audit-logs/service.ts | 256 ++ src/modules/customers/controller.ts | 1094 +++++- src/modules/customers/model.ts | 137 +- src/modules/customers/service.ts | 711 +++- src/modules/industrial-estates/controller.ts | 434 +++ src/modules/industrial-estates/model.ts | 50 + src/modules/industrial-estates/service.ts | 294 ++ src/modules/locations/controller.ts | 511 +++ src/modules/locations/model.ts | 54 + src/modules/locations/service.ts | 355 ++ src/modules/master-options/controller.ts | 503 +++ src/modules/master-options/model.ts | 71 + src/modules/master-options/service.ts | 355 ++ src/modules/quotations/controller.ts | 877 ++++- src/modules/quotations/model.ts | 246 +- src/modules/quotations/service.ts | 1949 +++++++++- src/types/api.ts | 239 ++ 64 files changed, 25076 insertions(+), 461 deletions(-) create mode 100644 docs/API_REFERENCE.md rename KEYCLOAK_AUTH.md => docs/KEYCLOAK_AUTH.md (100%) create mode 100644 docs/KEYCLOAK_ENV.md create mode 100644 docs/MODULES_SUMMARY.md create mode 100644 docs/PROJECT_SUMMARY.md create mode 100644 docs/api-documentation-summary.md create mode 100644 docs/checklist-phase1-database.md create mode 100644 docs/checklist-phase2-middleware.md create mode 100644 docs/checklist-phase3-keycloak.md create mode 100644 docs/checklist-phase4-services.md create mode 100644 docs/checklist-phase5-controllers.md create mode 100644 docs/checklist-phase6-models.md create mode 100644 docs/checklist-phase7-testing.md create mode 100644 docs/contact-sharing-implementation-summary.md create mode 100644 docs/quotation-checklist.md create mode 100644 drizzle/0000_cultured_dreaming_celestial.sql create mode 100644 drizzle/0001_curvy_sunspot.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 drizzle/migrations/0001_crm_refactor_uuid_multi_branch.sql create mode 100644 src/database/schema/audit-log.ts create mode 100644 src/database/schema/branches.ts create mode 100644 src/database/schema/contact-shares.ts create mode 100644 src/database/schema/customers.ts create mode 100644 src/database/schema/documents-sequences.ts create mode 100644 src/database/schema/industrialEstate.ts create mode 100644 src/database/schema/location.ts create mode 100644 src/database/schema/master-options.ts create mode 100644 src/database/schema/quotation-contacts.ts create mode 100644 src/database/schema/quotations.ts create mode 100644 src/lib/eden-helpers.ts create mode 100644 src/lib/eden.ts create mode 100644 src/lib/helpers/location-enrichment.ts create mode 100644 src/lib/helpers/user-enrichment.ts create mode 100644 src/lib/utils/file-upload.ts create mode 100644 src/middleware/branch.ts create mode 100644 src/modules/audit-logs/controller.ts create mode 100644 src/modules/audit-logs/index.ts create mode 100644 src/modules/audit-logs/model.ts create mode 100644 src/modules/audit-logs/service.ts create mode 100644 src/modules/industrial-estates/controller.ts create mode 100644 src/modules/industrial-estates/model.ts create mode 100644 src/modules/industrial-estates/service.ts create mode 100644 src/modules/locations/controller.ts create mode 100644 src/modules/locations/model.ts create mode 100644 src/modules/locations/service.ts create mode 100644 src/modules/master-options/controller.ts create mode 100644 src/modules/master-options/model.ts create mode 100644 src/modules/master-options/service.ts create mode 100644 src/types/api.ts diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 98dd4e7..2f9a6bc 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -7,7 +7,7 @@ This project uses ElysiaJS integrated with Next.js App Router to create high-per ## Base URL ``` -http://localhost:3001 +http://localhost:3000 ``` ## Endpoints @@ -32,19 +32,19 @@ GET /api/customers/:branch 1. Get all customers from branch-01: ```bash -curl http://localhost:3001/api/customers/branch-01 +curl http://localhost:3000/api/customers/branch-01 ``` 2. Get active customers from branch-02: ```bash -curl "http://localhost:3001/api/customers/branch-02?status=active" +curl "http://localhost:3000/api/customers/branch-02?status=active" ``` 3. Get pending customers from head-office: ```bash -curl "http://localhost:3001/api/customers/head-office?status=pending" +curl "http://localhost:3000/api/customers/head-office?status=pending" ``` **Response Format:** @@ -117,13 +117,13 @@ GET /api/quotations/:branch 1. Get all quotations from branch-01: ```bash -curl http://localhost:3001/api/quotations/branch-01 +curl http://localhost:3000/api/quotations/branch-01 ``` 2. Get sent quotations from head-office: ```bash -curl "http://localhost:3001/api/quotations/head-office?status=sent" +curl "http://localhost:3000/api/quotations/head-office?status=sent" ``` **Response Format:** @@ -181,6 +181,422 @@ DELETE /api/quotations/:branch/:id --- +### Master Options API + +#### Get All Master Options + +``` +GET /api/master-options +``` + +**Query Parameters:** + +- `category` (optional): Filter by category (e.g., `customer_type`, `payment_method`, `industry`) +- `isActive` (optional): Filter by active status (`true` or `false`) +- `search` (optional): Search in code, nameTh, or nameEn + +**Example:** + +```bash +curl "http://localhost:3000/api/master-options?category=customer_type&isActive=true" +``` + +**Response Format:** + +```json +{ + "success": true, + "data": [ + { + "id": "opt-001", + "branchId": "branch-01", + "category": "customer_type", + "code": "CORPORATE", + "nameTh": "องค์กร/บริษัท", + "nameEn": "Corporate", + "descriptionTh": "ลูกค้าประเภทองค์กร", + "descriptionEn": "Corporate customers", + "isActive": true, + "createdAt": "2024-01-15T09:00:00Z", + "updatedAt": "2024-01-15T09:00:00Z" + } + ], + "count": 1, + "message": "Found 1 master option(s)" +} +``` + +#### Get Single Master Option + +``` +GET /api/master-options/:id +``` + +#### Create Master Option + +``` +POST /api/master-options +``` + +**Request Body:** + +```json +{ + "category": "customer_type", + "code": "INDIVIDUAL", + "nameTh": "บุคคลธรรมดา", + "nameEn": "Individual", + "descriptionTh": "ลูกค้ารายบุคคล", + "descriptionEn": "Individual customers" +} +``` + +#### Update Master Option + +``` +PUT /api/master-options/:id +``` + +#### Delete Master Option + +``` +DELETE /api/master-options/:id +``` + +#### Toggle Active Status + +``` +PATCH /api/master-options/:id/toggle +``` + +--- + +### Locations API + +#### Get All Locations + +``` +GET /api/locations +``` + +**Query Parameters:** + +- `type` (optional): Filter by location type (`country`, `province`, `district`, `subdistrict`) +- `parentId` (optional): Filter by parent location ID +- `search` (optional): Search in code, nameTh, or nameEn +- `isActive` (optional): Filter by active status + +**Example:** + +```bash +curl "http://localhost:3000/api/locations?type=province&search=กรุงเทพ" +``` + +**Response Format:** + +```json +{ + "success": true, + "data": [ + { + "id": "loc-001", + "branchId": "head-office", + "code": "TH-10", + "nameTh": "กรุงเทพมหานคร", + "nameEn": "Bangkok", + "type": "province", + "parentId": "country-th-id", + "isActive": true, + "createdAt": "2024-01-15T09:00:00Z", + "updatedAt": "2024-01-15T09:00:00Z" + } + ], + "count": 1, + "message": "Found 1 location(s)" +} +``` + +#### Get Locations by Type + +``` +GET /api/locations/type/:type +``` + +**Parameters:** + +- `type` (path parameter): `country`, `province`, `district`, or `subdistrict` + +**Example:** + +```bash +curl http://localhost:3000/api/locations/type/province +``` + +#### Get Single Location + +``` +GET /api/locations/:id +``` + +#### Create Location + +``` +POST /api/locations +``` + +**Request Body:** + +```json +{ + "code": "TH-10", + "nameTh": "กรุงเทพมหานคร", + "nameEn": "Bangkok", + "type": "province", + "parentId": "country-th-id" +} +``` + +#### Update Location + +``` +PUT /api/locations/:id +``` + +#### Delete Location + +``` +DELETE /api/locations/:id +``` + +#### Toggle Active Status + +``` +PATCH /api/locations/:id/toggle +``` + +--- + +### Industrial Estates API + +#### Get All Industrial Estates + +``` +GET /api/industrial-estates +``` + +**Query Parameters:** + +- `locationId` (optional): Filter by location ID +- `isActive` (optional): Filter by active status +- `search` (optional): Search in code, nameTh, or nameEn + +**Example:** + +```bash +curl "http://localhost:3000/api/industrial-estates?locationId=th-10&isActive=true" +``` + +**Response Format:** + +```json +{ + "success": true, + "data": [ + { + "id": "ie-001", + "branchId": "head-office", + "code": "BPL", + "nameTh": "นิคมอุตสาหกรรมบางพลี", + "nameEn": "Bangpoo Industrial Estate", + "locationId": "th-10", + "latitude": 13.5991, + "longitude": 100.7015, + "isActive": true, + "createdAt": "2024-01-15T09:00:00Z", + "updatedAt": "2024-01-15T09:00:00Z" + } + ], + "count": 1, + "message": "Found 1 industrial estate(s)" +} +``` + +#### Get Industrial Estates by Location + +``` +GET /api/industrial-estates/location/:locationId +``` + +**Example:** + +```bash +curl http://localhost:3000/api/industrial-estates/location/th-10 +``` + +#### Get Single Industrial Estate + +``` +GET /api/industrial-estates/:id +``` + +#### Create Industrial Estate + +``` +POST /api/industrial-estates +``` + +**Request Body:** + +```json +{ + "code": "BPL", + "nameTh": "นิคมอุตสาหกรรมบางพลี", + "nameEn": "Bangpoo Industrial Estate", + "locationId": "th-10", + "latitude": 13.5991, + "longitude": 100.7015 +} +``` + +#### Update Industrial Estate + +``` +PUT /api/industrial-estates/:id +``` + +#### Delete Industrial Estate + +``` +DELETE /api/industrial-estates/:id +``` + +#### Toggle Active Status + +``` +PATCH /api/industrial-estates/:id/toggle +``` + +--- + +### Audit Logs API + +**Note:** This API requires Admin/Superadmin/Auditor access level. + +#### Get All Audit Logs + +``` +GET /api/audit-logs +``` + +**Query Parameters:** + +- `startDate` (optional): Filter logs from this date (ISO 8601 format) +- `endDate` (optional): Filter logs until this date (ISO 8601 format) +- `action` (optional): Filter by action type (`CREATE`, `UPDATE`, `DELETE`, `READ`) +- `entityType` (optional): Filter by entity type (`customer`, `quotation`, `location`, etc.) +- `limit` (optional): Number of results to return (default: 50) +- `offset` (optional): Number of results to skip (for pagination) + +**Example:** + +```bash +curl "http://localhost:3000/api/audit-logs?action=CREATE&limit=10" +``` + +**Response Format:** + +```json +{ + "success": true, + "data": [ + { + "id": "audit-001", + "branchId": "branch-01", + "userId": "user-123", + "actorId": "user-123", + "entityType": "customer", + "entityId": "cust-001", + "action": "CREATE", + "actionTh": "สร้าง", + "oldValues": null, + "newValues": { + "name": "สมชาย ใจดี", + "email": "somchai@example.com" + }, + "ipAddress": "192.168.1.100", + "userAgent": "Mozilla/5.0...", + "createdAt": "2024-01-15T09:00:00Z" + } + ], + "count": 1, + "message": "Found 1 audit log(s)" +} +``` + +#### Get Audit Log Statistics + +``` +GET /api/audit-logs/stats +``` + +**Response Format:** + +```json +{ + "success": true, + "data": { + "totalLogs": 1250, + "byAction": { + "CREATE": 350, + "UPDATE": 500, + "DELETE": 150, + "READ": 250 + }, + "byEntityType": { + "customer": 400, + "quotation": 300, + "location": 200, + "industrial_estate": 100, + "master_option": 250 + }, + "todayCount": 45, + "thisWeekCount": 320 + } +} +``` + +#### Get Logs by Entity + +``` +GET /api/audit-logs/entity/:entityType/:entityId +``` + +**Example:** + +```bash +curl http://localhost:3000/api/audit-logs/entity/customer/cust-001 +``` + +#### Get Logs by User + +``` +GET /api/audit-logs/user/:userId +``` + +**Example:** + +```bash +curl http://localhost:3000/api/audit-logs/user/user-123 +``` + +#### Get Single Audit Log + +``` +GET /api/audit-logs/:id +``` + +--- + ## Available Data ### Customers @@ -195,20 +611,62 @@ DELETE /api/quotations/:branch/:id - `branch-02`: 1 quotation (draft) - `head-office`: 1 quotation (sent) +### Master Options + +- Categories: `customer_type`, `payment_method`, `industry`, `lead_source` +- Each category has multiple options with Thai/English names + +### Locations + +- Countries: Thailand, etc. +- Provinces: All Thai provinces +- Districts/Subdistricts: Hierarchical data structure + +### Industrial Estates + +- Multiple industrial estates across Thailand +- Linked to locations with GPS coordinates + +### Audit Logs + +- Complete audit trail for all operations +- Admin-only access + ## Testing with Browser Simply open these URLs in your browser: ### Customers -- http://localhost:3001/api/customers/branch-01 -- http://localhost:3001/api/customers/branch-02?status=active -- http://localhost:3001/api/customers/head-office +- http://localhost:3000/api/customers/branch-01 +- http://localhost:3000/api/customers/branch-02?status=active +- http://localhost:3000/api/customers/head-office ### Quotations -- http://localhost:3001/api/quotations/branch-01 -- http://localhost:3001/api/quotations/head-office?status=sent +- http://localhost:3000/api/quotations/branch-01 +- http://localhost:3000/api/quotations/head-office?status=sent + +### Master Options + +- http://localhost:3000/api/master-options +- http://localhost:3000/api/master-options?category=customer_type + +### Locations + +- http://localhost:3000/api/locations +- http://localhost:3000/api/locations/type/province +- http://localhost:3000/api/locations?search=กรุงเทพ + +### Industrial Estates + +- http://localhost:3000/api/industrial-estates +- http://localhost:3000/api/industrial-estates?isActive=true + +### Audit Logs (Admin only) + +- http://localhost:3000/api/audit-logs +- http://localhost:3000/api/audit-logs/stats ## Project Structure @@ -225,7 +683,23 @@ src/ │ │ ├── controller.ts # HTTP handlers & routing │ │ ├── service.ts # Business logic │ │ └── model.ts # Schemas & validation -│ └── quotations/ +│ ├── quotations/ +│ │ ├── controller.ts # HTTP handlers & routing +│ │ ├── service.ts # Business logic +│ │ └── model.ts # Schemas & validation +│ ├── master-options/ +│ │ ├── controller.ts # HTTP handlers & routing +│ │ ├── service.ts # Business logic +│ │ └── model.ts # Schemas & validation +│ ├── locations/ +│ │ ├── controller.ts # HTTP handlers & routing +│ │ ├── service.ts # Business logic +│ │ └── model.ts # Schemas & validation +│ ├── industrial-estates/ +│ │ ├── controller.ts # HTTP handlers & routing +│ │ ├── service.ts # Business logic +│ │ └── model.ts # Schemas & validation +│ └── audit-logs/ │ ├── controller.ts # HTTP handlers & routing │ ├── service.ts # Business logic │ └── model.ts # Schemas & validation @@ -233,6 +707,8 @@ src/ │ └── customer.ts # Shared types ├── lib/ │ └── mock-data.ts # Mock data +└── database/ + └── schema.ts # Drizzle ORM schema ``` ### File Responsibilities @@ -275,6 +751,10 @@ This project follows the **correct ElysiaJS + Next.js integration pattern**: - ✅ WinterCG compliant - works as normal Next.js API route - ✅ Feature-based MVC pattern for maintainability - ✅ Clear separation of concerns between Model, View, and Controller +- ✅ Branch-level data scoping for multi-tenant architecture +- ✅ Audit logging for all operations +- ✅ Soft delete with `deletedAt` field +- ✅ Multi-language support (Thai/English) ## Technologies Used @@ -282,19 +762,28 @@ This project follows the **correct ElysiaJS + Next.js integration pattern**: - **Next.js 16**: React framework with App Router - **TypeScript**: Type safety throughout - **TypeBox**: Schema validation (via `@elysiajs/schema`) +- **Drizzle ORM**: Type-safe SQL ORM +- **PostgreSQL**: Primary database ## Features -✅ Feature-based MVC architecture -✅ Dynamic branch parameter support -✅ Type-safe request/response validation -✅ Optional query parameter filtering -✅ Mock data for customers and quotations -✅ Full TypeScript support -✅ Auto-generated API documentation (Swagger/OpenAPI ready) -✅ Correct ElysiaJS + Next.js integration pattern -✅ Scalable and maintainable code structure +✅ Feature-based MVC architecture +✅ Dynamic branch parameter support +✅ Type-safe request/response validation +✅ Optional query parameter filtering +✅ Mock data for customers and quotations +✅ Full TypeScript support +✅ Auto-generated API documentation (Swagger/OpenAPI ready) +✅ Correct ElysiaJS + Next.js integration pattern +✅ Scalable and maintainable code structure ✅ Clear separation of concerns +✅ Multi-tenant architecture with branch scoping +✅ Complete audit logging system +✅ Soft delete for data integrity +✅ Multi-language support (Thai/English) +✅ Hierarchical data structures (locations) +✅ GPS coordinate support (industrial estates) +✅ Admin-only access control (audit logs) ## Adding New Modules @@ -304,7 +793,8 @@ To add a new module (e.g., `products`): 2. Create `model.ts` - Define schemas 3. Create `service.ts` - Business logic 4. Create `controller.ts` - Routes and handlers -5. Update `src/app/api/[[...slugs]]/route.ts`: +5. Create `index.ts` - Module exports +6. Update `src/app/api/[[...slugs]]/route.ts`: ```typescript import { products } from "@/modules/products/controller"; @@ -312,5 +802,32 @@ To add a new module (e.g., `products`): const app = new Elysia({ prefix: "/api" }) .use(customers) .use(quotations) + .use(masterOptions) + .use(locations) + .use(industrialEstates) + .use(auditLogs) .use(products); // Add new module ``` + +## Security & Access Control + +### Branch Middleware + +All routes use `branchMiddleware` which injects: + +- `currentBranchId` - Current user's branch +- `userId` - Current user ID +- `userGroups` - User groups/roles +- `accessibleBranches` - Branches user can access + +### Permission Levels + +- **Standard Users**: Access to branch-scoped data +- **Admin/Superadmin**: Full access + audit logs +- **Auditor**: Read-only access to audit logs + +### Data Isolation + +- All queries are automatically filtered by `branchId` +- Cross-branch access is prevented +- Soft delete ensures data integrity diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md new file mode 100644 index 0000000..76b2ee1 --- /dev/null +++ b/docs/API_REFERENCE.md @@ -0,0 +1,1178 @@ +# API Reference for Front-end Developers + +**Last Updated:** 2026-04-25 +**Version:** 1.0.0 + +--- + +## 📋 Table of Contents + +1. [Overview](#overview) +2. [Authentication](#authentication) +3. [Base URL](#base-url) +4. [Response Format](#response-format) +5. [Error Handling](#error-handling) +6. [Customers API](#customers-api) +7. [Contacts API](#contacts-api) +8. [Contact Sharing API](#contact-sharing-api) +9. [Type Definitions](#type-definitions) +10. [Usage Examples](#usage-examples) + +--- + +## Overview + +This API provides type-safe access to the CRM system using ElysiaJS. All endpoints return consistent responses with built-in error handling. + +**Key Features:** + +- ✅ Type-safe with TypeScript +- ✅ Bearer token authentication via Keycloak +- ✅ Consistent response format +- ✅ Automatic error handling +- ✅ Multi-tenant support (branch-scoped) + +--- + +## Authentication + +All API endpoints require authentication via Bearer token from Keycloak. + +### How it works: + +1. Front-end authenticates with Keycloak +2. Token is stored in `window.__KEYCLOAK_TOKEN__` +3. API client automatically adds `Authorization: Bearer {token}` header +4. Token refresh is handled automatically on 401 errors + +### Example: + +```typescript +// Token is automatically added by api-client.ts +import { apiClient } from "@/lib/api-client"; + +const response = await apiClient("/customers"); +// Authorization header is added automatically +``` + +--- + +## Base URL + +``` +Development: http://localhost:3000/api +Production: {TBD} +``` + +--- + +## Response Format + +### Success Response + +```typescript +{ + success: true, + data: T, // The actual data + message?: string, // Optional success message + count?: number // Optional count for list responses +} +``` + +### Error Response + +```typescript +{ + success: false, + error: string, // Error message + details?: string // Optional detailed error info +} +``` + +--- + +## Error Handling + +### HTTP Status Codes + +| Status | Description | +| ------ | ------------------------------------ | +| 200 | Success | +| 400 | Bad Request (invalid input) | +| 401 | Unauthorized (token expired/invalid) | +| 403 | Forbidden (insufficient permissions) | +| 404 | Not Found | +| 500 | Internal Server Error | + +### Error Response Structure + +```typescript +{ + success: false, + error: "Customer not found or access denied", + details: "Customer ID 'xyz' does not exist" +} +``` + +### Handling Errors in Front-end + +```typescript +try { + const response = await apiClient("/customers/123"); + + if (!response.success) { + // Handle error + console.error(response.error); + return; + } + + // Success - use response.data + console.log(response.data); +} catch (error) { + // Network error or unexpected error + console.error("API call failed:", error); +} +``` + +--- + +## Customers API + +### 1. Get All Customers + +Get all customers for the current user's branch. + +**Endpoint:** `GET /customers` + +**Query Parameters:** + +- `status` (optional) - Filter by status: `active`, `inactive`, `pending` + +**Request:** + +```typescript +GET /customers +GET /customers?status=active +``` + +**Response:** + +```typescript +{ + success: true, + data: Customer[], + count: 10, + message: "Found 10 customer(s)" +} +``` + +**Example:** + +```typescript +import { apiClient } from "@/lib/api-client"; +import type { CustomerListResponse } from "@/types/api"; + +const response = await apiClient( + "/customers?status=active", +); +``` + +--- + +### 2. Get Single Customer + +Get a specific customer by ID. + +**Endpoint:** `GET /customers/:id` + +**Path Parameters:** + +- `id` (required) - Customer ID + +**Request:** + +```typescript +GET /customers/123e4567-e89b-12d3-a456-426614174000 +``` + +**Response:** + +```typescript +{ + success: true, + data: Customer +} +``` + +**Example:** + +```typescript +const response = await apiClient(`/customers/${customerId}`); +``` + +--- + +### 3. Create Customer + +Create a new customer. + +**Endpoint:** `POST /customers` + +**Request Body:** + +```typescript +{ + name: string, // Required + email: string, // Required (must be valid email) + phone: string, // Required + company: string, // Required + address: string, // Required + customerStatus?: string, // Optional + customerType?: string, // Optional + taxId?: string // Optional +} +``` + +**Response:** + +```typescript +{ + success: true, + data: Customer, + message: "Customer created successfully" +} +``` + +**Example:** + +```typescript +import type { CreateCustomerRequest } from "@/types/api"; + +const newCustomer: CreateCustomerRequest = { + name: "สมชาย ใจดี", + email: "somchai@example.com", + phone: "081-234-5678", + company: "บริษัท ไทยธุรกิจ จำกัด", + address: "123 ถนนสุขุมวิท แขวงคลองตัน เขตคลองเตย กรุงเทพฯ 10110", + customerStatus: "active", + customerType: "corporate", + taxId: "0105551234567", +}; + +const response = await apiClient("/customers", { + method: "POST", + body: JSON.stringify(newCustomer), +}); +``` + +--- + +### 4. Update Customer + +Update an existing customer. + +**Endpoint:** `PUT /customers/:id` + +**Path Parameters:** + +- `id` (required) - Customer ID + +**Request Body:** + +```typescript +{ + name?: string, + email?: string, + phone?: string, + company?: string, + address?: string, + customerStatus?: string, + erpCustomerCode?: string // For ERP sync +} +``` + +**Response:** + +```typescript +{ + success: true, + data: Customer, + message: "Customer updated successfully" +} +``` + +**Example:** + +```typescript +const updates = { + erpCustomerCode: "ERP-001234", +}; + +const response = await apiClient( + `/customers/${customerId}`, + { + method: "PUT", + body: JSON.stringify(updates), + }, +); +``` + +--- + +### 5. Delete Customer + +Delete a customer (soft delete). + +**Endpoint:** `DELETE /customers/:id` + +**Path Parameters:** + +- `id` (required) - Customer ID + +**Response:** + +```typescript +{ + success: true, + data: Customer, + message: "Customer deleted successfully" +} +``` + +**Example:** + +```typescript +const response = await apiClient( + `/customers/${customerId}`, + { + method: "DELETE", + }, +); +``` + +--- + +## Contacts API + +### 1. Get Contacts for Customer + +Get all visible contacts for a customer. + +**Endpoint:** `GET /customers/:customerId/contacts` + +**Path Parameters:** + +- `customerId` (required) - Customer ID + +**Visibility Rules:** +A contact is visible if: + +1. Created by the current user, OR +2. Marked as public (`isPublic: true`), OR +3. Shared with the current user via `contact_shares` + +**Response:** + +```typescript +{ + success: true, + data: Contact[], + count: 5, + message: "Found 5 contact(s)" +} +``` + +**Example:** + +```typescript +const response = await apiClient( + `/customers/${customerId}/contacts`, +); +``` + +--- + +### 2. Create Contact + +Create a new contact for a customer. + +**Endpoint:** `POST /customers/:customerId/contacts` + +**Path Parameters:** + +- `customerId` (required) - Customer ID + +**Request Body:** + +```typescript +{ + name: string, // Required + position?: string, // Optional + phone?: string, // Optional + mobile?: string, // Optional + email?: string, // Optional + isPrimary?: boolean, // Optional (default: false) + notes?: string // Optional +} +``` + +**Response:** + +```typescript +{ + success: true, + data: Contact, + message: "Contact created successfully" +} +``` + +**Example:** + +```typescript +const newContact: CreateContactRequest = { + name: "วิภาวี สุขสันต์", + position: "ผู้จัดการฝ่ายจัดซื้อ", + phone: "02-123-4567", + mobile: "089-876-5432", + email: "wipavi@example.com", + isPrimary: true, + notes: "Key decision maker", +}; + +const response = await apiClient( + `/customers/${customerId}/contacts`, + { + method: "POST", + body: JSON.stringify(newContact), + }, +); +``` + +--- + +### 3. Update Contact + +Update an existing contact. + +**Endpoint:** `PUT /contacts/:contactId` + +**Path Parameters:** + +- `contactId` (required) - Contact ID + +**Rules:** + +- Only the contact creator can update + +**Request Body:** + +```typescript +{ + name?: string, + position?: string, + phone?: string, + mobile?: string, + email?: string, + isPrimary?: boolean, + isPublic?: boolean, + notes?: string +} +``` + +**Response:** + +```typescript +{ + success: true, + data: Contact, + message: "Contact updated successfully" +} +``` + +**Example:** + +```typescript +const updates = { + isPublic: true, + notes: "Shared with team", +}; + +const response = await apiClient( + `/contacts/${contactId}`, + { + method: "PUT", + body: JSON.stringify(updates), + }, +); +``` + +--- + +### 4. Share Contact (Make Public) + +Make a contact visible to all users in the branch. + +**Endpoint:** `POST /contacts/:contactId/share` + +**Path Parameters:** + +- `contactId` (required) - Contact ID + +**Rules:** + +- Only the contact creator can share + +**Response:** + +```typescript +{ + success: true, + data: Contact, + message: "Contact shared successfully" +} +``` + +**Example:** + +```typescript +const response = await apiClient( + `/contacts/${contactId}/share`, + { + method: "POST", + }, +); +``` + +--- + +### 5. Unshare Contact (Make Private) + +Make a contact private (visible only to creator). + +**Endpoint:** `POST /contacts/:contactId/unshare` + +**Path Parameters:** + +- `contactId` (required) - Contact ID + +**Rules:** + +- Only the contact creator can unshare + +**Response:** + +```typescript +{ + success: true, + data: Contact, + message: "Contact unshared successfully" +} +``` + +**Example:** + +```typescript +const response = await apiClient( + `/contacts/${contactId}/unshare`, + { + method: "POST", + }, +); +``` + +--- + +### 6. Delete Contact + +Delete a contact. + +**Endpoint:** `DELETE /contacts/:contactId` + +**Path Parameters:** + +- `contactId` (required) - Contact ID + +**Rules:** + +- Only the contact creator can delete + +**Response:** + +```typescript +{ + success: true, + data: Contact, + message: "Contact deleted successfully" +} +``` + +**Example:** + +```typescript +const response = await apiClient( + `/contacts/${contactId}`, + { + method: "DELETE", + }, +); +``` + +--- + +## Contact Sharing API + +### 1. Share Contact with Specific User + +Share a contact with a specific user (not public). + +**Endpoint:** `POST /contacts/:contactId/share-with` + +**Path Parameters:** + +- `contactId` (required) - Contact ID + +**Request Body:** + +```typescript +{ + targetUserId: string, // Required - User ID to share with + notes?: string // Optional - Notes about the share +} +``` + +**Rules:** + +- Only the contact creator can share +- Cannot share with yourself +- Cannot share non-existent contact +- Cannot share contact from different branch +- Duplicate share prevention + +**Response:** + +```typescript +{ + success: true, + data: ContactShare, + message: "Contact shared successfully" +} +``` + +**Example:** + +```typescript +const shareRequest = { + targetUserId: "user-456", + notes: "Sales lead for Q4 project", +}; + +const response = await apiClient( + `/contacts/${contactId}/share-with`, + { + method: "POST", + body: JSON.stringify(shareRequest), + }, +); +``` + +--- + +### 2. Unshare Contact from Specific User + +Remove sharing from a specific user. + +**Endpoint:** `DELETE /contacts/:contactId/share/:targetUserId` + +**Path Parameters:** + +- `contactId` (required) - Contact ID +- `targetUserId` (required) - User ID to unshare from + +**Rules:** + +- Only the contact creator can unshare + +**Response:** + +```typescript +{ + success: true, + data: ContactShare, + message: "Contact unshared successfully" +} +``` + +**Example:** + +```typescript +const response = await apiClient( + `/contacts/${contactId}/share/${targetUserId}`, + { + method: "DELETE", + }, +); +``` + +--- + +### 3. Get Contact Shares + +Get all shares for a contact. + +**Endpoint:** `GET /contacts/:contactId/shares` + +**Path Parameters:** + +- `contactId` (required) - Contact ID + +**Rules:** + +- Only the contact creator can view shares + +**Response:** + +```typescript +{ + success: true, + data: ContactShare[], + count: 3, + message: "Found 3 share(s)" +} +``` + +**Example:** + +```typescript +const response = await apiClient( + `/contacts/${contactId}/shares`, +); +``` + +--- + +### 4. Get Contacts Shared With Me + +Get all contacts that have been shared with the current user. + +**Endpoint:** `GET /contacts/shared-with-me` + +**Query Parameters:** + +- `customerId` (optional) - Filter by customer ID + +**Response:** + +```typescript +{ + success: true, + data: Contact[], + count: 5, + message: "Found 5 contact(s) shared with you" +} +``` + +**Example:** + +```typescript +// Get all contacts shared with me +const response1 = await apiClient( + "/contacts/shared-with-me", +); + +// Filter by customer +const response2 = await apiClient( + `/contacts/shared-with-me?customerId=${customerId}`, +); +``` + +--- + +## Type Definitions + +All API types are exported from `@/types/api`. + +### Import Example: + +```typescript +import type { + Customer, + Contact, + ContactShare, + CustomerListResponse, + CreateCustomerRequest, + UpdateContactRequest, + ApiResponse, +} from "@/types/api"; +``` + +### Available Types: + +- **Customer Types:** `Customer`, `CreateCustomerRequest`, `UpdateCustomerRequest` +- **Contact Types:** `Contact`, `CreateContactRequest`, `UpdateContactRequest` +- **Share Types:** `ContactShare`, `ShareContactRequest` +- **Response Types:** `SuccessResponse`, `ErrorResponse`, `ApiResponse` +- **List Responses:** `CustomerListResponse`, `ContactListResponse`, `ContactShareListResponse` +- **Single Item Responses:** `CustomerResponse`, `ContactResponse`, `ContactShareResponse` +- **Operation Responses:** `CreateCustomerResponse`, `UpdateCustomerResponse`, etc. + +--- + +## Usage Examples + +### Example 1: Fetch and Display Customers + +```typescript +"use client"; + +import { useEffect, useState } from "react"; +import { apiClient } from "@/lib/api-client"; +import type { CustomerListResponse, Customer } from "@/types/api"; + +export default function CustomerList() { + const [customers, setCustomers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchCustomers() { + try { + const response = await apiClient("/customers?status=active"); + + if (response.success) { + setCustomers(response.data); + } else { + setError(response.error); + } + } catch (err) { + setError("Failed to fetch customers"); + } finally { + setLoading(false); + } + } + + fetchCustomers(); + }, []); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + + return ( +
    + {customers.map(customer => ( +
  • + {customer.name} - {customer.company} +
  • + ))} +
+ ); +} +``` + +--- + +### Example 2: Create New Customer with Form + +```typescript +"use client"; + +import { useState } from "react"; +import { apiClient } from "@/lib/api-client"; +import type { CreateCustomerRequest, CreateCustomerResponse } from "@/types/api"; + +export default function CreateCustomerForm() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + setError(null); + setSuccess(false); + + const formData = new FormData(e.currentTarget); + + const request: CreateCustomerRequest = { + name: formData.get("name") as string, + email: formData.get("email") as string, + phone: formData.get("phone") as string, + company: formData.get("company") as string, + address: formData.get("address") as string, + customerStatus: formData.get("status") as string || undefined + }; + + try { + const response = await apiClient("/customers", { + method: "POST", + body: JSON.stringify(request) + }); + + if (response.success) { + setSuccess(true); + e.currentTarget.reset(); + } else { + setError(response.error); + } + } catch (err) { + setError("Failed to create customer"); + } finally { + setLoading(false); + } + } + + return ( +
+ + + + + + + + + + {error &&
{error}
} + {success &&
Customer created successfully!
} +
+ ); +} +``` + +--- + +### Example 3: Share Contact with User + +```typescript +"use client"; + +import { useState } from "react"; +import { apiClient } from "@/lib/api-client"; +import type { ShareContactWithUserResponse } from "@/types/api"; + +interface ShareContactModalProps { + contactId: string; + onClose: () => void; +} + +export default function ShareContactModal({ contactId, onClose }: ShareContactModalProps) { + const [targetUserId, setTargetUserId] = useState(""); + const [notes, setNotes] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleShare() { + if (!targetUserId) { + setError("Please select a user"); + return; + } + + setLoading(true); + setError(null); + + try { + const response = await apiClient( + `/contacts/${contactId}/share-with`, + { + method: "POST", + body: JSON.stringify({ targetUserId, notes }) + } + ); + + if (response.success) { + onClose(); // Close modal on success + } else { + setError(response.error); + } + } catch (err) { + setError("Failed to share contact"); + } finally { + setLoading(false); + } + } + + return ( +
+

Share Contact

+ + setTargetUserId(e.target.value)} + /> + +