# Front-End API Usage Guide

This document covers how to use the `CollectionDataService` and supporting types to interact with the collection data API, including the promoted relational columns and the featured-item flag.

---

## TypeScript Interfaces

### `ICollectionData`

The base shape for every record returned or sent to the API.

```ts
export type FilterOperator = 'equals' | 'contains' | 'greater_than' | 'less_than' | 'in';
export type FilterType = 'string' | 'number' | 'date';

export interface ICollectionFilter {
  field: string;
  operator: FilterOperator;
  value: any;
  type?: FilterType;
}

export interface ICollectionFindParams {
  collection_id: string;
  website_id: string;
  // Native column filters — use indexes, fast
  user_ref_id?: number;
  owner_user_id?: number;
  entity_ref_id?: number;
  status_id?: number;
  is_featured?: boolean;
  // JSON field filters — flexible
  filters?: ICollectionFilter[];
  sortBy?: string;
  sortOrder?: 'ASC' | 'DESC';
  limit?: number;
  offset?: number;
}

export interface ICollectionData<T = any, J = any> {
  id: number;
  slug?: string;
  collection_id: string;
  parent_id: number;
  website_id: string;
  company_id?: string;

  // Promoted relational columns
  user_ref_id?: number | null;
  owner_user_id?: number | null;
  entity_ref_id?: number | null;
  status_id?: number | null;
  sort_order?: number;
  is_featured?: boolean;

  // Flexible JSON payload
  data: T;

  created_at?: string;
  updated_at?: string;
  created_by?: number;
  updated_by?: number;

  // Client-side helpers
  selected?: boolean;
  children?: ICollectionData<J>[];
}
```

---

### Collection name constants

```ts
export enum CollectionNames {
  WebsiteId   = 'fieldforge',

  Customers   = 'customers',
  Sites       = 'sites',
  Machines    = 'machines',
  MachineTypes = 'machine_types',
  MachineParts = 'machine_parts',

  Jobs        = 'jobs',
  JobTypes    = 'job_types',
  JobStatuses = 'job_statuses',
  JobWork     = 'job_work',

  Parts       = 'parts',
  PartTypes   = 'part_types',
  StockLocations = 'stock_locations',
  LocationStock  = 'location_stock',
  StockTransactions = 'stock_transactions',

  Invoices    = 'invoices',
}
```

---

## Service

```ts
@Injectable({ providedIn: 'root' })
export class CollectionDataService<T = any, J = any> {
  private apiUrl = `${Constants.ApiBase}/collection-data`;

  constructor(private http: HttpClient) {}

  // ─── List ────────────────────────────────────────────────────────────────

  list(
    collectionId: string,
    options: {
      parentId?: number;
      childrenCollection?: string;
      sortBy?: string;
      sortOrder?: 'ASC' | 'DESC';
      userRefId?: number;
      ownerUserId?: number;
      entityRefId?: number;
      statusId?: number;
      isFeatured?: boolean;
    } = {}
  ): Observable<ICollectionData<T, J>[]> {
    const params = new URLSearchParams({
      collectionId,
      websiteId: CollectionNames.WebsiteId,
      ...(options.parentId         != null && { parent_id: String(options.parentId) }),
      ...(options.childrenCollection       && { childrenCollection: options.childrenCollection }),
      ...(options.sortBy                   && { sortBy: options.sortBy }),
      ...(options.sortOrder                && { sortOrder: options.sortOrder }),
      ...(options.userRefId        != null && { user_ref_id: String(options.userRefId) }),
      ...(options.ownerUserId      != null && { owner_user_id: String(options.ownerUserId) }),
      ...(options.entityRefId      != null && { entity_ref_id: String(options.entityRefId) }),
      ...(options.statusId         != null && { status_id: String(options.statusId) }),
      ...(options.isFeatured       != null && { is_featured: String(Number(options.isFeatured)) }),
    });
    return this.http.get<ICollectionData<T, J>[]>(`${this.apiUrl}/list.php?${params}`);
  }

  pagedList(
    collectionId: string,
    options: {
      parentId?: number;
      limit?: number;
      offset?: number;
      sortBy?: string;
      sortOrder?: 'ASC' | 'DESC';
      userRefId?: number;
      ownerUserId?: number;
      entityRefId?: number;
      statusId?: number;
      isFeatured?: boolean;
    } = {}
  ): Observable<ICollectionData<T, J>[]> {
    const params = new URLSearchParams({
      collectionId,
      websiteId: CollectionNames.WebsiteId,
      ...(options.parentId    != null && { parent_id: String(options.parentId) }),
      ...(options.limit       != null && { limit: String(options.limit) }),
      ...(options.offset      != null && { offset: String(options.offset) }),
      ...(options.sortBy              && { sortBy: options.sortBy }),
      ...(options.sortOrder           && { sortOrder: options.sortOrder }),
      ...(options.userRefId   != null && { user_ref_id: String(options.userRefId) }),
      ...(options.ownerUserId != null && { owner_user_id: String(options.ownerUserId) }),
      ...(options.entityRefId != null && { entity_ref_id: String(options.entityRefId) }),
      ...(options.statusId    != null && { status_id: String(options.statusId) }),
      ...(options.isFeatured  != null && { is_featured: String(Number(options.isFeatured)) }),
    });
    return this.http.get<ICollectionData<T, J>[]>(`${this.apiUrl}/paged-list.php?${params}`);
  }

  // ─── Find (advanced POST search) ─────────────────────────────────────────

  find<R = T>(params: ICollectionFindParams): Observable<ICollectionData<R>[]> {
    return this.http.post<ICollectionData<R>[]>(`${this.apiUrl}/find.php`, params);
  }

  // ─── Single record ────────────────────────────────────────────────────────

  getById(id: number | string): Observable<ICollectionData<T, J>> {
    return this.http.get<ICollectionData<T, J>>(`${this.apiUrl}/get.php?id=${id}`);
  }

  // ─── Save (create or update) ──────────────────────────────────────────────

  save(data: ICollectionData<T, J>): Observable<ICollectionData<T, J>> {
    return this.http.post<ICollectionData<T, J>>(`${this.apiUrl}/save.php`, data);
  }

  saveRange(items: ICollectionData<T, J>[]): Observable<ICollectionData<T, J>[]> {
    return this.http.post<ICollectionData<T, J>[]>(`${this.apiUrl}/save-range.php`, items);
  }

  // ─── Delete ───────────────────────────────────────────────────────────────

  delete(id: number): Observable<any> {
    return this.http.delete(`${this.apiUrl}/delete.php?id=${id}`);
  }
}
```

---

## Data Types

Define typed interfaces for each collection's `data` field.

```ts
export interface IJob {
  title: string;
  description?: string;
  scheduled_date?: string;
  notes?: string;
  job_type?: string;
}

export interface IJobStatus {
  name: string;
  color: string;
  is_default: boolean;
  is_final: boolean;
}

export interface ICustomer {
  name: string;
  email: string;
  phone: string;
  address: string;
  city: string;
  notes?: string;
}

export interface ISite {
  name: string;
  address: string;
  city: string;
  contact_name?: string;
  contact_phone?: string;
  notes?: string;
}

export interface IMachine {
  name: string;
  machine_type: string;
  serial_number?: string;
  model?: string;
  notes?: string;
  field_values?: Record<string, string>;
}

export interface IPart {
  name: string;
  sku: string;
  item_type: string;
  cost: number;
  price: number;
  unit: string;
  description?: string;
}

export interface IJobWork {
  description: string;
  report?: string;
  mileage?: number;
  parts_used?: Array<{
    part_id: number;
    part_name: string;
    part_sku: string;
    quantity: number;
    unit: string;
  }>;
}
```

---

## Usage Examples

### Get all customers

```ts
this.collectionDataService.list<ICustomer>(CollectionNames.Customers)
  .subscribe(customers => { ... });
```

---

### Get machines for a site

`parent_id` is the site id.

```ts
this.collectionDataService.list<IMachine>(CollectionNames.Machines, {
  parentId: siteId
}).subscribe(machines => { ... });
```

---

### Get all jobs for a machine

`entity_ref_id` is the machine id — uses the index directly.

```ts
this.collectionDataService.list<IJob>(CollectionNames.Jobs, {
  entityRefId: machineId,
  sortBy: 'created_at',
  sortOrder: 'DESC'
}).subscribe(jobs => { ... });
```

---

### Get open jobs for a technician

```ts
this.collectionDataService.list<IJob>(CollectionNames.Jobs, {
  userRefId: technicianUserId,
  statusId: openStatusId
}).subscribe(jobs => { ... });
```

---

### Get job statuses ordered by sort_order (for a status dropdown)

```ts
this.collectionDataService.list<IJobStatus>(CollectionNames.JobStatuses, {
  sortBy: 'sort_order',
  sortOrder: 'ASC'
}).subscribe(statuses => { ... });
```

---

### Get the featured item for a collection

`is_featured` is a top-level indexed flag. Use it to fetch only featured records for one collection within the current website.

```ts
this.collectionDataService.list<ICustomer>(CollectionNames.Customers, {
  isFeatured: true,
  sortBy: 'updated_at',
  sortOrder: 'DESC'
}).subscribe(featuredCustomers => { ... });
```

---

### Create a new job

All relational references go in the top-level columns, not inside `data`.

```ts
const job: ICollectionData<IJob> = {
  id: 0,
  collection_id: CollectionNames.Jobs,
  parent_id: customerId,       // customer this job belongs to
  website_id: CollectionNames.WebsiteId,
  entity_ref_id: machineId,    // machine being serviced
  user_ref_id: technicianId,   // assigned technician
  status_id: newStatusId,      // job status node id
  sort_order: 0,
  is_featured: false,
  data: {
    title: 'Annual Service',
    description: 'Full service including oil change and filter replacement',
    scheduled_date: '2026-04-01',
    job_type: 'Onsite FSR',
    notes: '',
  }
};

this.collectionDataService.save(job).subscribe(saved => { ... });
```

---

### Update a job — change status and reassign technician

Only the fields you include are updated. Everything else is left unchanged.

```ts
this.collectionDataService.save({
  id: jobId,
  collection_id: CollectionNames.Jobs,
  parent_id: customerId,
  website_id: CollectionNames.WebsiteId,
  status_id: inProgressStatusId,
  user_ref_id: newTechnicianId,
  data: existingJob.data
}).subscribe(updated => { ... });
```

---

### Reorder job statuses (drag and drop)

After a drag-and-drop reorder, call `saveRange` with updated `sort_order` values.

```ts
const reordered = statuses.map((s, index) => ({
  ...s,
  sort_order: index
}));

this.collectionDataService.saveRange(reordered).subscribe(() => { ... });
```

---

### Advanced search with POST (find)

Use `find` when you need JSON field filters combined with native column filters.

```ts
// Jobs for machine 66, assigned to technician 2, filtered by scheduled date
this.collectionDataService.find<IJob>({
  collection_id: CollectionNames.Jobs,
  website_id: CollectionNames.WebsiteId,
  entity_ref_id: 66,
  user_ref_id: 2,
  filters: [
    {
      field: 'scheduled_date',
      operator: 'greater_than',
      value: '2026-03-01',
      type: 'date'
    }
  ],
  sortBy: 'sort_order',
  sortOrder: 'ASC'
}).subscribe(jobs => { ... });
```

---

### Technician dashboard — all open jobs

```ts
this.collectionDataService.find<IJob>({
  collection_id: CollectionNames.Jobs,
  website_id: CollectionNames.WebsiteId,
  user_ref_id: currentUser.id,
  status_id: openStatusId,
  sortBy: 'sort_order',
  sortOrder: 'ASC'
}).subscribe(jobs => { ... });
```

---

### Find featured records with POST

```ts
this.collectionDataService.find<ICustomer>({
  collection_id: CollectionNames.Customers,
  website_id: CollectionNames.WebsiteId,
  is_featured: true,
  limit: 1,
  sortBy: 'updated_at',
  sortOrder: 'DESC'
}).subscribe(featuredCustomers => { ... });
```

---

### Machine service history (paginated)

```ts
this.collectionDataService.pagedList<IJob>(CollectionNames.Jobs, {
  entityRefId: machineId,
  sortBy: 'created_at',
  sortOrder: 'DESC',
  limit: 20,
  offset: 0
}).subscribe(history => { ... });
```

---

### Get sites for a customer with their machines

```ts
this.collectionDataService.list<ISite>(CollectionNames.Sites, {
  parentId: customerId,
  childrenCollection: CollectionNames.Machines,
  sortBy: 'sort_order',
  sortOrder: 'ASC'
}).subscribe(sites => {
  // sites[0].children = machines for that site
});
```

---

## Column Responsibilities

| What you're expressing | Where it lives |
|---|---|
| Which machine a job is for | `entity_ref_id` |
| Which technician is assigned | `user_ref_id` |
| Who owns the record | `owner_user_id` |
| Current workflow state | `status_id` (points to a `job_statuses` node) |
| Display order in a list | `sort_order` |
| Whether the item is featured | `is_featured` |
| Which customer/site a record belongs to | `parent_id` |
| Everything else (title, notes, dates, fields) | `data` JSON |

---

## Rules

- **Never store `machine_id`, `technician`, or `status` inside the `data` JSON field.** Those belong in the promoted columns above.
- `data` is for flexible attributes only: titles, descriptions, scheduled dates, custom field values, notes.
- `sort_order` is sibling-relative — `0, 1, 2...` within a collection or parent group, not globally unique.
- `is_featured` belongs on the top-level record so the API can filter it directly per `collection_id` and `website_id`.
- Pass `null` to explicitly clear a relational column. Omitting the key leaves it unchanged.
